From 6d9185a248d101b07eecaf8fd60b18129545fd33 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:28:59 +0000 Subject: [PATCH 001/596] Update version to v12.55.1 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index d0dd80eb4e1d..30aef88fb12c 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.55.0'; + const VERSION = '12.55.1'; /** * The base path for the Laravel installation. From 38aabd4c2dcf1148007c66f659cf88f22ace0f15 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:30:34 +0000 Subject: [PATCH 002/596] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e393e3c6371..097c18a3c76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.55.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.55.1...12.x) + +## [v12.55.1](https://github.com/laravel/framework/compare/v12.55.0...v12.55.1) - 2026-03-18 + +* [12.x] Correct truncate exceptions at by [@bretto36](https://github.com/bretto36) in https://github.com/laravel/framework/pull/59239 +* [12.x] Fix float pluralization in trans_choice() by [@JulianGlueck](https://github.com/JulianGlueck) in https://github.com/laravel/framework/pull/59268 +* [12.x] Fix tests on PHP 8.5 by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/59251 ## [v12.55.0](https://github.com/laravel/framework/compare/v12.54.1...v12.55.0) - 2026-03-17 From d75a3e8141523bceaf91b1d5415e079c8b98d9b5 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:12:00 +0000 Subject: [PATCH 003/596] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b12504f1e43a..463836965740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.1.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.1.1...13.x) + +## [v13.1.1](https://github.com/laravel/framework/compare/v13.1.0...v13.1.1) - 2026-03-18 + +* Break queue dependency by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/59275 ## [v13.1.0](https://github.com/laravel/framework/compare/v13.0.0...v13.1.0) - 2026-03-18 From 11bc76af7a1e440052f2e8b4e4c93169e4255b1a Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Wed, 18 Mar 2026 19:39:03 +0100 Subject: [PATCH 004/596] feat(queue): support enums in `#[Queue]` and `#[Connection]` (#59278) * feat(queue): support enums in `#[Queue]` and `#[Connection]` * Update Connection.php * Update Queue.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Queue/Attributes/Connection.php | 3 ++- src/Illuminate/Queue/Attributes/Queue.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Attributes/Connection.php b/src/Illuminate/Queue/Attributes/Connection.php index 7f7bd0e7d1f2..514b7790b5b0 100644 --- a/src/Illuminate/Queue/Attributes/Connection.php +++ b/src/Illuminate/Queue/Attributes/Connection.php @@ -3,6 +3,7 @@ namespace Illuminate\Queue\Attributes; use Attribute; +use BackedEnum; #[Attribute(Attribute::TARGET_CLASS)] class Connection @@ -12,7 +13,7 @@ class Connection * * @param string $connection */ - public function __construct(public string $connection) + public function __construct(public BackedEnum|string $connection) { // } diff --git a/src/Illuminate/Queue/Attributes/Queue.php b/src/Illuminate/Queue/Attributes/Queue.php index be886be30fcc..89a9f1948d9d 100644 --- a/src/Illuminate/Queue/Attributes/Queue.php +++ b/src/Illuminate/Queue/Attributes/Queue.php @@ -3,6 +3,7 @@ namespace Illuminate\Queue\Attributes; use Attribute; +use BackedEnum; #[Attribute(Attribute::TARGET_CLASS)] class Queue @@ -12,7 +13,7 @@ class Queue * * @param string $queue */ - public function __construct(public string $queue) + public function __construct(public BackedEnum|string $queue) { // } From e4223623a04f8c7ffd1ecc82af430fe7292ef007 Mon Sep 17 00:00:00 2001 From: Giacomo Rizzi Date: Wed, 18 Mar 2026 19:45:21 +0100 Subject: [PATCH 005/596] Improve raw SQL binding substitution performance (#59277) --- src/Illuminate/Database/Query/Grammars/Grammar.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 5b61c009121c..429ae1da4fd2 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -1621,6 +1621,7 @@ public function substituteBindingsIntoRawSql($sql, $bindings) $bindings = array_map(fn ($value) => $this->escape($value, is_resource($value) || gettype($value) === 'resource (closed)'), $bindings); $query = ''; + $bindingIndex = 0; $isStringLiteral = false; @@ -1638,7 +1639,7 @@ public function substituteBindingsIntoRawSql($sql, $bindings) $query .= $char; $isStringLiteral = ! $isStringLiteral; } elseif ($char === '?' && ! $isStringLiteral) { // Substitutable binding... - $query .= array_shift($bindings) ?? '?'; + $query .= $bindings[$bindingIndex++] ?? '?'; } else { // Normal character... $query .= $char; } From 6914aa6f48fad553110f367c81cc65deac00080a Mon Sep 17 00:00:00 2001 From: Jesper Beisner Date: Fri, 20 Mar 2026 16:07:11 +0100 Subject: [PATCH 006/596] fix: add missing negate for SeeInHtml assertion (#59303) --- src/Illuminate/Testing/TestView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Testing/TestView.php b/src/Illuminate/Testing/TestView.php index f7a6212725fa..3469363b4d5f 100644 --- a/src/Illuminate/Testing/TestView.php +++ b/src/Illuminate/Testing/TestView.php @@ -255,7 +255,7 @@ public function assertDontSeeText($value, $escape = true) $values = $escape ? array_map(e(...), $value) : $value; - PHPUnit::assertThat($values, new SeeInHtml($this->rendered)); + PHPUnit::assertThat($values, new SeeInHtml($this->rendered, negate: true)); return $this; } From 825418937fb8fc76f50e0c6acbb48d761dd34996 Mon Sep 17 00:00:00 2001 From: Richard van Baarsen Date: Fri, 20 Mar 2026 16:16:26 +0100 Subject: [PATCH 007/596] [13.x] Allow for passing enums to attributes (#59297) * Allow for UnitEnum in attributes * Remove redundant PhpDoc * Allow for UnitEnum in Storage attribute * Add tests for enum database Connection attribute * Add tests for enum filesystem Storage attribute --- src/Illuminate/Cache/RedisTaggedCache.php | 10 +++--- src/Illuminate/Cache/TaggedCache.php | 4 +-- src/Illuminate/Container/Attributes/Bind.php | 2 +- .../Container/Attributes/Storage.php | 3 +- src/Illuminate/Contracts/Cache/Repository.php | 22 ++++++------ .../Eloquent/Attributes/Connection.php | 5 +-- .../Queue/Attributes/Connection.php | 6 ++-- src/Illuminate/Queue/Attributes/Queue.php | 6 ++-- .../ContextualAttributeBindingTest.php | 21 +++++++++-- .../DatabaseEloquentModelAttributesTest.php | 36 +++++++++++++++++++ 10 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/Illuminate/Cache/RedisTaggedCache.php b/src/Illuminate/Cache/RedisTaggedCache.php index f71fde0e4ce2..e68ed76dbf44 100644 --- a/src/Illuminate/Cache/RedisTaggedCache.php +++ b/src/Illuminate/Cache/RedisTaggedCache.php @@ -16,7 +16,7 @@ class RedisTaggedCache extends TaggedCache /** * Store an item in the cache if the key does not exist. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|int|null $ttl * @return bool @@ -44,7 +44,7 @@ public function add($key, $value, $ttl = null) /** * Store an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|int|null $ttl * @return bool @@ -72,7 +72,7 @@ public function put($key, $value, $ttl = null) /** * Increment the value of an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return int|bool */ @@ -88,7 +88,7 @@ public function increment($key, $value = 1) /** * Decrement the value of an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return int|bool */ @@ -102,7 +102,7 @@ public function decrement($key, $value = 1) /** * Store an item in the cache indefinitely. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return bool */ diff --git a/src/Illuminate/Cache/TaggedCache.php b/src/Illuminate/Cache/TaggedCache.php index 9d523d39b2a5..bca493a94f8f 100644 --- a/src/Illuminate/Cache/TaggedCache.php +++ b/src/Illuminate/Cache/TaggedCache.php @@ -53,7 +53,7 @@ public function putMany(array $values, $ttl = null) /** * Increment the value of an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return int|bool */ @@ -65,7 +65,7 @@ public function increment($key, $value = 1) /** * Decrement the value of an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return int|bool */ diff --git a/src/Illuminate/Container/Attributes/Bind.php b/src/Illuminate/Container/Attributes/Bind.php index 3e74b944b97d..4e3482ad142d 100644 --- a/src/Illuminate/Container/Attributes/Bind.php +++ b/src/Illuminate/Container/Attributes/Bind.php @@ -29,7 +29,7 @@ class Bind * Create a new attribute instance. * * @param class-string $concrete - * @param non-empty-array|non-empty-string|\UnitEnum $environments + * @param non-empty-array|non-empty-string|\UnitEnum $environments * * @throws \InvalidArgumentException */ diff --git a/src/Illuminate/Container/Attributes/Storage.php b/src/Illuminate/Container/Attributes/Storage.php index b9a16d19817a..e80e9d377e17 100644 --- a/src/Illuminate/Container/Attributes/Storage.php +++ b/src/Illuminate/Container/Attributes/Storage.php @@ -5,6 +5,7 @@ use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; +use UnitEnum; #[Attribute(Attribute::TARGET_PARAMETER)] class Storage implements ContextualAttribute @@ -12,7 +13,7 @@ class Storage implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public ?string $disk = null) + public function __construct(public UnitEnum|string|null $disk = null) { } diff --git a/src/Illuminate/Contracts/Cache/Repository.php b/src/Illuminate/Contracts/Cache/Repository.php index d555c451f647..cdbbdb1ace92 100644 --- a/src/Illuminate/Contracts/Cache/Repository.php +++ b/src/Illuminate/Contracts/Cache/Repository.php @@ -12,7 +12,7 @@ interface Repository extends CacheInterface * * @template TCacheValue * - * @param \BackedEnum|\UnitEnum|array|string $key + * @param \UnitEnum|array|string $key * @param TCacheValue|(\Closure(): TCacheValue) $default * @return (TCacheValue is null ? mixed : TCacheValue) */ @@ -21,7 +21,7 @@ public function pull($key, $default = null); /** * Store an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|int|null $ttl * @return bool @@ -31,7 +31,7 @@ public function put($key, $value, $ttl = null); /** * Store an item in the cache if the key does not exist. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|int|null $ttl * @return bool @@ -41,7 +41,7 @@ public function add($key, $value, $ttl = null); /** * Increment the value of an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return int|bool */ @@ -50,7 +50,7 @@ public function increment($key, $value = 1); /** * Decrement the value of an item in the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return int|bool */ @@ -59,7 +59,7 @@ public function decrement($key, $value = 1); /** * Store an item in the cache indefinitely. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param mixed $value * @return bool */ @@ -70,7 +70,7 @@ public function forever($key, $value); * * @template TCacheValue * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param \DateTimeInterface|\DateInterval|\Closure|int|null $ttl * @param \Closure(): TCacheValue $callback * @return TCacheValue @@ -82,7 +82,7 @@ public function remember($key, $ttl, Closure $callback); * * @template TCacheValue * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param \Closure(): TCacheValue $callback * @return TCacheValue */ @@ -93,7 +93,7 @@ public function sear($key, Closure $callback); * * @template TCacheValue * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param \Closure(): TCacheValue $callback * @return TCacheValue */ @@ -102,7 +102,7 @@ public function rememberForever($key, Closure $callback); /** * Set the expiration of a cached item. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @param \DateTimeInterface|\DateInterval|int $ttl * @return bool */ @@ -111,7 +111,7 @@ public function touch($key, $ttl); /** * Remove an item from the cache. * - * @param \BackedEnum|\UnitEnum|string $key + * @param \UnitEnum|string $key * @return bool */ public function forget($key); diff --git a/src/Illuminate/Database/Eloquent/Attributes/Connection.php b/src/Illuminate/Database/Eloquent/Attributes/Connection.php index d02fcc502f52..cc182ac4549f 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Connection.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Connection.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent\Attributes; use Attribute; +use UnitEnum; #[Attribute(Attribute::TARGET_CLASS)] class Connection @@ -10,9 +11,9 @@ class Connection /** * Create a new attribute instance. * - * @param string $name + * @param UnitEnum|string $name */ - public function __construct(public string $name) + public function __construct(public UnitEnum|string $name) { } } diff --git a/src/Illuminate/Queue/Attributes/Connection.php b/src/Illuminate/Queue/Attributes/Connection.php index 514b7790b5b0..c5b1db67ccc5 100644 --- a/src/Illuminate/Queue/Attributes/Connection.php +++ b/src/Illuminate/Queue/Attributes/Connection.php @@ -3,7 +3,7 @@ namespace Illuminate\Queue\Attributes; use Attribute; -use BackedEnum; +use UnitEnum; #[Attribute(Attribute::TARGET_CLASS)] class Connection @@ -11,9 +11,9 @@ class Connection /** * Create a new attribute instance. * - * @param string $connection + * @param UnitEnum|string $connection */ - public function __construct(public BackedEnum|string $connection) + public function __construct(public UnitEnum|string $connection) { // } diff --git a/src/Illuminate/Queue/Attributes/Queue.php b/src/Illuminate/Queue/Attributes/Queue.php index 89a9f1948d9d..a1892e99c32c 100644 --- a/src/Illuminate/Queue/Attributes/Queue.php +++ b/src/Illuminate/Queue/Attributes/Queue.php @@ -3,7 +3,7 @@ namespace Illuminate\Queue\Attributes; use Attribute; -use BackedEnum; +use UnitEnum; #[Attribute(Attribute::TARGET_CLASS)] class Queue @@ -11,9 +11,9 @@ class Queue /** * Create a new attribute instance. * - * @param string $queue + * @param UnitEnum|string $queue */ - public function __construct(public BackedEnum|string $queue) + public function __construct(public UnitEnum|string $queue) { // } diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index d085cd2fa8c1..3341f7760caf 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -272,6 +272,8 @@ public function testStorageAttribute() $manager = m::mock(FilesystemManager::class); $manager->shouldReceive('disk')->with('foo')->andReturn(m::mock(Filesystem::class)); $manager->shouldReceive('disk')->with('bar')->andReturn(m::mock(Filesystem::class)); + $manager->shouldReceive('disk')->with(StorageDiskUnitEnum::unit)->andReturn(m::mock(Filesystem::class)); + $manager->shouldReceive('disk')->with(StorageDiskBackedEnum::Backed)->andReturn(m::mock(Filesystem::class)); return $manager; }); @@ -360,6 +362,16 @@ public function __construct( } } +enum StorageDiskUnitEnum +{ + case unit; +} + +enum StorageDiskBackedEnum: string +{ + case Backed = 'backed'; +} + interface ContainerTestContract { } @@ -530,9 +542,12 @@ public function __construct(#[RouteParameter('foo')] Model $foo, #[RouteParamete final class StorageTest { - public function __construct(#[Storage('foo')] Filesystem $foo, #[Storage('bar')] Filesystem $bar) - { - } + public function __construct( + #[Storage('foo')] Filesystem $foo, + #[Storage('bar')] Filesystem $bar, + #[Storage(StorageDiskUnitEnum::unit)] Filesystem $unit, + #[Storage(StorageDiskBackedEnum::Backed)] Filesystem $backed, + ) {} } final class GiveTestSimple diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index b1bea02fa8ed..2080920c495b 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -97,6 +97,20 @@ public function test_connection_attribute(): void $this->assertSame('secondary', $model->getConnectionName()); } + public function test_connection_attribute_with_unit_enum(): void + { + $model = new ModelWithUnitEnumConnectionAttribute; + + $this->assertSame('secondary', $model->getConnectionName()); + } + + public function test_connection_attribute_with_backed_enum(): void + { + $model = new ModelWithBackedEnumConnectionAttribute; + + $this->assertSame('secondary', $model->getConnectionName()); + } + public function test_timestamps_attribute(): void { $model = new ModelWithTimestampsFalseAttribute; @@ -248,6 +262,16 @@ public function test_is_ignoring_touch_with_timestamps_attribute(): void } } +enum ConnectionUnitEnum +{ + case secondary; +} + +enum ConnectionBackedEnum: string +{ + case Secondary = 'secondary'; +} + #[Table('custom_table_name')] class ModelWithTableAttribute extends Model { @@ -296,6 +320,18 @@ class ModelWithConnectionAttribute extends Model // } +#[Connection(ConnectionUnitEnum::secondary)] +class ModelWithUnitEnumConnectionAttribute extends Model +{ + // +} + +#[Connection(ConnectionBackedEnum::Secondary)] +class ModelWithBackedEnumConnectionAttribute extends Model +{ + // +} + #[Table(timestamps: false)] class ModelWithTimestampsFalseAttribute extends Model { From c73da80eb231550cd8f9e31aacfa1abb0e86de51 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 20 Mar 2026 15:16:58 +0000 Subject: [PATCH 008/596] Apply fixes from StyleCI --- tests/Container/ContextualAttributeBindingTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index 3341f7760caf..78e321033f85 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -547,7 +547,8 @@ public function __construct( #[Storage('bar')] Filesystem $bar, #[Storage(StorageDiskUnitEnum::unit)] Filesystem $unit, #[Storage(StorageDiskBackedEnum::Backed)] Filesystem $backed, - ) {} + ) { + } } final class GiveTestSimple From 30f1e508a3ccc9a0a832f19382a7896fc1d9c6ee Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 20 Mar 2026 15:20:47 +0000 Subject: [PATCH 009/596] [13.x] Add releaseOnSignal param to withoutOverlapping (#59298) * releaseOnSignal * couple to without overlapping * make default --------- Co-authored-by: Taylor Otwell --- .../Console/Scheduling/CallbackEvent.php | 4 +-- src/Illuminate/Console/Scheduling/Event.php | 26 +++++++++++++++++++ .../Console/Scheduling/ManagesAttributes.php | 13 +++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/CallbackEvent.php b/src/Illuminate/Console/Scheduling/CallbackEvent.php index 9ee9a6e46e38..4a754bd6329c 100644 --- a/src/Illuminate/Console/Scheduling/CallbackEvent.php +++ b/src/Illuminate/Console/Scheduling/CallbackEvent.php @@ -135,7 +135,7 @@ protected function execute($container) * * @throws \LogicException */ - public function withoutOverlapping($expiresAt = 1440) + public function withoutOverlapping($expiresAt = 1440, $releaseOnTerminationSignals = true) { if (! isset($this->description)) { throw new LogicException( @@ -143,7 +143,7 @@ public function withoutOverlapping($expiresAt = 1440) ); } - return parent::withoutOverlapping($expiresAt); + return parent::withoutOverlapping($expiresAt, $releaseOnTerminationSignals); } /** diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index bc50be5b3421..246acd2ce359 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -131,6 +131,8 @@ public function run(Container $container) return; } + $this->ensureMutexIsReleasedOnSignal(); + $exitCode = $this->start($container); if (! $this->runInBackground) { @@ -841,6 +843,30 @@ public function createMutexNameUsing(Closure|string $mutexName) return $this; } + /** + * Ensure the mutex is released if the process receives a termination signal. + * + * @return void + */ + protected function ensureMutexIsReleasedOnSignal() + { + if (! $this->releaseOnTerminationSignals || + $this->runInBackground || + ! extension_loaded('pcntl')) { + return; + } + + pcntl_async_signals(true); + + foreach ([SIGTERM, SIGINT, SIGQUIT] as $signal) { + pcntl_signal($signal, function () { + $this->removeMutex(); + + exit(1); + }); + } + } + /** * Delete the mutex for the event. * diff --git a/src/Illuminate/Console/Scheduling/ManagesAttributes.php b/src/Illuminate/Console/Scheduling/ManagesAttributes.php index 096455035f2d..d541dc0ee899 100644 --- a/src/Illuminate/Console/Scheduling/ManagesAttributes.php +++ b/src/Illuminate/Console/Scheduling/ManagesAttributes.php @@ -62,6 +62,13 @@ trait ManagesAttributes */ public $withoutOverlapping = false; + /** + * Indicates if the mutex should be released when the process receives a termination signal. + * + * @var bool + */ + public $releaseOnTerminationSignals = true; + /** * Indicates if the command should only be allowed to run on one server for each cron expression. * @@ -156,17 +163,21 @@ public function evenWhenPaused() /** * Do not allow the event to overlap each other. + * * The expiration time of the underlying cache lock may be specified in minutes. * * @param int $expiresAt + * @param bool $releaseOnTerminationSignals * @return $this */ - public function withoutOverlapping($expiresAt = 1440) + public function withoutOverlapping($expiresAt = 1440, $releaseOnTerminationSignals = true) { $this->withoutOverlapping = true; $this->expiresAt = $expiresAt; + $this->releaseOnTerminationSignals = $releaseOnTerminationSignals; + return $this->skip(function () { return $this->mutex->exists($this); }); From b0c222fffd989f6e1327771f84d027c5d8fc1152 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Fri, 20 Mar 2026 11:30:21 -0400 Subject: [PATCH 010/596] Add symmetrical, expressive attributes (#59284) * Add `#[Aliases]` * Revive `#[DateFormat]` * Revive `#[WithoutTimestamps]` * Revive `#[WithoutIncrementing]` --- src/Illuminate/Console/Attributes/Aliases.php | 19 +++++ src/Illuminate/Console/Command.php | 7 ++ .../Eloquent/Attributes/DateFormat.php | 19 +++++ .../Attributes/WithoutIncrementing.php | 17 ++++ .../Eloquent/Attributes/WithoutTimestamps.php | 17 ++++ .../Eloquent/Concerns/HasAttributes.php | 5 +- .../Eloquent/Concerns/HasTimestamps.php | 5 +- src/Illuminate/Database/Eloquent/Model.php | 9 +- tests/Console/CommandTest.php | 35 ++++++++ .../DatabaseEloquentModelAttributesTest.php | 84 +++++++++++++++++++ 10 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 src/Illuminate/Console/Attributes/Aliases.php create mode 100644 src/Illuminate/Database/Eloquent/Attributes/DateFormat.php create mode 100644 src/Illuminate/Database/Eloquent/Attributes/WithoutIncrementing.php create mode 100644 src/Illuminate/Database/Eloquent/Attributes/WithoutTimestamps.php diff --git a/src/Illuminate/Console/Attributes/Aliases.php b/src/Illuminate/Console/Attributes/Aliases.php new file mode 100644 index 000000000000..e3e9669d3142 --- /dev/null +++ b/src/Illuminate/Console/Attributes/Aliases.php @@ -0,0 +1,19 @@ +getAttributes(Hidden::class)) > 0) { $this->hidden = true; } + + $aliases = $reflection->getAttributes(Aliases::class); + + if (count($aliases) > 0) { + $this->aliases = $aliases[0]->newInstance()->aliases; + } } /** diff --git a/src/Illuminate/Database/Eloquent/Attributes/DateFormat.php b/src/Illuminate/Database/Eloquent/Attributes/DateFormat.php new file mode 100644 index 000000000000..20bcd53feb5d --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/DateFormat.php @@ -0,0 +1,19 @@ +casts, $this->casts()), ); - $this->dateFormat ??= static::resolveClassAttribute(Table::class)->dateFormat ?? null; + $this->dateFormat ??= static::resolveClassAttribute(DateFormat::class, 'format') + ?? static::resolveClassAttribute(Table::class)->dateFormat + ?? null; if (empty($this->appends)) { $this->appends = static::resolveClassAttribute(Appends::class, 'columns') ?? []; diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php index 6f1f901d5570..e612cf5db94f 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Attributes\Initialize; use Illuminate\Database\Eloquent\Attributes\Table; +use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Date; @@ -32,7 +33,9 @@ trait HasTimestamps public function initializeHasTimestamps() { if ($this->timestamps === true) { - if (($table = static::resolveClassAttribute(Table::class)) && $table->timestamps !== null) { + if (static::resolveClassAttribute(WithoutTimestamps::class) !== null) { + $this->timestamps = false; + } elseif (($table = static::resolveClassAttribute(Table::class)) && $table->timestamps !== null) { $this->timestamps = $table->timestamps; } } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 09074b5072bd..1993b73f6eb8 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\Attributes\Scope as LocalScope; use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder; +use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; @@ -445,8 +446,12 @@ public function initializeModelAttributes() $this->keyType = $table->keyType; } - if ($this->incrementing === true && $table && $table->incrementing !== null) { - $this->incrementing = $table->incrementing; + if ($this->incrementing === true) { + if (static::resolveClassAttribute(WithoutIncrementing::class) !== null) { + $this->incrementing = false; + } elseif ($table && $table->incrementing !== null) { + $this->incrementing = $table->incrementing; + } } } diff --git a/tests/Console/CommandTest.php b/tests/Console/CommandTest.php index b1e683654fb5..824efa337d09 100644 --- a/tests/Console/CommandTest.php +++ b/tests/Console/CommandTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Console; use Illuminate\Console\Application; +use Illuminate\Console\Attributes\Aliases; use Illuminate\Console\Attributes\Help; use Illuminate\Console\Attributes\Hidden; use Illuminate\Console\Attributes\Signature; @@ -223,6 +224,22 @@ public function testSignatureAttributeCanSetAliases() $this->assertSame(['bar:baz', 'baz:qux'], $command->getAliases()); } + public function testAliasesAttributeCanSetAliases() + { + $command = new AliasesAttributeCommand; + + $this->assertSame('foo:bar', $command->getName()); + $this->assertSame(['bar:baz', 'baz:qux'], $command->getAliases()); + } + + public function testAliasesAttributeOverridesSignatureAliases() + { + $command = new AliasesAttributeOverridesSignatureCommand; + + $this->assertSame('foo:bar', $command->getName()); + $this->assertSame(['override:alias'], $command->getAliases()); + } + public function testHiddenAttributeHidesCommand() { $command = new HiddenCommand; @@ -280,3 +297,21 @@ public function handle() { } } + +#[Signature('foo:bar')] +#[Aliases(['bar:baz', 'baz:qux'])] +class AliasesAttributeCommand extends Command +{ + public function handle() + { + } +} + +#[Signature('foo:bar', aliases: ['ignored:alias'])] +#[Aliases(['override:alias'])] +class AliasesAttributeOverridesSignatureCommand extends Command +{ + public function handle() + { + } +} diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index 2080920c495b..7cfc9cbfe4cb 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -5,6 +5,7 @@ use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Connection; +use Illuminate\Database\Eloquent\Attributes\DateFormat; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Hidden; @@ -12,6 +13,8 @@ use Illuminate\Database\Eloquent\Attributes\Touches; use Illuminate\Database\Eloquent\Attributes\Unguarded; use Illuminate\Database\Eloquent\Attributes\Visible; +use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing; +use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps; use Illuminate\Database\Eloquent\Model; use PHPUnit\Framework\TestCase; @@ -90,6 +93,20 @@ public function test_primary_key_attribute_with_all_options(): void $this->assertFalse($model->getIncrementing()); } + public function test_dedicated_without_incrementing_attribute(): void + { + $model = new ModelWithDedicatedWithoutIncrementingAttribute; + + $this->assertFalse($model->getIncrementing()); + } + + public function test_dedicated_without_incrementing_attribute_overrides_table_incrementing(): void + { + $model = new ModelWithWithoutIncrementingAttributeOverride; + + $this->assertFalse($model->getIncrementing()); + } + public function test_connection_attribute(): void { $model = new ModelWithConnectionAttribute; @@ -139,6 +156,34 @@ public function test_date_format_attribute(): void $this->assertSame('U', $model->getDateFormat()); } + public function test_dedicated_date_format_attribute(): void + { + $model = new ModelWithDedicatedDateFormatAttribute; + + $this->assertSame('Y-m-d', $model->getDateFormat()); + } + + public function test_dedicated_date_format_attribute_overrides_table_date_format(): void + { + $model = new ModelWithDateFormatAttributeOverride; + + $this->assertSame('Y-m-d', $model->getDateFormat()); + } + + public function test_dedicated_without_timestamps_attribute(): void + { + $model = new ModelWithDedicatedWithoutTimestampsAttribute; + + $this->assertFalse($model->usesTimestamps()); + } + + public function test_dedicated_without_timestamps_attribute_overrides_table_timestamps(): void + { + $model = new ModelWithWithoutTimestampsAttributeOverride; + + $this->assertFalse($model->usesTimestamps()); + } + public function test_fillable_attribute(): void { $model = new ModelWithFillableAttribute; @@ -420,3 +465,42 @@ class ModelWithTouchesAttribute extends Model { // } + +#[DateFormat('Y-m-d')] +class ModelWithDedicatedDateFormatAttribute extends Model +{ + // +} + +#[Table(dateFormat: 'U')] +#[DateFormat('Y-m-d')] +class ModelWithDateFormatAttributeOverride extends Model +{ + // +} + +#[WithoutTimestamps] +class ModelWithDedicatedWithoutTimestampsAttribute extends Model +{ + // +} + +#[Table(timestamps: true)] +#[WithoutTimestamps] +class ModelWithWithoutTimestampsAttributeOverride extends Model +{ + // +} + +#[WithoutIncrementing] +class ModelWithDedicatedWithoutIncrementingAttribute extends Model +{ + // +} + +#[Table(incrementing: true)] +#[WithoutIncrementing] +class ModelWithWithoutIncrementingAttributeOverride extends Model +{ + // +} From 275c9de3fd3b53710f66ca1a445b2dcd9615192d Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Fri, 20 Mar 2026 11:33:14 -0400 Subject: [PATCH 011/596] move guzzlehttp/promises to production dependencies (#59301) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db742a9146f1..27b7526184c4 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "egulias/email-validator": "^4.0", "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^2.0.10", @@ -69,7 +70,6 @@ "ably/ably-php": "^1.0", "aws/aws-sdk-php": "^3.322.9", "fakerphp/faker": "^1.24", - "guzzlehttp/promises": "^2.0.3", "guzzlehttp/psr7": "^2.4", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", From bb12117901c919a0f3c2e6b27c785d213926f159 Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 20 Mar 2026 16:09:54 +0000 Subject: [PATCH 012/596] [13.x] `schedule:list` display expression in the correct timezone (#59286) * wip * wip * extract class --------- Co-authored-by: Taylor Otwell --- .../CronExpressionTimezoneConverter.php | 176 ++++++++++++++++++ .../Scheduling/ScheduleListCommand.php | 26 +-- .../Scheduling/ScheduleListCommandTest.php | 117 ++++++++++++ 3 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php diff --git a/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php b/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php new file mode 100644 index 000000000000..04ee0b88279f --- /dev/null +++ b/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php @@ -0,0 +1,176 @@ + + */ + public static function forEvent(Event $event, DateTimeZone $timezone) + { + $eventTimezone = static::resolveEventTimezone($event, $timezone); + + [$totalOffsetMinutes, $hourOffset, $minuteOffset] = static::offsetComponents( + $eventTimezone, $timezone + ); + + if ($totalOffsetMinutes === 0) { + return [$event->expression]; + } + + $segments = preg_split("/\s+/", $event->expression); + $minuteGroups = static::shiftAndGroup($segments[0], $minuteOffset, 60); + + $expressions = []; + + foreach ($minuteGroups as $minuteCarry => $minuteValues) { + $hourGroups = static::shiftAndGroup($segments[1], $hourOffset + $minuteCarry, 24); + + foreach ($hourGroups as $hourCarry => $hourValues) { + $parts = $segments; + $parts[0] = $minuteValues; + $parts[1] = $hourValues; + + foreach (static::expressionsForHourCarry($segments, $parts, $hourCarry) as $expression) { + $expressions[] = $expression; + } + } + } + + return $expressions; + } + + /** + * Resolve the timezone used by the given event. + * + * @param \Illuminate\Console\Scheduling\Event $event + * @param \DateTimeZone $defaultTimezone + * @return \DateTimeZone + */ + protected static function resolveEventTimezone(Event $event, DateTimeZone $defaultTimezone) + { + return $event->timezone instanceof DateTimeZone + ? $event->timezone + : new DateTimeZone($event->timezone ?? $defaultTimezone->getName()); + } + + /** + * Get offset components between the event and display timezones. + * + * @return array{int, int, int} + */ + protected static function offsetComponents(DateTimeZone $eventTimezone, DateTimeZone $displayTimezone) + { + $now = Carbon::now(); + + $totalOffsetMinutes = intdiv( + $displayTimezone->getOffset($now) - $eventTimezone->getOffset($now), + 60 + ); + + return [$totalOffsetMinutes, intdiv($totalOffsetMinutes, 60), $totalOffsetMinutes % 60]; + } + + /** + * Build expressions for the given hour carry direction. + * + * @param array $segments + * @param array $parts + * @return array + */ + protected static function expressionsForHourCarry(array $segments, array $parts, int $hourCarry) + { + if ($hourCarry === 0) { + return [implode(' ', $parts)]; + } + + $parts[4] = static::shiftField($segments[4], $hourCarry, 7); + + $dayGroups = static::shiftAndGroup($segments[2], $hourCarry, 31, min: 1); + + $expressions = []; + + foreach ($dayGroups as $dayCarry => $dayValues) { + $dayParts = $parts; + $dayParts[2] = $dayValues; + + if ($dayCarry !== 0) { + $dayParts[3] = static::shiftField($segments[3], $dayCarry, 12, min: 1); + } + + $expressions[] = implode(' ', $dayParts); + } + + return $expressions; + } + + /** + * Shift values in a cron field and group them by carry direction. + * + * @param string $field + * @param int $offset + * @param int $mod + * @return array + */ + protected static function shiftAndGroup($field, $offset, $mod, $min = 0) + { + if ($offset === 0 || ! preg_match('/^[\d,]+$/', $field)) { + return [0 => $field]; + } + + $groups = []; + + foreach (explode(',', $field) as $value) { + $new = (int) $value + $offset; + $carry = 0; + + if ($new >= $mod + $min) { + $carry = 1; + $new -= $mod; + } elseif ($new < $min) { + $carry = -1; + $new += $mod; + } + + $groups[$carry][] = $new; + } + + return collect($groups)->map(function ($values) { + sort($values); + + return implode(',', $values); + })->all(); + } + + /** + * Shift a cron field by the given offset. + * + * @param string $field + * @param int $offset + * @param int $mod + * @param int $min + * @return string + */ + protected static function shiftField($field, $offset, $mod, $min = 0) + { + if ($offset === 0 || ! preg_match('/^[\d,]+$/', $field)) { + return $field; + } + + $shifted = collect(explode(',', $field)) + ->map(fn ($v) => (((int) $v + $offset - $min) % $mod + $mod) % $mod + $min) + ->sort(); + + return $shifted->implode(','); + } +} diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 1138ab7ffa52..c1fa4783053b 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -79,7 +79,7 @@ public function handle(Schedule $schedule) */ protected function displayJson(Collection $events, DateTimeZone $timezone) { - $this->output->writeln($events->map(function ($event) use ($timezone) { + $this->output->writeln($events->flatMap(function ($event) use ($timezone) { $nextDueDate = $this->getNextDueDateForEvent($event, $timezone); $command = $event->command ?? ''; @@ -96,8 +96,8 @@ protected function displayJson(Collection $events, DateTimeZone $timezone) } } - return [ - 'expression' => $event->expression, + return collect(CronExpressionTimezoneConverter::forEvent($event, $timezone))->map(fn ($expression) => [ + 'expression' => $expression, 'command' => $command, 'description' => $event->description ?? null, 'next_due_date' => $nextDueDate->format('Y-m-d H:i:s P'), @@ -106,7 +106,7 @@ protected function displayJson(Collection $events, DateTimeZone $timezone) 'has_mutex' => $event->mutex->exists($event), 'repeat_seconds' => $event->isRepeatable() ? $event->repeatSeconds : null, 'environments' => $event->environments, - ]; + ]); })->values()->toJson()); } @@ -121,12 +121,14 @@ protected function displayForCli(Collection $events, DateTimeZone $timezone) { $terminalWidth = self::getTerminalWidth(); - $expressionSpacing = $this->getCronExpressionSpacing($events); + $expressionSpacing = $this->getCronExpressionSpacing($events, $timezone); $repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events); - $events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) { - return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone); + $events = $events->flatMap(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) { + return collect(CronExpressionTimezoneConverter::forEvent($event, $timezone))->map( + fn ($expression) => $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone, $expression) + ); }); $this->line( @@ -140,9 +142,10 @@ protected function displayForCli(Collection $events, DateTimeZone $timezone) * @param \Illuminate\Support\Collection $events * @return array */ - private function getCronExpressionSpacing($events) + private function getCronExpressionSpacing($events, DateTimeZone $timezone) { - $rows = $events->map(fn ($event) => array_map(mb_strlen(...), preg_split("/\s+/", $event->expression))); + $rows = $events->flatMap(fn ($event) => collect(CronExpressionTimezoneConverter::forEvent($event, $timezone)) + ->map(fn ($expression) => array_map(mb_strlen(...), preg_split("/\s+/", $expression)))); return (new Collection($rows[0] ?? []))->keys()->map(fn ($key) => $rows->max($key))->all(); } @@ -166,11 +169,12 @@ private function getRepeatExpressionSpacing($events) * @param array $expressionSpacing * @param int $repeatExpressionSpacing * @param \DateTimeZone $timezone + * @param string|null $convertedExpression * @return array */ - private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) + private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone, $convertedExpression = null) { - $expression = $this->formatCronExpression($event->expression, $expressionSpacing); + $expression = $this->formatCronExpression($convertedExpression ?? $event->expression, $expressionSpacing); $repeatExpression = str_pad($this->getRepeatExpression($event), $repeatExpressionSpacing); diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index b42c42116418..dbb72958c01d 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\ProcessUtils; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ScheduleListCommandTest extends TestCase { @@ -188,6 +189,122 @@ public function testDisplayScheduleAsJsonWithTimezone() $this->assertSame('php artisan inspire', $data[0]['command']); } + public static function expressionTimezoneConversionProvider() + { + return [ + // [cron expression, event timezone, display timezone, expected expressions] + + // No conversion needed — same timezone + 'same timezone' => ['0 8 * * *', 'UTC', null, ['0 8 * * *']], + + // Wildcards/steps — pass through unchanged + 'every minute' => ['* * * * *', 'America/New_York', null, ['* * * * *']], + 'every five minutes' => ['*/5 * * * *', 'Asia/Tokyo', null, ['*/5 * * * *']], + 'every two hours' => ['0 */2 * * *', 'Asia/Tokyo', null, ['0 */2 * * *']], + 'every odd hour' => ['0 1-23/2 * * *', 'Asia/Tokyo', null, ['0 1-23/2 * * *']], + 'quarterly step month' => ['0 0 1 1-12/3 *', 'Asia/Tokyo', null, ['0 15 31 1-12/3 *']], + 'weekdays range' => ['0 8 * * 1-5', 'Asia/Tokyo', null, ['0 23 * * 1-5']], + + // Simple hour shift (no day boundary) + 'daily LA to UTC' => ['0 8 * * *', 'America/Los_Angeles', null, ['0 16 * * *']], + 'daily Tokyo to UTC' => ['0 14 * * *', 'Asia/Tokyo', null, ['0 5 * * *']], + '--timezone flag' => ['0 0 * * *', 'UTC', 'Asia/Tokyo', ['0 9 * * *']], + + // Hour wraparound (crosses midnight, no fixed day) + 'hour wraps forward' => ['0 23 * * *', 'Asia/Tokyo', null, ['0 14 * * *']], + 'hour wraps backward' => ['0 2 * * *', 'America/Los_Angeles', null, ['0 10 * * *']], + + // Half-hour timezone offset + 'Kolkata +5:30' => ['0 8 * * *', 'Asia/Kolkata', null, ['30 2 * * *']], + 'Kathmandu +5:45' => ['0 8 * * *', 'Asia/Kathmandu', null, ['15 2 * * *']], + + // Comma-separated hours — same carry direction + 'twice daily Tokyo' => ['0 9,17 * * *', 'Asia/Tokyo', null, ['0 0,8 * * *']], + 'twice daily Tokyo wrapping' => ['0 13,22 * * *', 'Asia/Tokyo', null, ['0 4,13 * * *']], + + // Comma-separated hours — mixed carries (splits into two entries) + 'twice daily LA mixed carry' => ['0 13,17 * * *', 'America/Los_Angeles', null, ['0 21 * * *', '0 1 * * *']], + 'twice daily Tokyo mixed carry' => ['0 3,20 * * *', 'Asia/Tokyo', null, ['0 18 * * *', '0 11 * * *']], + + // Day-of-week shifts + 'weekly Monday night LA' => ['0 22 * * 1', 'America/Los_Angeles', null, ['0 6 * * 2']], + 'weekly Wednesday morning Tokyo' => ['0 3 * * 3', 'Asia/Tokyo', null, ['0 18 * * 2']], + 'weekly Sunday night LA' => ['0 22 * * 0', 'America/Los_Angeles', null, ['0 6 * * 1']], + 'weekly Saturday morning Tokyo' => ['0 3 * * 6', 'Asia/Tokyo', null, ['0 18 * * 5']], + + // Day-of-month shifts + 'monthly 15th Tokyo (no day wrap)' => ['0 12 15 * *', 'Asia/Tokyo', null, ['0 3 15 * *']], + 'monthly 15th Tokyo (day wraps back)' => ['0 8 15 * *', 'Asia/Tokyo', null, ['0 23 14 * *']], + 'monthly 1st Tokyo (day wraps to 31)' => ['0 1 1 * *', 'Asia/Tokyo', null, ['0 16 31 * *']], + 'monthly 1st LA (day wraps forward)' => ['0 22 1 * *', 'America/Los_Angeles', null, ['0 6 2 * *']], + + // Month shifts (day-of-month carry propagates to month) + 'yearly Jan 1 Tokyo' => ['0 1 1 1 *', 'Asia/Tokyo', null, ['0 16 31 12 *']], + 'yearly Jul 1 Tokyo' => ['0 1 1 7 *', 'Asia/Tokyo', null, ['0 16 31 6 *']], + 'yearly Dec 31 LA' => ['0 22 31 12 *', 'America/Los_Angeles', null, ['0 6 1 1 *']], + + // Comma day-of-month + 'twice monthly Tokyo (no wrap)' => ['0 12 1,16 * *', 'Asia/Tokyo', null, ['0 3 1,16 * *']], + // day 1→31 (carry -1 to month) and 16→15 (no carry) — splits + 'twice monthly Tokyo (wraps)' => ['0 1 1,16 * *', 'Asia/Tokyo', null, ['0 16 31 * *', '0 16 15 * *']], + + // Comma day-of-week + 'weekends LA night' => ['0 22 * * 0,6', 'America/Los_Angeles', null, ['0 6 * * 0,1']], + + // Hourly with minute offset (half-hour timezone) + 'hourly at 30 Kolkata' => ['30 * * * *', 'Asia/Kolkata', null, ['0 * * * *']], + 'hourly at 0 Kolkata' => ['0 * * * *', 'Asia/Kolkata', null, ['30 * * * *']], + + // Comma minutes with half-hour timezone — mixed minute carries (splits) + // 15+(-30)=-15→45 (carry -1) and 45+(-30)=15 (no carry) + 'comma minutes Kolkata mixed carry' => ['15,45 8 * * *', 'Asia/Kolkata', null, ['45 2 * * *', '15 3 * * *']], + ]; + } + + #[DataProvider('expressionTimezoneConversionProvider')] + public function testExpressionTimezoneConversion($expression, $eventTimezone, $displayTimezone, $expectedExpressions) + { + $this->schedule->command('inspire')->cron($expression)->timezone($eventTimezone); + + $options = ['--json' => true]; + + if ($displayTimezone) { + $options['--timezone'] = $displayTimezone; + } + + $this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, $options); + $output = Artisan::output(); + + $data = json_decode($output, true); + + $this->assertCount(count($expectedExpressions), $data); + + foreach ($expectedExpressions as $index => $expected) { + $this->assertSame($expected, $data[$index]['expression']); + } + } + + public function testDisplayScheduleCliSplitsExpressionWhenMixedCarry() + { + // 13+8=21 (no carry), 17+8=1 (carry +1) — splits into two CLI rows + $this->schedule->command('inspire')->twiceDaily(13, 17)->timezone('America/Los_Angeles'); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutputToContain('0 21 * * *') + ->expectsOutputToContain('0 1 * * *'); + } + + public function testDisplayScheduleCliConvertsExpression() + { + // 8:00 AM LA (UTC-8 in January) = 16:00 UTC + $this->schedule->command('inspire')->dailyAt('08:00')->timezone('America/Los_Angeles'); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutputToContain('0 16 * * *'); + } + public function testDisplayScheduleAsJsonInVerboseMode() { $this->schedule->command(FooCommand::class)->quarterly(); From ba172006b02ceb80e49a03df031a2e4ab0a7c438 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Fri, 20 Mar 2026 20:17:41 +0000 Subject: [PATCH 013/596] Handle exceptions in eventStream to prevent fatal error (#59292) * Handle exceptions in eventStream to prevent fatal error When an exception is thrown mid-stream in an eventStream response, headers have already been sent. The global exception handler then tries to send a new error response, which triggers a fatal "Cannot modify header information" error. Catch exceptions during streaming, report them through the normal exception handler, and emit an SSE error event so the client can detect and handle the failure gracefully. Fixes #59291 * Do not leak exception messages to eventStream clients Exceptions thrown mid-stream are now silently caught and reported server-side only. The error message is no longer emitted as an SSE event, preventing internal details from being exposed to clients. --- src/Illuminate/Routing/ResponseFactory.php | 68 ++++++++------- .../Http/EventStreamResponseTest.php | 85 +++++++++++++++++++ 2 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 tests/Integration/Http/EventStreamResponseTest.php diff --git a/src/Illuminate/Routing/ResponseFactory.php b/src/Illuminate/Routing/ResponseFactory.php index 7c91102b347c..71b1eb7e4ec9 100644 --- a/src/Illuminate/Routing/ResponseFactory.php +++ b/src/Illuminate/Routing/ResponseFactory.php @@ -131,50 +131,54 @@ public function jsonp($callback, $data = [], $status = 200, array $headers = [], public function eventStream(Closure $callback, array $headers = [], StreamedEvent|string|null $endStreamWith = '') { return $this->stream(function () use ($callback, $endStreamWith) { - foreach ($callback() as $message) { - if (connection_aborted()) { - break; - } + try { + foreach ($callback() as $message) { + if (connection_aborted()) { + break; + } - $event = 'update'; + $event = 'update'; - if ($message instanceof StreamedEvent) { - $event = $message->event; - $message = $message->data; - } + if ($message instanceof StreamedEvent) { + $event = $message->event; + $message = $message->data; + } - if (! is_string($message) && ! is_numeric($message)) { - $message = Js::encode($message); - } + if (! is_string($message) && ! is_numeric($message)) { + $message = Js::encode($message); + } + + echo "event: $event\n"; + echo 'data: '.$message; + echo "\n\n"; - echo "event: $event\n"; - echo 'data: '.$message; - echo "\n\n"; + if (ob_get_level() > 0) { + ob_flush(); + } - if (ob_get_level() > 0) { - ob_flush(); + flush(); } - flush(); - } + if (filled($endStreamWith)) { + $endEvent = 'update'; - if (filled($endStreamWith)) { - $endEvent = 'update'; + if ($endStreamWith instanceof StreamedEvent) { + $endEvent = $endStreamWith->event; + $endStreamWith = $endStreamWith->data; + } - if ($endStreamWith instanceof StreamedEvent) { - $endEvent = $endStreamWith->event; - $endStreamWith = $endStreamWith->data; - } + echo "event: $endEvent\n"; + echo 'data: '.$endStreamWith; + echo "\n\n"; - echo "event: $endEvent\n"; - echo 'data: '.$endStreamWith; - echo "\n\n"; + if (ob_get_level() > 0) { + ob_flush(); + } - if (ob_get_level() > 0) { - ob_flush(); + flush(); } - - flush(); + } catch (Throwable $e) { + report($e); } }, 200, array_merge($headers, [ 'Content-Type' => 'text/event-stream', diff --git a/tests/Integration/Http/EventStreamResponseTest.php b/tests/Integration/Http/EventStreamResponseTest.php new file mode 100644 index 000000000000..8fbe8e835c80 --- /dev/null +++ b/tests/Integration/Http/EventStreamResponseTest.php @@ -0,0 +1,85 @@ +eventStream(function () { + yield new StreamedEvent( + event: 'update', + data: ['message' => 'hello'], + ); + + yield new StreamedEvent( + event: 'update', + data: ['message' => 'world'], + ); + }); + }); + + $response = $this->get('/stream'); + + $response->assertOk(); + $response->assertHeader('Content-Type', 'text/event-stream; charset=utf-8'); + $response->assertHeader('X-Accel-Buffering', 'no'); + + $content = $response->streamedContent(); + + $this->assertStringContainsString("event: update\n", $content); + $this->assertStringContainsString('data: {"message":"hello"}', $content); + $this->assertStringContainsString('data: {"message":"world"}', $content); + $this->assertStringContainsString('data: ', $content); + } + + public function testEventStreamExceptionDoesNotLeakToClient() + { + Route::get('/stream', function () { + return response()->eventStream(function () { + yield new StreamedEvent( + event: 'update', + data: ['message' => 'hello'], + ); + + throw new Exception('Something went wrong during streaming'); + }); + }); + + Log::shouldReceive('error') + ->once() + ->with('Something went wrong during streaming', \Mockery::type('array')); + + $response = $this->get('/stream'); + $content = $response->streamedContent(); + + $this->assertStringContainsString("event: update\n", $content); + $this->assertStringContainsString('data: {"message":"hello"}', $content); + $this->assertStringNotContainsString('Something went wrong during streaming', $content); + $this->assertStringNotContainsString("event: error\n", $content); + $this->assertStringNotContainsString('data: ', $content); + } + + public function testEventStreamExceptionIsReportedToExceptionHandler() + { + Route::get('/stream', function () { + return response()->eventStream(function () { + throw new Exception('Test exception reporting'); + }); + }); + + Log::shouldReceive('error') + ->once() + ->with('Test exception reporting', \Mockery::type('array')); + + $response = $this->get('/stream'); + $response->streamedContent(); + } +} From 8d7d4d32e283678d541d194c3c26ade63df50d84 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sat, 21 Mar 2026 01:00:51 +0000 Subject: [PATCH 014/596] [13.x] Ensure connectUsing works with UnitEnum / FileManager drive docblock (#59306) * sort some docblocks * cs --- src/Illuminate/Database/DatabaseManager.php | 7 +++++-- src/Illuminate/Filesystem/FilesystemManager.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index 2d8edbc5330f..0afeb7d41fd8 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -12,6 +12,7 @@ use InvalidArgumentException; use PDO; use RuntimeException; +use UnitEnum; use function Illuminate\Support\enum_value; @@ -146,10 +147,12 @@ public static function calculateDynamicConnectionName(array $config) * * @throws \RuntimeException */ - public function connectUsing(string $name, array $config, bool $force = false) + public function connectUsing(UnitEnum|string $name, array $config, bool $force = false) { + $name = enum_value($name); + if ($force) { - $this->purge($name = enum_value($name)); + $this->purge($name); } if (isset($this->connections[$name])) { diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index a4e567046eab..dbe18a435e7d 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -63,7 +63,7 @@ public function __construct($app) /** * Get a filesystem instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Filesystem\Filesystem */ public function drive($name = null) From 9f3c52ca5a0ec2e0aed67eef35b724223fa9848f Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:01:22 +0000 Subject: [PATCH 015/596] Update facade docblocks --- src/Illuminate/Support/Facades/Storage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Storage.php b/src/Illuminate/Support/Facades/Storage.php index 2d214390b438..9f37e575eca4 100644 --- a/src/Illuminate/Support/Facades/Storage.php +++ b/src/Illuminate/Support/Facades/Storage.php @@ -7,7 +7,7 @@ use function Illuminate\Support\enum_value; /** - * @method static \Illuminate\Contracts\Filesystem\Filesystem drive(string|null $name = null) + * @method static \Illuminate\Contracts\Filesystem\Filesystem drive(\UnitEnum|string|null $name = null) * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(\UnitEnum|string|null $name = null) * @method static \Illuminate\Contracts\Filesystem\Cloud cloud() * @method static \Illuminate\Contracts\Filesystem\Filesystem build(string|array $config) From fa959160fdda2f3bd17e7b03ae103dffbd7199da Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Sat, 21 Mar 2026 11:33:57 +0000 Subject: [PATCH 016/596] [12.x] `schedule:list` display expression in the correct timezone (#59307) * wip * wip * extract class --------- Co-authored-by: Taylor Otwell --- .../CronExpressionTimezoneConverter.php | 176 ++++++++++++++++++ .../Scheduling/ScheduleListCommand.php | 26 +-- .../Scheduling/ScheduleListCommandTest.php | 117 ++++++++++++ 3 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php diff --git a/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php b/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php new file mode 100644 index 000000000000..04ee0b88279f --- /dev/null +++ b/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php @@ -0,0 +1,176 @@ + + */ + public static function forEvent(Event $event, DateTimeZone $timezone) + { + $eventTimezone = static::resolveEventTimezone($event, $timezone); + + [$totalOffsetMinutes, $hourOffset, $minuteOffset] = static::offsetComponents( + $eventTimezone, $timezone + ); + + if ($totalOffsetMinutes === 0) { + return [$event->expression]; + } + + $segments = preg_split("/\s+/", $event->expression); + $minuteGroups = static::shiftAndGroup($segments[0], $minuteOffset, 60); + + $expressions = []; + + foreach ($minuteGroups as $minuteCarry => $minuteValues) { + $hourGroups = static::shiftAndGroup($segments[1], $hourOffset + $minuteCarry, 24); + + foreach ($hourGroups as $hourCarry => $hourValues) { + $parts = $segments; + $parts[0] = $minuteValues; + $parts[1] = $hourValues; + + foreach (static::expressionsForHourCarry($segments, $parts, $hourCarry) as $expression) { + $expressions[] = $expression; + } + } + } + + return $expressions; + } + + /** + * Resolve the timezone used by the given event. + * + * @param \Illuminate\Console\Scheduling\Event $event + * @param \DateTimeZone $defaultTimezone + * @return \DateTimeZone + */ + protected static function resolveEventTimezone(Event $event, DateTimeZone $defaultTimezone) + { + return $event->timezone instanceof DateTimeZone + ? $event->timezone + : new DateTimeZone($event->timezone ?? $defaultTimezone->getName()); + } + + /** + * Get offset components between the event and display timezones. + * + * @return array{int, int, int} + */ + protected static function offsetComponents(DateTimeZone $eventTimezone, DateTimeZone $displayTimezone) + { + $now = Carbon::now(); + + $totalOffsetMinutes = intdiv( + $displayTimezone->getOffset($now) - $eventTimezone->getOffset($now), + 60 + ); + + return [$totalOffsetMinutes, intdiv($totalOffsetMinutes, 60), $totalOffsetMinutes % 60]; + } + + /** + * Build expressions for the given hour carry direction. + * + * @param array $segments + * @param array $parts + * @return array + */ + protected static function expressionsForHourCarry(array $segments, array $parts, int $hourCarry) + { + if ($hourCarry === 0) { + return [implode(' ', $parts)]; + } + + $parts[4] = static::shiftField($segments[4], $hourCarry, 7); + + $dayGroups = static::shiftAndGroup($segments[2], $hourCarry, 31, min: 1); + + $expressions = []; + + foreach ($dayGroups as $dayCarry => $dayValues) { + $dayParts = $parts; + $dayParts[2] = $dayValues; + + if ($dayCarry !== 0) { + $dayParts[3] = static::shiftField($segments[3], $dayCarry, 12, min: 1); + } + + $expressions[] = implode(' ', $dayParts); + } + + return $expressions; + } + + /** + * Shift values in a cron field and group them by carry direction. + * + * @param string $field + * @param int $offset + * @param int $mod + * @return array + */ + protected static function shiftAndGroup($field, $offset, $mod, $min = 0) + { + if ($offset === 0 || ! preg_match('/^[\d,]+$/', $field)) { + return [0 => $field]; + } + + $groups = []; + + foreach (explode(',', $field) as $value) { + $new = (int) $value + $offset; + $carry = 0; + + if ($new >= $mod + $min) { + $carry = 1; + $new -= $mod; + } elseif ($new < $min) { + $carry = -1; + $new += $mod; + } + + $groups[$carry][] = $new; + } + + return collect($groups)->map(function ($values) { + sort($values); + + return implode(',', $values); + })->all(); + } + + /** + * Shift a cron field by the given offset. + * + * @param string $field + * @param int $offset + * @param int $mod + * @param int $min + * @return string + */ + protected static function shiftField($field, $offset, $mod, $min = 0) + { + if ($offset === 0 || ! preg_match('/^[\d,]+$/', $field)) { + return $field; + } + + $shifted = collect(explode(',', $field)) + ->map(fn ($v) => (((int) $v + $offset - $min) % $mod + $mod) % $mod + $min) + ->sort(); + + return $shifted->implode(','); + } +} diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 1138ab7ffa52..c1fa4783053b 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -79,7 +79,7 @@ public function handle(Schedule $schedule) */ protected function displayJson(Collection $events, DateTimeZone $timezone) { - $this->output->writeln($events->map(function ($event) use ($timezone) { + $this->output->writeln($events->flatMap(function ($event) use ($timezone) { $nextDueDate = $this->getNextDueDateForEvent($event, $timezone); $command = $event->command ?? ''; @@ -96,8 +96,8 @@ protected function displayJson(Collection $events, DateTimeZone $timezone) } } - return [ - 'expression' => $event->expression, + return collect(CronExpressionTimezoneConverter::forEvent($event, $timezone))->map(fn ($expression) => [ + 'expression' => $expression, 'command' => $command, 'description' => $event->description ?? null, 'next_due_date' => $nextDueDate->format('Y-m-d H:i:s P'), @@ -106,7 +106,7 @@ protected function displayJson(Collection $events, DateTimeZone $timezone) 'has_mutex' => $event->mutex->exists($event), 'repeat_seconds' => $event->isRepeatable() ? $event->repeatSeconds : null, 'environments' => $event->environments, - ]; + ]); })->values()->toJson()); } @@ -121,12 +121,14 @@ protected function displayForCli(Collection $events, DateTimeZone $timezone) { $terminalWidth = self::getTerminalWidth(); - $expressionSpacing = $this->getCronExpressionSpacing($events); + $expressionSpacing = $this->getCronExpressionSpacing($events, $timezone); $repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events); - $events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) { - return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone); + $events = $events->flatMap(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) { + return collect(CronExpressionTimezoneConverter::forEvent($event, $timezone))->map( + fn ($expression) => $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone, $expression) + ); }); $this->line( @@ -140,9 +142,10 @@ protected function displayForCli(Collection $events, DateTimeZone $timezone) * @param \Illuminate\Support\Collection $events * @return array */ - private function getCronExpressionSpacing($events) + private function getCronExpressionSpacing($events, DateTimeZone $timezone) { - $rows = $events->map(fn ($event) => array_map(mb_strlen(...), preg_split("/\s+/", $event->expression))); + $rows = $events->flatMap(fn ($event) => collect(CronExpressionTimezoneConverter::forEvent($event, $timezone)) + ->map(fn ($expression) => array_map(mb_strlen(...), preg_split("/\s+/", $expression)))); return (new Collection($rows[0] ?? []))->keys()->map(fn ($key) => $rows->max($key))->all(); } @@ -166,11 +169,12 @@ private function getRepeatExpressionSpacing($events) * @param array $expressionSpacing * @param int $repeatExpressionSpacing * @param \DateTimeZone $timezone + * @param string|null $convertedExpression * @return array */ - private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) + private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone, $convertedExpression = null) { - $expression = $this->formatCronExpression($event->expression, $expressionSpacing); + $expression = $this->formatCronExpression($convertedExpression ?? $event->expression, $expressionSpacing); $repeatExpression = str_pad($this->getRepeatExpression($event), $repeatExpressionSpacing); diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 582de917b6e4..a37cd3f68ff8 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\ProcessUtils; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ScheduleListCommandTest extends TestCase { @@ -188,6 +189,122 @@ public function testDisplayScheduleAsJsonWithTimezone() $this->assertSame('php artisan inspire', $data[0]['command']); } + public static function expressionTimezoneConversionProvider() + { + return [ + // [cron expression, event timezone, display timezone, expected expressions] + + // No conversion needed — same timezone + 'same timezone' => ['0 8 * * *', 'UTC', null, ['0 8 * * *']], + + // Wildcards/steps — pass through unchanged + 'every minute' => ['* * * * *', 'America/New_York', null, ['* * * * *']], + 'every five minutes' => ['*/5 * * * *', 'Asia/Tokyo', null, ['*/5 * * * *']], + 'every two hours' => ['0 */2 * * *', 'Asia/Tokyo', null, ['0 */2 * * *']], + 'every odd hour' => ['0 1-23/2 * * *', 'Asia/Tokyo', null, ['0 1-23/2 * * *']], + 'quarterly step month' => ['0 0 1 1-12/3 *', 'Asia/Tokyo', null, ['0 15 31 1-12/3 *']], + 'weekdays range' => ['0 8 * * 1-5', 'Asia/Tokyo', null, ['0 23 * * 1-5']], + + // Simple hour shift (no day boundary) + 'daily LA to UTC' => ['0 8 * * *', 'America/Los_Angeles', null, ['0 16 * * *']], + 'daily Tokyo to UTC' => ['0 14 * * *', 'Asia/Tokyo', null, ['0 5 * * *']], + '--timezone flag' => ['0 0 * * *', 'UTC', 'Asia/Tokyo', ['0 9 * * *']], + + // Hour wraparound (crosses midnight, no fixed day) + 'hour wraps forward' => ['0 23 * * *', 'Asia/Tokyo', null, ['0 14 * * *']], + 'hour wraps backward' => ['0 2 * * *', 'America/Los_Angeles', null, ['0 10 * * *']], + + // Half-hour timezone offset + 'Kolkata +5:30' => ['0 8 * * *', 'Asia/Kolkata', null, ['30 2 * * *']], + 'Kathmandu +5:45' => ['0 8 * * *', 'Asia/Kathmandu', null, ['15 2 * * *']], + + // Comma-separated hours — same carry direction + 'twice daily Tokyo' => ['0 9,17 * * *', 'Asia/Tokyo', null, ['0 0,8 * * *']], + 'twice daily Tokyo wrapping' => ['0 13,22 * * *', 'Asia/Tokyo', null, ['0 4,13 * * *']], + + // Comma-separated hours — mixed carries (splits into two entries) + 'twice daily LA mixed carry' => ['0 13,17 * * *', 'America/Los_Angeles', null, ['0 21 * * *', '0 1 * * *']], + 'twice daily Tokyo mixed carry' => ['0 3,20 * * *', 'Asia/Tokyo', null, ['0 18 * * *', '0 11 * * *']], + + // Day-of-week shifts + 'weekly Monday night LA' => ['0 22 * * 1', 'America/Los_Angeles', null, ['0 6 * * 2']], + 'weekly Wednesday morning Tokyo' => ['0 3 * * 3', 'Asia/Tokyo', null, ['0 18 * * 2']], + 'weekly Sunday night LA' => ['0 22 * * 0', 'America/Los_Angeles', null, ['0 6 * * 1']], + 'weekly Saturday morning Tokyo' => ['0 3 * * 6', 'Asia/Tokyo', null, ['0 18 * * 5']], + + // Day-of-month shifts + 'monthly 15th Tokyo (no day wrap)' => ['0 12 15 * *', 'Asia/Tokyo', null, ['0 3 15 * *']], + 'monthly 15th Tokyo (day wraps back)' => ['0 8 15 * *', 'Asia/Tokyo', null, ['0 23 14 * *']], + 'monthly 1st Tokyo (day wraps to 31)' => ['0 1 1 * *', 'Asia/Tokyo', null, ['0 16 31 * *']], + 'monthly 1st LA (day wraps forward)' => ['0 22 1 * *', 'America/Los_Angeles', null, ['0 6 2 * *']], + + // Month shifts (day-of-month carry propagates to month) + 'yearly Jan 1 Tokyo' => ['0 1 1 1 *', 'Asia/Tokyo', null, ['0 16 31 12 *']], + 'yearly Jul 1 Tokyo' => ['0 1 1 7 *', 'Asia/Tokyo', null, ['0 16 31 6 *']], + 'yearly Dec 31 LA' => ['0 22 31 12 *', 'America/Los_Angeles', null, ['0 6 1 1 *']], + + // Comma day-of-month + 'twice monthly Tokyo (no wrap)' => ['0 12 1,16 * *', 'Asia/Tokyo', null, ['0 3 1,16 * *']], + // day 1→31 (carry -1 to month) and 16→15 (no carry) — splits + 'twice monthly Tokyo (wraps)' => ['0 1 1,16 * *', 'Asia/Tokyo', null, ['0 16 31 * *', '0 16 15 * *']], + + // Comma day-of-week + 'weekends LA night' => ['0 22 * * 0,6', 'America/Los_Angeles', null, ['0 6 * * 0,1']], + + // Hourly with minute offset (half-hour timezone) + 'hourly at 30 Kolkata' => ['30 * * * *', 'Asia/Kolkata', null, ['0 * * * *']], + 'hourly at 0 Kolkata' => ['0 * * * *', 'Asia/Kolkata', null, ['30 * * * *']], + + // Comma minutes with half-hour timezone — mixed minute carries (splits) + // 15+(-30)=-15→45 (carry -1) and 45+(-30)=15 (no carry) + 'comma minutes Kolkata mixed carry' => ['15,45 8 * * *', 'Asia/Kolkata', null, ['45 2 * * *', '15 3 * * *']], + ]; + } + + #[DataProvider('expressionTimezoneConversionProvider')] + public function testExpressionTimezoneConversion($expression, $eventTimezone, $displayTimezone, $expectedExpressions) + { + $this->schedule->command('inspire')->cron($expression)->timezone($eventTimezone); + + $options = ['--json' => true]; + + if ($displayTimezone) { + $options['--timezone'] = $displayTimezone; + } + + $this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, $options); + $output = Artisan::output(); + + $data = json_decode($output, true); + + $this->assertCount(count($expectedExpressions), $data); + + foreach ($expectedExpressions as $index => $expected) { + $this->assertSame($expected, $data[$index]['expression']); + } + } + + public function testDisplayScheduleCliSplitsExpressionWhenMixedCarry() + { + // 13+8=21 (no carry), 17+8=1 (carry +1) — splits into two CLI rows + $this->schedule->command('inspire')->twiceDaily(13, 17)->timezone('America/Los_Angeles'); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutputToContain('0 21 * * *') + ->expectsOutputToContain('0 1 * * *'); + } + + public function testDisplayScheduleCliConvertsExpression() + { + // 8:00 AM LA (UTC-8 in January) = 16:00 UTC + $this->schedule->command('inspire')->dailyAt('08:00')->timezone('America/Los_Angeles'); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutputToContain('0 16 * * *'); + } + public function testDisplayScheduleAsJsonInVerboseMode() { $this->schedule->command(FooCommand::class)->quarterly(); From 2d8e8f401584ea007888487316fbd633b99e51bd Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sat, 21 Mar 2026 07:38:45 -0400 Subject: [PATCH 017/596] [13.x] Include columns and index in UniqueConstraintViolationException (#59299) * parse the exception details * passing tests * for postgres * sqlserver * comments and fix mysql test * style * style and comments * taylor-y * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Connection.php | 23 ++- src/Illuminate/Database/MySqlConnection.php | 17 +++ .../Database/PostgresConnection.php | 21 +++ src/Illuminate/Database/SQLiteConnection.php | 22 +++ .../Database/SqlServerConnection.php | 19 +++ .../UniqueConstraintViolationException.php | 39 +++++ .../UniqueConstraintViolationTest.php | 133 ++++++++++++++++++ 7 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 tests/Integration/Database/UniqueConstraintViolationTest.php diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index aade6d7d4fda..0b729062f475 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -834,11 +834,11 @@ protected function runQueryCallback($query, $bindings, Closure $callback) // message to include the bindings with SQL, which will make this exception a // lot more helpful to the developer instead of just the database's errors. catch (Exception $e) { - $exceptionType = $this->isUniqueConstraintError($e) + $exceptionType = ($isUniqueConstraintError = $this->isUniqueConstraintError($e)) ? UniqueConstraintViolationException::class : QueryException::class; - throw new $exceptionType( + $exception = new $exceptionType( $this->getNameWithReadWriteType(), $query, $this->prepareBindings($bindings), @@ -846,6 +846,14 @@ protected function runQueryCallback($query, $bindings, Closure $callback) $this->getConnectionDetails(), $this->latestReadWriteTypeUsed(), ); + + if ($isUniqueConstraintError) { + ['index' => $index, 'columns' => $columns] = $this->parseUniqueConstraintViolation($e); + + $exception->setIndex($index)->setColumns($columns); + } + + throw $exception; } } @@ -860,6 +868,17 @@ protected function isUniqueConstraintError(Exception $exception) return false; } + /** + * Extract the index and columns that caused a unique constraint violation. + * + * @param Exception $exception + * @return array{index: string|null, columns: list} + */ + protected function parseUniqueConstraintViolation(Exception $exception): array + { + return ['index' => null, 'columns' => []]; + } + /** * Log a query in the connection's query log. * diff --git a/src/Illuminate/Database/MySqlConnection.php b/src/Illuminate/Database/MySqlConnection.php index ab541c9aa5b6..bb9c984784c3 100755 --- a/src/Illuminate/Database/MySqlConnection.php +++ b/src/Illuminate/Database/MySqlConnection.php @@ -82,6 +82,23 @@ protected function isUniqueConstraintError(Exception $exception) return (bool) preg_match('#Integrity constraint violation: 1062#i', $exception->getMessage()); } + /** + * Extract the index that caused a unique constraint violation. + * + * @param Exception $exception + * @return array{index: string|null, columns: list} + */ + protected function parseUniqueConstraintViolation(Exception $exception): array + { + preg_match( + '#Duplicate entry \'.*?\' for key \'(?:.*?\.)?(.+?)\'#i', + $exception->getMessage(), + $matches + ); + + return ['columns' => [], 'index' => $matches[1] ?? null]; + } + /** * Get the connection's last insert ID. * diff --git a/src/Illuminate/Database/PostgresConnection.php b/src/Illuminate/Database/PostgresConnection.php index f80b5dce5df1..1c2b372c622a 100755 --- a/src/Illuminate/Database/PostgresConnection.php +++ b/src/Illuminate/Database/PostgresConnection.php @@ -55,6 +55,27 @@ protected function isUniqueConstraintError(Exception $exception) return '23505' === $exception->getCode(); } + /** + * Extract the index and columns that caused a unique constraint violation. + * + * @param Exception $exception + * @return array{index: string|null, columns: list} + */ + protected function parseUniqueConstraintViolation(Exception $exception): array + { + [$index, $columns] = [null, []]; + + if (preg_match('#unique constraint "([^"]+)"#i', $message = $exception->getMessage(), $matches)) { + $index = $matches[1]; + } + + if (preg_match('#Key \(([^)]+)\)=#i', $message, $matches)) { + $columns = array_map(trim(...), explode(',', $matches[1])); + } + + return ['columns' => $columns, 'index' => $index]; + } + /** * Get the default query grammar instance. * diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index db8afd79e52e..99d0f5e2c49f 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -62,6 +62,28 @@ protected function isUniqueConstraintError(Exception $exception) return (bool) preg_match('#(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)#i', $exception->getMessage()); } + /** + * Extract the columns that caused a unique constraint violation. + * + * @param Exception $exception + * @return array{index: null, columns: list} + */ + protected function parseUniqueConstraintViolation(Exception $exception): array + { + preg_match('#UNIQUE constraint failed: (.+)#i', $exception->getMessage(), $matches); + + $columns = []; + + if (isset($matches[1])) { + $columns = array_map( + static fn ($col) => last(explode('.', trim($col))), + explode(',', $matches[1]) + ); + } + + return ['columns' => $columns, 'index' => null]; + } + /** * Get the default query grammar instance. * diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index 7b3d0c5f0183..b18f97cb1f23 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -86,6 +86,25 @@ protected function isUniqueConstraintError(Exception $exception) return (bool) preg_match('#Cannot insert duplicate key row in object#i', $exception->getMessage()); } + /** + * Extract the index that caused a unique constraint violation. + * + * @param Exception $exception + * @return array{index: string|null, columns: list} + */ + protected function parseUniqueConstraintViolation(Exception $exception): array + { + $index = null; + + if (preg_match('#with unique index \'([^\']+)\'#i', $message = $exception->getMessage(), $matches)) { + $index = $matches[1]; + } elseif (preg_match('#Violation of [A-Z ]+ constraint \'([^\']+)\'#i', $message, $matches)) { + $index = $matches[1]; + } + + return ['columns' => [], 'index' => $index]; + } + /** * Get the default query grammar instance. * diff --git a/src/Illuminate/Database/UniqueConstraintViolationException.php b/src/Illuminate/Database/UniqueConstraintViolationException.php index 13b705b77c3b..6ea79edb3d64 100644 --- a/src/Illuminate/Database/UniqueConstraintViolationException.php +++ b/src/Illuminate/Database/UniqueConstraintViolationException.php @@ -4,4 +4,43 @@ class UniqueConstraintViolationException extends QueryException { + /** + * The unique index which prevented the query. + * + * @var string|null + */ + public ?string $index = null; + + /** + * The columns which caused the violation. + * + * @var list + */ + public array $columns = []; + + /** + * Set the unique index which caused the violation. + * + * @param string|null $index + * @return $this + */ + public function setIndex(?string $index): self + { + $this->index = $index; + + return $this; + } + + /** + * Set the columns that caused the violation. + * + * @param list $columns + * @return $this + */ + public function setColumns(array $columns): self + { + $this->columns = $columns; + + return $this; + } } diff --git a/tests/Integration/Database/UniqueConstraintViolationTest.php b/tests/Integration/Database/UniqueConstraintViolationTest.php new file mode 100644 index 000000000000..37f430a597e8 --- /dev/null +++ b/tests/Integration/Database/UniqueConstraintViolationTest.php @@ -0,0 +1,133 @@ +id(); + $table->string('name')->unique('single_unique_idx'); + }); + + Schema::create('test_unique_constraint_composite', function (Blueprint $table) { + $table->id(); + $table->string('first_name'); + $table->string('last_name'); + + $table->unique(['first_name', 'last_name'], 'unique_composite_idx'); + }); + } + + private function createUniqueModel(): UniqueConstraintViolationException + { + UniqueSingleModel::query()->create(['name' => 'test']); + try { + UniqueSingleModel::query()->create(['name' => 'test']); + } catch (UniqueConstraintViolationException $e) { + return $e; + } + $this->fail('No exception was thrown'); + } + + private function createCompositeModel(): UniqueConstraintViolationException + { + UniqueCompositeModel::query()->create(['first_name' => 'Taylor', 'last_name' => 'Otwell']); + try { + UniqueCompositeModel::query()->create(['first_name' => 'Taylor', 'last_name' => 'Otwell']); + } catch (UniqueConstraintViolationException $e) { + return $e; + } + + $this->fail('No exception was thrown'); + } + + #[RequiresDatabase('sqlite')] + public function testSqliteUniqueConstraint() + { + $e = $this->createUniqueModel(); + $this->assertSame(['name'], $e->columns); + $this->assertNull($e->index); + } + + #[RequiresDatabase('sqlite')] + public function testSqliteUniqueCompositeConstraint() + { + $e = $this->createCompositeModel(); + $this->assertSame(['first_name', 'last_name'], $e->columns); + $this->assertNull($e->index); + } + + #[RequiresDatabase('mysql')] + public function testMysqlUniqueConstraint() + { + $e = $this->createUniqueModel(); + $this->assertSame('single_unique_idx', $e->index); + $this->assertSame([], $e->columns); + } + + #[RequiresDatabase('mysql')] + public function testMysqlUniqueCompositeConstraint() + { + $e = $this->createCompositeModel(); + $this->assertSame('unique_composite_idx', $e->index); + $this->assertSame([], $e->columns); + } + + #[RequiresDatabase('pgsql')] + public function testPostgresUniqueConstraint() + { + $e = $this->createUniqueModel(); + $this->assertSame('single_unique_idx', $e->index); + $this->assertSame(['name'], $e->columns); + } + + #[RequiresDatabase('pgsql')] + public function testPostgresUniqueCompositeConstraint() + { + $e = $this->createCompositeModel(); + $this->assertSame('unique_composite_idx', $e->index); + $this->assertSame(['first_name', 'last_name'], $e->columns); + } + + #[RequiresDatabase('sqlsrv')] + public function testSqlServerUniqueConstraint() + { + $e = $this->createUniqueModel(); + $this->assertSame('single_unique_idx', $e->index); + $this->assertSame([], $e->columns); + } + + #[RequiresDatabase('sqlsrv')] + public function testSqlServerUniqueCompositeConstraint() + { + $e = $this->createCompositeModel(); + $this->assertSame('unique_composite_idx', $e->index); + $this->assertSame([], $e->columns); + } +} + +class UniqueSingleModel extends Model +{ + protected $table = 'test_unique_constraint'; + + protected $fillable = ['name']; + + public $timestamps = false; +} + +class UniqueCompositeModel extends Model +{ + protected $table = 'test_unique_constraint_composite'; + + protected $fillable = ['first_name', 'last_name']; + + public $timestamps = false; +} From 8aa511e8f4615379000eae661f7f12ddeb6adb0f Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 22 Mar 2026 15:42:41 +0000 Subject: [PATCH 018/596] 13.x-timedout-worker--stop-reason (#59310) --- src/Illuminate/Queue/Worker.php | 7 ++++--- src/Illuminate/Queue/WorkerStopReason.php | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 6cfc2847a5bc..1825b13c26ad 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -255,7 +255,7 @@ protected function registerTimeoutHandler($job, WorkerOptions $options) )); } - $this->kill(static::EXIT_ERROR, $options); + $this->kill(static::EXIT_ERROR, $options, WorkerStopReason::TimedOut); }, true); pcntl_alarm( @@ -839,11 +839,12 @@ public function stop($status = 0, $options = null, $reason = null) * * @param int $status * @param \Illuminate\Queue\WorkerOptions|null $options + * @param \Illuminate\Queue\WorkerStopReason|null $reason * @return never */ - public function kill($status = 0, $options = null) + public function kill($status = 0, $options = null, $reason = null) { - $this->events->dispatch(new WorkerStopping($status, $options)); + $this->events->dispatch(new WorkerStopping($status, $options, $reason)); if (extension_loaded('posix')) { posix_kill(getmypid(), SIGKILL); diff --git a/src/Illuminate/Queue/WorkerStopReason.php b/src/Illuminate/Queue/WorkerStopReason.php index 59964fe6151e..52b7cb8ff2f6 100644 --- a/src/Illuminate/Queue/WorkerStopReason.php +++ b/src/Illuminate/Queue/WorkerStopReason.php @@ -10,4 +10,5 @@ enum WorkerStopReason: string case MaxTimeExceeded = 'max_time'; case QueueEmpty = 'empty'; case ReceivedRestartSignal = 'restart_signal'; + case TimedOut = 'timed_out'; } From 05e550dc4958520beb221de50cfdf6172d49df23 Mon Sep 17 00:00:00 2001 From: sadique hussain <32757358+sadique-cws@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:56:54 +0530 Subject: [PATCH 019/596] Fix Table attribute incrementing not working for Pivot models (#59336) * Fix Table attribute incrementing not working for Pivot models The `#[Table]` attribute includes an argument to specify whether the primary key should be incrementing. However, for Pivot models that default to non-incrementing, the Table attribute's incrementing argument was not being applied because the logic only checked it when `$this->incrementing === true`. This fix removes the outer condition so that the Table attribute can override the default incrementing behavior for all models, including Pivots. * Update Model.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Eloquent/Model.php | 10 ++++------ .../DatabaseEloquentModelAttributesTest.php | 13 +++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 1993b73f6eb8..e1df4268d405 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -446,12 +446,10 @@ public function initializeModelAttributes() $this->keyType = $table->keyType; } - if ($this->incrementing === true) { - if (static::resolveClassAttribute(WithoutIncrementing::class) !== null) { - $this->incrementing = false; - } elseif ($table && $table->incrementing !== null) { - $this->incrementing = $table->incrementing; - } + if (static::resolveClassAttribute(WithoutIncrementing::class) !== null) { + $this->incrementing = false; + } elseif ($table && $table->incrementing !== null) { + $this->incrementing = $table->incrementing; } } diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index 7cfc9cbfe4cb..be2c9351051f 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -107,6 +107,13 @@ public function test_dedicated_without_incrementing_attribute_overrides_table_in $this->assertFalse($model->getIncrementing()); } + public function test_table_attribute_incrementing_applies_to_pivot_models(): void + { + $model = new PivotWithIncrementing; + + $this->assertTrue($model->getIncrementing()); + } + public function test_connection_attribute(): void { $model = new ModelWithConnectionAttribute; @@ -504,3 +511,9 @@ class ModelWithWithoutIncrementingAttributeOverride extends Model { // } + +#[Table(incrementing: true)] +class PivotWithIncrementing extends \Illuminate\Database\Eloquent\Relations\Pivot +{ + // +} From 96f99108a7ee7d5cfb13ea2996249d7f7b02b025 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 23 Mar 2026 14:27:57 +0000 Subject: [PATCH 020/596] 13.x-scopedby-inheritence (#59332) ensure it works with inheritence --- .../Eloquent/Concerns/HasGlobalScopes.php | 8 ++++++++ .../DatabaseEloquentGlobalScopesTest.php | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php index 635ac8d1fe9d..243415b1442d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Database\Eloquent\Attributes\ScopedBy; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -38,8 +39,15 @@ public static function resolveGlobalScopeAttributes() $attributes->push(...$trait->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF)); } + $isEloquentGrandchild = is_subclass_of(static::class, Model::class) + && get_parent_class(static::class) !== Model::class; + return $attributes->map(fn ($attribute) => $attribute->getArguments()) ->flatten() + ->when($isEloquentGrandchild, function (Collection $attributes) { + return (new Collection(get_parent_class(static::class)::resolveGlobalScopeAttributes())) + ->merge($attributes); + }) ->all(); } diff --git a/tests/Database/DatabaseEloquentGlobalScopesTest.php b/tests/Database/DatabaseEloquentGlobalScopesTest.php index 0589af064495..62954c20b3da 100644 --- a/tests/Database/DatabaseEloquentGlobalScopesTest.php +++ b/tests/Database/DatabaseEloquentGlobalScopesTest.php @@ -68,6 +68,14 @@ public function testGlobalScopeInInheritedAttributeIsApplied() $this->assertEquals([1], $query->getBindings()); } + public function testGlobalScopeInParentClassAttributeIsApplied() + { + $model = new EloquentGlobalScopeInAttributeChildTestModel; + $query = $model->newQuery(); + $this->assertSame('select * from "table" where "active" = ?', $query->toSql()); + $this->assertEquals([1], $query->getBindings()); + } + public function testClosureGlobalScopeIsApplied() { $model = new EloquentClosureGlobalScopesTestModel; @@ -325,3 +333,14 @@ class EloquentGlobalScopeInInheritedAttributeTestModel extends Model protected $table = 'table'; } + +#[ScopedBy(ActiveScope::class)] +class EloquentGlobalScopeInAttributeParentTestModel extends Model +{ + protected $table = 'table'; +} + +class EloquentGlobalScopeInAttributeChildTestModel extends EloquentGlobalScopeInAttributeParentTestModel +{ + // +} From 531251333cfe0ce6c009defb09bd3079817d9c43 Mon Sep 17 00:00:00 2001 From: Mathieu TUDISCO Date: Mon, 23 Mar 2026 15:32:36 +0100 Subject: [PATCH 021/596] Modify sum callback to include item key (#59322) --- src/Illuminate/Collections/Traits/EnumeratesValues.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index a475ee10b145..d0f6102971ee 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -581,7 +581,7 @@ public function sum($callback = null) ? $this->identity() : $this->valueRetriever($callback); - return $this->reduce(fn ($result, $item) => $result + $callback($item), 0); + return $this->reduce(fn ($result, $item, $key) => $result + $callback($item, $key), 0); } /** From b3fe63ce192919352c2b18bc300ab0106ef7a6f9 Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Tue, 24 Mar 2026 01:26:25 +1000 Subject: [PATCH 022/596] [13.x] Bound error page query listener to prevent memory bloat in Octane (#59309) * Bound error page query listener to prevent memory bloat in Octane workers Co-Authored-By: Claude Opus 4.6 (1M context) * Update Listener.php --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Taylor Otwell --- .../Exceptions/Renderer/Listener.php | 16 ++- .../Exceptions/Renderer/ListenerTest.php | 130 ++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php b/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php index ad6ffe402047..2e53146d5cc3 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php @@ -59,15 +59,25 @@ public function queries() */ public function onQueryExecuted(QueryExecuted $event) { - if (count($this->queries) === 101) { + if (count($this->queries) >= 100) { return; } + $sql = strlen($event->sql) <= 2000 + ? $event->sql + : mb_strcut($event->sql, 0, 2000); + + $bindings = $event->connection->prepareBindings($event->bindings); + + $bindingCount = substr_count($sql, '?'); + $this->queries[] = [ 'connectionName' => $event->connectionName, 'time' => $event->time, - 'sql' => $event->sql, - 'bindings' => $event->connection->prepareBindings($event->bindings), + 'sql' => $sql, + 'bindings' => count($bindings) <= $bindingCount + ? $bindings + : array_slice($bindings, 0, $bindingCount), ]; } } diff --git a/tests/Foundation/Exceptions/Renderer/ListenerTest.php b/tests/Foundation/Exceptions/Renderer/ListenerTest.php index 889dbc4d2472..cef5c0c135f1 100644 --- a/tests/Foundation/Exceptions/Renderer/ListenerTest.php +++ b/tests/Foundation/Exceptions/Renderer/ListenerTest.php @@ -39,4 +39,134 @@ public function test_queries_returns_expected_shape_after_query_executed() $this->assertEquals('select * from users where id = ?', $query['sql']); $this->assertEquals(['foo'], $query['bindings']); } + + public function test_listener_caps_at_100_queries() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + for ($i = 0; $i < 150; $i++) { + $listener->onQueryExecuted( + new QueryExecuted("select {$i}", [], 1.0, $connection) + ); + } + + $this->assertCount(100, $listener->queries()); + $this->assertEquals('select 0', $listener->queries()[0]['sql']); + $this->assertEquals('select 99', $listener->queries()[99]['sql']); + } + + public function test_large_sql_is_truncated() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + $largeSql = str_repeat('x', 5000); + $listener->onQueryExecuted( + new QueryExecuted($largeSql, [], 1.0, $connection) + ); + + $this->assertLessThanOrEqual(2000, strlen($listener->queries()[0]['sql'])); + } + + public function test_bindings_match_placeholder_count_in_truncated_sql() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + // Build SQL with 500 placeholders — when truncated to 2000 bytes, + // only some ? will remain, and bindings should match that count. + $placeholders = implode(', ', array_fill(0, 500, '?')); + $sql = "INSERT INTO t (a) VALUES ({$placeholders})"; + $bindings = array_fill(0, 500, 'value'); + + $listener->onQueryExecuted( + new QueryExecuted($sql, $bindings, 1.0, $connection) + ); + + $storedQuery = $listener->queries()[0]; + $storedPlaceholders = substr_count($storedQuery['sql'], '?'); + + $this->assertCount($storedPlaceholders, $storedQuery['bindings']); + } + + public function test_excess_bindings_are_trimmed_to_match_placeholders() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + // 1 placeholder but 1000 bindings — only 1 binding should be kept + $listener->onQueryExecuted( + new QueryExecuted('select ?', array_fill(0, 1000, 'v'), 1.0, $connection) + ); + + $this->assertCount(1, $listener->queries()[0]['bindings']); + } + + public function test_short_sql_and_bindings_are_not_modified() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + $sql = 'select * from users where name = ?'; + $listener->onQueryExecuted( + new QueryExecuted($sql, ['John'], 1.0, $connection) + ); + + $this->assertEquals($sql, $listener->queries()[0]['sql']); + $this->assertEquals(['John'], $listener->queries()[0]['bindings']); + } + + public function test_query_with_no_bindings_is_unchanged() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + $listener->onQueryExecuted( + new QueryExecuted('select count(*) from users', [], 1.0, $connection) + ); + + $this->assertEquals('select count(*) from users', $listener->queries()[0]['sql']); + $this->assertEmpty($listener->queries()[0]['bindings']); + } + + public function test_normal_query_skips_truncation() + { + $listener = new Listener(); + + $connection = m::mock(); + $connection->shouldReceive('getName')->andReturn('testing'); + $connection->shouldReceive('prepareBindings')->andReturnUsing(fn ($b) => $b); + + $sql = 'select * from users where id = ? and name = ? and email = ?'; + $bindings = [1, 'John', 'john@example.com']; + + $listener->onQueryExecuted( + new QueryExecuted($sql, $bindings, 1.0, $connection) + ); + + $storedQuery = $listener->queries()[0]; + + // Nothing should be modified — SQL is short and bindings match placeholders + $this->assertEquals($sql, $storedQuery['sql']); + $this->assertEquals($bindings, $storedQuery['bindings']); + } } From f513824416edd00889d149c657cb85703e0df803 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 23 Mar 2026 15:58:59 +0000 Subject: [PATCH 023/596] [13.x] Allow opting out of worker Job exception reporting (#59308) * Update Worker.php * add a test * wip - more specific homie --- src/Illuminate/Queue/Worker.php | 11 ++++++++++- tests/Queue/QueueWorkerTest.php | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 1825b13c26ad..88fd17e72dd9 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -106,6 +106,13 @@ class Worker */ public static $memoryExceededExitCode; + /** + * Indicates if the worker should report job exceptions. + * + * @var bool + */ + public static $reportJobExceptions = true; + /** * Indicates if the worker should check for the restart signal in the cache. * @@ -434,7 +441,9 @@ protected function runJob($job, $connectionName, WorkerOptions $options) try { return $this->process($connectionName, $job, $options); } catch (Throwable $e) { - $this->exceptions->report($e); + if (static::$reportJobExceptions) { + $this->exceptions->report($e); + } $this->stopWorkerIfLostConnection($e); } diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index 83c17c94ef8a..b196db433657 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -195,6 +195,27 @@ public function testJobIsReleasedOnException() $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); } + public function testExceptionIsNotReportedIfReportJobExceptionsIsDisabled() + { + $e = new RuntimeException; + + $job = new WorkerFakeJob(function () use ($e) { + throw $e; + }); + + Worker::$reportJobExceptions = false; + + try { + $worker = $this->getWorker('default', ['queue' => [$job]]); + $worker->runNextJob('default', 'queue', $this->workerOptions(['backoff' => 10])); + + $this->exceptionHandler->shouldNotHaveReceived('report'); + $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); + } finally { + Worker::$reportJobExceptions = true; + } + } + public function testJobIsNotReleasedIfItHasExceededMaxAttempts() { $e = new RuntimeException; From ede45d83d47f41d374762ecda3122b73f7403aa3 Mon Sep 17 00:00:00 2001 From: dr-codswallop <49959728+dr-codswallop@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:04:05 -0500 Subject: [PATCH 024/596] Add safe area inset support to exception renderer (#59341) Include viewport-fit=cover in the exception renderer layout meta to respect device safe areas (e.g. iPhone notch, Dynamic Island). Adds body padding via CSS. --- .../exceptions/renderer/components/layout.blade.php | 2 +- .../resources/exceptions/renderer/dist/styles.css | 2 +- .../Foundation/resources/exceptions/renderer/styles.css | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/layout.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/layout.blade.php index 25a28d86740d..11ee942a2347 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/layout.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/layout.blade.php @@ -3,7 +3,7 @@ - + {{ config('app.name', 'Laravel') }} diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/dist/styles.css b/src/Illuminate/Foundation/resources/exceptions/renderer/dist/styles.css index 1cfe9edada25..b2a497f74c34 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/dist/styles.css +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/dist/styles.css @@ -1 +1 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-\[-1px\]{top:-1px}.right-0{right:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.-z-10{z-index:-10}.z-50{z-index:50}.mx-auto{margin-inline:auto}.my-1\.5{margin-block:calc(var(--spacing)*1.5)}.-mt-3{margin-top:calc(var(--spacing)*-3)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mr-6{margin-right:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-\[9px\]{width:9px;height:9px}.h-0{height:calc(var(--spacing)*0)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-\[18px\]{height:18px}.h-\[23\.5px\]{height:23.5px}.h-\[56px\]{height:56px}.min-h-dvh{min-height:100dvh}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-\[18px\]{width:18px}.w-fit{width:fit-content}.w-full{width:100%}.w-px{width:1px}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-5{min-width:calc(var(--spacing)*5)}.min-w-6{min-width:calc(var(--spacing)*6)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)!important}.cursor-not-allowed\!{cursor:not-allowed!important}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-12{gap:calc(var(--spacing)*12)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-200>:not(:last-child)){border-color:var(--color-neutral-200)}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-emerald-900{border-color:var(--color-emerald-900)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-black\/8{background-color:#00000014}@supports (color:color-mix(in lab,red,red)){.bg-black\/8{background-color:color-mix(in oklab,var(--color-black)8%,transparent)}}.bg-black\/10{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.bg-black\/10{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-emerald-200{background-color:var(--color-emerald-200)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-800{background-color:var(--color-emerald-800)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-rose-200{background-color:var(--color-rose-200)}.bg-rose-200\!{background-color:var(--color-rose-200)!important}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-600{background-color:var(--color-rose-600)}.bg-transparent\!{background-color:#0000!important}.bg-white{background-color:var(--color-white)}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.bg-white\/50{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.bg-white\/50{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.bg-white\/\[2\%\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[2\%\]{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-2\.5{padding:calc(var(--spacing)*2.5)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-\[6px\]{padding-inline:6px}.py-0{padding-block:calc(var(--spacing)*0)}.py-1{padding-block:calc(var(--spacing)*1)}.py-4{padding-block:calc(var(--spacing)*4)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-14{padding-top:calc(var(--spacing)*14)}.pr-2\.5{padding-right:calc(var(--spacing)*2.5)}.pb-0{padding-bottom:calc(var(--spacing)*0)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-xs\/none{font-size:var(--text-xs);line-height:1}.text-\[13px\]{font-size:13px}.leading-3{--tw-leading:calc(var(--spacing)*3);line-height:calc(var(--spacing)*3)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-words{overflow-wrap:break-word}.text-amber-900{color:var(--color-amber-900)}.text-blue-500{color:var(--color-blue-500)}.text-blue-900{color:var(--color-blue-900)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-900{color:var(--color-emerald-900)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-500\!{color:var(--color-neutral-500)!important}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-neutral-950{color:var(--color-neutral-950)}.text-rose-900{color:var(--color-rose-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.decoration-neutral-400{-webkit-text-decoration-color:var(--color-neutral-400);text-decoration-color:var(--color-neutral-400)}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.scheme-light-dark{color-scheme:light dark}.opacity-90{opacity:.9}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}@media(hover:hover){.group-hover\:text-blue-500:is(:where(.group):hover *),.group-hover\/exception\:text-blue-500:is(:where(.group\/exception):hover *){color:var(--color-blue-500)}}.odd\:bg-white\/2:nth-child(odd){background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.odd\:bg-white\/2:nth-child(odd){background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}@media(hover:hover){.hover\:border:hover{border-style:var(--tw-border-style);border-width:1px}.hover\:border-neutral-200:hover{border-color:var(--color-neutral-200)}.hover\:bg-neutral-100:hover{background-color:var(--color-neutral-100)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-white\/50:hover{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/50:hover{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:underline:hover{text-decoration-line:underline}}@media(min-width:40rem){.sm\:mb-16{margin-bottom:calc(var(--spacing)*16)}.sm\:p-14{padding:calc(var(--spacing)*14)}.sm\:py-0{padding-block:calc(var(--spacing)*0)}.sm\:pt-16{padding-top:calc(var(--spacing)*16)}.sm\:pb-0{padding-bottom:calc(var(--spacing)*0)}}@media(prefers-color-scheme:dark){:where(.dark\:divide-white\/5>:not(:last-child)){border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){:where(.dark\:divide-white\/5>:not(:last-child)){border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}:where(.dark\:divide-white\/10>:not(:last-child)){border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){:where(.dark\:divide-white\/10>:not(:last-child)){border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:border{border-style:var(--tw-border-style);border-width:1px}.dark\:border-none{--tw-border-style:none;border-style:none}.dark\:border-amber-500{border-color:var(--color-amber-500)}.dark\:border-amber-800{border-color:var(--color-amber-800)}.dark\:border-blue-600{border-color:var(--color-blue-600)}.dark\:border-blue-800{border-color:var(--color-blue-800)}.dark\:border-emerald-500{border-color:var(--color-emerald-500)}.dark\:border-emerald-600{border-color:var(--color-emerald-600)}.dark\:border-neutral-500{border-color:var(--color-neutral-500)}.dark\:border-neutral-700{border-color:var(--color-neutral-700)}.dark\:border-neutral-800{border-color:var(--color-neutral-800)}.dark\:border-rose-500{border-color:var(--color-rose-500)}.dark\:border-rose-900{border-color:var(--color-rose-900)}.dark\:border-transparent{border-color:#0000}.dark\:border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/5{border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.dark\:border-white\/8{border-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/8{border-color:color-mix(in oklab,var(--color-white)8%,transparent)}}.dark\:border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.dark\:border-white\/\[9\%\]{border-color:#ffffff17}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/\[9\%\]{border-color:color-mix(in oklab,var(--color-white)9%,transparent)}}.dark\:bg-\[\#1a1a1a\]{background-color:#1a1a1a}.dark\:bg-amber-600{background-color:var(--color-amber-600)}.dark\:bg-amber-950{background-color:var(--color-amber-950)}.dark\:bg-blue-700{background-color:var(--color-blue-700)}.dark\:bg-blue-950{background-color:var(--color-blue-950)}.dark\:bg-emerald-600{background-color:var(--color-emerald-600)}.dark\:bg-emerald-900\/70{background-color:#004e3bb3}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-900\/70{background-color:color-mix(in oklab,var(--color-emerald-900)70%,transparent)}}.dark\:bg-neutral-400{background-color:var(--color-neutral-400)}.dark\:bg-neutral-600{background-color:var(--color-neutral-600)}.dark\:bg-neutral-700{background-color:var(--color-neutral-700)}.dark\:bg-neutral-800{background-color:var(--color-neutral-800)}.dark\:bg-neutral-900{background-color:var(--color-neutral-900)}.dark\:bg-rose-600{background-color:var(--color-rose-600)}.dark\:bg-rose-900\!{background-color:var(--color-rose-900)!important}.dark\:bg-rose-950{background-color:var(--color-rose-950)}.dark\:bg-transparent{background-color:#0000}.dark\:bg-white{background-color:var(--color-white)}.dark\:bg-white\/1{background-color:#ffffff03}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/1{background-color:color-mix(in oklab,var(--color-white)1%,transparent)}}.dark\:bg-white\/2{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/2{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.dark\:bg-white\/3{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/3{background-color:color-mix(in oklab,var(--color-white)3%,transparent)}}.dark\:bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/5{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.dark\:bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:bg-white\/\[2\%\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/\[2\%\]{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.dark\:bg-white\/\[3\%\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/\[3\%\]{background-color:color-mix(in oklab,var(--color-white)3%,transparent)}}.dark\:text-amber-300{color:var(--color-amber-300)}.dark\:text-blue-300{color:var(--color-blue-300)}.dark\:text-emerald-400{color:var(--color-emerald-400)}.dark\:text-emerald-500{color:var(--color-emerald-500)}.dark\:text-neutral-100{color:var(--color-neutral-100)}.dark\:text-neutral-200{color:var(--color-neutral-200)}.dark\:text-neutral-300{color:var(--color-neutral-300)}.dark\:text-neutral-400{color:var(--color-neutral-400)}.dark\:text-neutral-500{color:var(--color-neutral-500)}.dark\:text-neutral-600{color:var(--color-neutral-600)}.dark\:text-neutral-600\!{color:var(--color-neutral-600)!important}.dark\:text-neutral-900{color:var(--color-neutral-900)}.dark\:text-rose-100{color:var(--color-rose-100)}.dark\:text-white{color:var(--color-white)}.dark\:text-white\!{color:var(--color-white)!important}}@media(hover:hover){@media(prefers-color-scheme:dark){.group-hover\:dark\:text-emerald-500:is(:where(.group):hover *),.group-hover\/exception\:dark\:text-emerald-500:is(:where(.group\/exception):hover *){color:var(--color-emerald-500)}}}@media(prefers-color-scheme:dark){.odd\:dark\:bg-white\/4:nth-child(odd){background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.odd\:dark\:bg-white\/4:nth-child(odd){background-color:color-mix(in oklab,var(--color-white)4%,transparent)}}.even\:dark\:bg-white\/2:nth-child(2n){background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.even\:dark\:bg-white\/2:nth-child(2n){background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}@media(hover:hover){.dark\:hover\:border-none:hover{--tw-border-style:none;border-style:none}.dark\:hover\:bg-white\/2:hover{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-white\/2:hover{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.dark\:hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.dark\:hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}}}@media(hover:hover){@media(prefers-color-scheme:dark){.hover\:dark\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:dark\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}}}@media(prefers-color-scheme:dark){@media(hover:hover){.dark\:hover\:text-emerald-500:hover{color:var(--color-emerald-500)}}}@media(hover:hover){@media(prefers-color-scheme:dark){.hover\:dark\:text-white:hover{color:var(--color-white)}}}.\[\&_svg\]\:size-2\.5 svg{width:calc(var(--spacing)*2.5);height:calc(var(--spacing)*2.5)}.\[\&_svg\]\:\!text-white svg{color:var(--color-white)!important}@media(hover:hover){.hover\:\[\&_svg\]\:stroke-emerald-500:hover svg{stroke:var(--color-emerald-500)}}@media(prefers-color-scheme:dark){.dark\:\[\&_svg\]\:\!text-white svg{color:var(--color-white)!important}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{color:#fff;white-space:normal;background-color:#333;border-radius:4px;outline:0;font-size:14px;line-height:1.4;transition-property:transform,visibility,opacity;position:relative}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-width:8px 8px 0;border-top-color:initial;transform-origin:top;bottom:-7px;left:0}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-width:0 8px 8px;border-bottom-color:initial;transform-origin:bottom;top:-7px;left:0}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;transform-origin:0;right:-7px}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:100%;left:-7px}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;width:16px;height:16px}.tippy-arrow:before{content:"";border-style:solid;border-color:#0000;position:absolute}.tippy-content{z-index:1;padding:5px 9px;position:relative}.tippy-box[data-animation=shift-away][data-state=hidden]{opacity:0}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=top]{transform:translateY(10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=bottom]{transform:translateY(-10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=left]{transform:translate(10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=right]{transform:translate(-10px)}[x-cloak]{display:none!important}.tippy-box[data-theme~=laravel]{border-radius:var(--radius-md);border-style:var(--tw-border-style);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);border-width:1px;border-color:var(--color-neutral-800);background-color:var(--color-neutral-900);color:var(--color-white);overflow-x:auto;max-width:var(--container-7xl)!important}@media(prefers-color-scheme:dark){.tippy-box[data-theme~=laravel]{border-color:var(--color-neutral-700);background-color:var(--color-neutral-800);color:var(--color-neutral-100)}}.tippy-content[data-theme~=laravel]{padding-inline:calc(var(--spacing)*2);padding-block:calc(var(--spacing)*1)}@media(prefers-color-scheme:dark){.shiki,.shiki span{color:var(--shiki-dark)!important;font-style:var(--shiki-dark-font-style)!important;font-weight:var(--shiki-dark-font-weight)!important;-webkit-text-decoration:var(--shiki-dark-text-decoration)!important;text-decoration:var(--shiki-dark-text-decoration)!important}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-\[-1px\]{top:-1px}.right-0{right:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.-z-10{z-index:-10}.z-50{z-index:50}.mx-auto{margin-inline:auto}.my-1\.5{margin-block:calc(var(--spacing)*1.5)}.-mt-3{margin-top:calc(var(--spacing)*-3)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mr-6{margin-right:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-\[9px\]{width:9px;height:9px}.h-0{height:calc(var(--spacing)*0)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-\[18px\]{height:18px}.h-\[23\.5px\]{height:23.5px}.h-\[56px\]{height:56px}.min-h-dvh{min-height:100dvh}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-\[18px\]{width:18px}.w-fit{width:fit-content}.w-full{width:100%}.w-px{width:1px}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-5{min-width:calc(var(--spacing)*5)}.min-w-6{min-width:calc(var(--spacing)*6)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)!important}.cursor-not-allowed\!{cursor:not-allowed!important}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-12{gap:calc(var(--spacing)*12)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-200>:not(:last-child)){border-color:var(--color-neutral-200)}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-emerald-900{border-color:var(--color-emerald-900)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-black\/8{background-color:#00000014}@supports (color:color-mix(in lab,red,red)){.bg-black\/8{background-color:color-mix(in oklab,var(--color-black)8%,transparent)}}.bg-black\/10{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.bg-black\/10{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-emerald-200{background-color:var(--color-emerald-200)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-800{background-color:var(--color-emerald-800)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-rose-200{background-color:var(--color-rose-200)}.bg-rose-200\!{background-color:var(--color-rose-200)!important}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-600{background-color:var(--color-rose-600)}.bg-transparent\!{background-color:#0000!important}.bg-white{background-color:var(--color-white)}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.bg-white\/50{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.bg-white\/50{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.bg-white\/\[2\%\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[2\%\]{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-2\.5{padding:calc(var(--spacing)*2.5)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-\[6px\]{padding-inline:6px}.py-0{padding-block:calc(var(--spacing)*0)}.py-1{padding-block:calc(var(--spacing)*1)}.py-4{padding-block:calc(var(--spacing)*4)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-14{padding-top:calc(var(--spacing)*14)}.pr-2\.5{padding-right:calc(var(--spacing)*2.5)}.pb-0{padding-bottom:calc(var(--spacing)*0)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-xs\/none{font-size:var(--text-xs);line-height:1}.text-\[13px\]{font-size:13px}.leading-3{--tw-leading:calc(var(--spacing)*3);line-height:calc(var(--spacing)*3)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-words{overflow-wrap:break-word}.text-amber-900{color:var(--color-amber-900)}.text-blue-500{color:var(--color-blue-500)}.text-blue-900{color:var(--color-blue-900)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-900{color:var(--color-emerald-900)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-500\!{color:var(--color-neutral-500)!important}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-neutral-950{color:var(--color-neutral-950)}.text-rose-900{color:var(--color-rose-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.decoration-neutral-400{-webkit-text-decoration-color:var(--color-neutral-400);text-decoration-color:var(--color-neutral-400)}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.scheme-light-dark{color-scheme:light dark}.opacity-90{opacity:.9}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}@media(hover:hover){.group-hover\:text-blue-500:is(:where(.group):hover *),.group-hover\/exception\:text-blue-500:is(:where(.group\/exception):hover *){color:var(--color-blue-500)}}.odd\:bg-white\/2:nth-child(odd){background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.odd\:bg-white\/2:nth-child(odd){background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}@media(hover:hover){.hover\:border:hover{border-style:var(--tw-border-style);border-width:1px}.hover\:border-neutral-200:hover{border-color:var(--color-neutral-200)}.hover\:bg-neutral-100:hover{background-color:var(--color-neutral-100)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-white\/50:hover{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/50:hover{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:underline:hover{text-decoration-line:underline}}@media(min-width:40rem){.sm\:mb-16{margin-bottom:calc(var(--spacing)*16)}.sm\:p-14{padding:calc(var(--spacing)*14)}.sm\:py-0{padding-block:calc(var(--spacing)*0)}.sm\:pt-16{padding-top:calc(var(--spacing)*16)}.sm\:pb-0{padding-bottom:calc(var(--spacing)*0)}}@media(prefers-color-scheme:dark){:where(.dark\:divide-white\/5>:not(:last-child)){border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){:where(.dark\:divide-white\/5>:not(:last-child)){border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}:where(.dark\:divide-white\/10>:not(:last-child)){border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){:where(.dark\:divide-white\/10>:not(:last-child)){border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:border{border-style:var(--tw-border-style);border-width:1px}.dark\:border-none{--tw-border-style:none;border-style:none}.dark\:border-amber-500{border-color:var(--color-amber-500)}.dark\:border-amber-800{border-color:var(--color-amber-800)}.dark\:border-blue-600{border-color:var(--color-blue-600)}.dark\:border-blue-800{border-color:var(--color-blue-800)}.dark\:border-emerald-500{border-color:var(--color-emerald-500)}.dark\:border-emerald-600{border-color:var(--color-emerald-600)}.dark\:border-neutral-500{border-color:var(--color-neutral-500)}.dark\:border-neutral-700{border-color:var(--color-neutral-700)}.dark\:border-neutral-800{border-color:var(--color-neutral-800)}.dark\:border-rose-500{border-color:var(--color-rose-500)}.dark\:border-rose-900{border-color:var(--color-rose-900)}.dark\:border-transparent{border-color:#0000}.dark\:border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/5{border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.dark\:border-white\/8{border-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/8{border-color:color-mix(in oklab,var(--color-white)8%,transparent)}}.dark\:border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.dark\:border-white\/\[9\%\]{border-color:#ffffff17}@supports (color:color-mix(in lab,red,red)){.dark\:border-white\/\[9\%\]{border-color:color-mix(in oklab,var(--color-white)9%,transparent)}}.dark\:bg-\[\#1a1a1a\]{background-color:#1a1a1a}.dark\:bg-amber-600{background-color:var(--color-amber-600)}.dark\:bg-amber-950{background-color:var(--color-amber-950)}.dark\:bg-blue-700{background-color:var(--color-blue-700)}.dark\:bg-blue-950{background-color:var(--color-blue-950)}.dark\:bg-emerald-600{background-color:var(--color-emerald-600)}.dark\:bg-emerald-900\/70{background-color:#004e3bb3}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-900\/70{background-color:color-mix(in oklab,var(--color-emerald-900)70%,transparent)}}.dark\:bg-neutral-400{background-color:var(--color-neutral-400)}.dark\:bg-neutral-600{background-color:var(--color-neutral-600)}.dark\:bg-neutral-700{background-color:var(--color-neutral-700)}.dark\:bg-neutral-800{background-color:var(--color-neutral-800)}.dark\:bg-neutral-900{background-color:var(--color-neutral-900)}.dark\:bg-rose-600{background-color:var(--color-rose-600)}.dark\:bg-rose-900\!{background-color:var(--color-rose-900)!important}.dark\:bg-rose-950{background-color:var(--color-rose-950)}.dark\:bg-transparent{background-color:#0000}.dark\:bg-white{background-color:var(--color-white)}.dark\:bg-white\/1{background-color:#ffffff03}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/1{background-color:color-mix(in oklab,var(--color-white)1%,transparent)}}.dark\:bg-white\/2{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/2{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.dark\:bg-white\/3{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/3{background-color:color-mix(in oklab,var(--color-white)3%,transparent)}}.dark\:bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/5{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.dark\:bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:bg-white\/\[2\%\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/\[2\%\]{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.dark\:bg-white\/\[3\%\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.dark\:bg-white\/\[3\%\]{background-color:color-mix(in oklab,var(--color-white)3%,transparent)}}.dark\:text-amber-300{color:var(--color-amber-300)}.dark\:text-blue-300{color:var(--color-blue-300)}.dark\:text-emerald-400{color:var(--color-emerald-400)}.dark\:text-emerald-500{color:var(--color-emerald-500)}.dark\:text-neutral-100{color:var(--color-neutral-100)}.dark\:text-neutral-200{color:var(--color-neutral-200)}.dark\:text-neutral-300{color:var(--color-neutral-300)}.dark\:text-neutral-400{color:var(--color-neutral-400)}.dark\:text-neutral-500{color:var(--color-neutral-500)}.dark\:text-neutral-600{color:var(--color-neutral-600)}.dark\:text-neutral-600\!{color:var(--color-neutral-600)!important}.dark\:text-neutral-900{color:var(--color-neutral-900)}.dark\:text-rose-100{color:var(--color-rose-100)}.dark\:text-white{color:var(--color-white)}.dark\:text-white\!{color:var(--color-white)!important}}@media(hover:hover){@media(prefers-color-scheme:dark){.group-hover\:dark\:text-emerald-500:is(:where(.group):hover *),.group-hover\/exception\:dark\:text-emerald-500:is(:where(.group\/exception):hover *){color:var(--color-emerald-500)}}}@media(prefers-color-scheme:dark){.odd\:dark\:bg-white\/4:nth-child(odd){background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.odd\:dark\:bg-white\/4:nth-child(odd){background-color:color-mix(in oklab,var(--color-white)4%,transparent)}}.even\:dark\:bg-white\/2:nth-child(2n){background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.even\:dark\:bg-white\/2:nth-child(2n){background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}@media(hover:hover){.dark\:hover\:border-none:hover{--tw-border-style:none;border-style:none}.dark\:hover\:bg-white\/2:hover{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-white\/2:hover{background-color:color-mix(in oklab,var(--color-white)2%,transparent)}}.dark\:hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.dark\:hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}}}@media(hover:hover){@media(prefers-color-scheme:dark){.hover\:dark\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:dark\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}}}@media(prefers-color-scheme:dark){@media(hover:hover){.dark\:hover\:text-emerald-500:hover{color:var(--color-emerald-500)}}}@media(hover:hover){@media(prefers-color-scheme:dark){.hover\:dark\:text-white:hover{color:var(--color-white)}}}.\[\&_svg\]\:size-2\.5 svg{width:calc(var(--spacing)*2.5);height:calc(var(--spacing)*2.5)}.\[\&_svg\]\:\!text-white svg{color:var(--color-white)!important}@media(hover:hover){.hover\:\[\&_svg\]\:stroke-emerald-500:hover svg{stroke:var(--color-emerald-500)}}@media(prefers-color-scheme:dark){.dark\:\[\&_svg\]\:\!text-white svg{color:var(--color-white)!important}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{color:#fff;white-space:normal;background-color:#333;border-radius:4px;outline:0;font-size:14px;line-height:1.4;transition-property:transform,visibility,opacity;position:relative}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-width:8px 8px 0;border-top-color:initial;transform-origin:top;bottom:-7px;left:0}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-width:0 8px 8px;border-bottom-color:initial;transform-origin:bottom;top:-7px;left:0}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;transform-origin:0;right:-7px}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:100%;left:-7px}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;width:16px;height:16px}.tippy-arrow:before{content:"";border-style:solid;border-color:#0000;position:absolute}.tippy-content{z-index:1;padding:5px 9px;position:relative}.tippy-box[data-animation=shift-away][data-state=hidden]{opacity:0}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=top]{transform:translateY(10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=bottom]{transform:translateY(-10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=left]{transform:translate(10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=right]{transform:translate(-10px)}[x-cloak]{display:none!important}body{padding-top:env(safe-area-inset-top,0px);padding-right:env(safe-area-inset-right,0px);padding-bottom:env(safe-area-inset-bottom,0px);padding-left:env(safe-area-inset-left,0px)}.tippy-box[data-theme~=laravel]{border-radius:var(--radius-md);border-style:var(--tw-border-style);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);border-width:1px;border-color:var(--color-neutral-800);background-color:var(--color-neutral-900);color:var(--color-white);overflow-x:auto;max-width:var(--container-7xl)!important}@media(prefers-color-scheme:dark){.tippy-box[data-theme~=laravel]{border-color:var(--color-neutral-700);background-color:var(--color-neutral-800);color:var(--color-neutral-100)}}.tippy-content[data-theme~=laravel]{padding-inline:calc(var(--spacing)*2);padding-block:calc(var(--spacing)*1)}@media(prefers-color-scheme:dark){.shiki,.shiki span{color:var(--shiki-dark)!important;font-style:var(--shiki-dark-font-style)!important;font-weight:var(--shiki-dark-font-weight)!important;-webkit-text-decoration:var(--shiki-dark-text-decoration)!important;text-decoration:var(--shiki-dark-text-decoration)!important}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/styles.css b/src/Illuminate/Foundation/resources/exceptions/renderer/styles.css index 22a6bb97fcb4..29337530b21e 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/styles.css +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/styles.css @@ -7,6 +7,13 @@ display: none !important; } +body { + padding-top: env(safe-area-inset-top, 0px); + padding-right: env(safe-area-inset-right, 0px); + padding-bottom: env(safe-area-inset-bottom, 0px); + padding-left: env(safe-area-inset-left, 0px); +} + .tippy-box[data-theme~='laravel'] { @apply max-w-7xl! rounded-md border text-xs shadow-md backdrop-blur-md overflow-x-auto; @apply border-neutral-800 bg-neutral-900 text-white; From 08af86ac3cabe5316bdb247146c473a4aacdf725 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 24 Mar 2026 14:08:27 +0000 Subject: [PATCH 025/596] 13.x-allow-arrays-in-magic-has (#59343) allow arrays --- .../Database/Eloquent/Factories/Factory.php | 4 ++++ tests/Database/DatabaseEloquentFactoryTest.php | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 43fb2bcc295f..efaf904d3013 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -1148,6 +1148,10 @@ public function __call($method, $parameters) if (str_starts_with($method, 'for')) { return $this->for($factory->state($parameters[0] ?? []), $relationship); } elseif (str_starts_with($method, 'has')) { + if (count($parameters) > 1 && array_all($parameters, fn ($p) => is_array($p))) { + return $this->has($factory->forEachSequence(...$parameters), $relationship); + } + return $this->has( $factory ->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : 1) diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index a9213948a9eb..2cb37c377bb8 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -755,6 +755,22 @@ public function test_dynamic_has_and_for_methods() $this->assertCount(2, $post->comments); } + public function test_dynamic_has_methods_with_multiple_arrays() + { + Factory::guessFactoryNamesUsing(function ($model) { + return $model.'Factory'; + }); + + $user = FactoryTestUserFactory::new() + ->hasPosts(['title' => 'First Post'], ['title' => 'Second Post'], ['title' => 'Third Post']) + ->create(); + + $this->assertCount(3, $user->posts); + $this->assertSame('First Post', $user->posts[0]->title); + $this->assertSame('Second Post', $user->posts[1]->title); + $this->assertSame('Third Post', $user->posts[2]->title); + } + public function test_can_be_macroable() { $factory = FactoryTestUserFactory::new(); From 67b64d9a9157f48dae4b375c3887327478673943 Mon Sep 17 00:00:00 2001 From: sadique hussain <32757358+sadique-cws@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:39:51 +0530 Subject: [PATCH 026/596] Fix/validation wildcard array message type error (#59339) * [12.x] Fix TypeError when wildcard custom message is array without matching rule When a custom validation message is defined for a wildcard attribute key (e.g. students.*.grade) using a nested array of rule-specific messages, and the failing rule is not in that array, getFromLocalArray returned the entire array instead of null. This caused a TypeError in replaceInputPlaceholder (introduced in v12.55.1) which calls str_contains() on the message, expecting a string. The fix makes the wildcard branch consistent with the exact-match branch which already returns $message[$lowerRule] ?? null. Fixes #59316 * Add test for wildcard custom message on unmatched array rules --- .../Validation/Concerns/FormatsMessages.php | 4 ++-- tests/Validation/ValidationValidatorTest.php | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/FormatsMessages.php b/src/Illuminate/Validation/Concerns/FormatsMessages.php index 411b65a8e2bf..74f64123d203 100644 --- a/src/Illuminate/Validation/Concerns/FormatsMessages.php +++ b/src/Illuminate/Validation/Concerns/FormatsMessages.php @@ -122,8 +122,8 @@ protected function getFromLocalArray($attribute, $lowerRule, $source = null) if (preg_match('#^'.$pattern.'\z#u', $key) === 1) { $message = $source[$sourceKey]; - if (is_array($message) && isset($message[$lowerRule])) { - return $message[$lowerRule]; + if (is_array($message)) { + return $message[$lowerRule] ?? null; } return $message; diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 197c2b7e185e..9920a81dd3ec 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -105,6 +105,27 @@ public function testNestedArrayErrorMessagesAreRetrievedFromLocalArray() $this->assertSame('post name is required', $v->errors()->all()[0]); } + public function testWildcardArrayCustomMessagesHandleMissingRulesGracefullyWhenAnotherRuleFails() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, [ + 'users' => [ + [ + 'name' => 'Taylor', + ], + ], + ], [ + 'users.*.name' => ['required', 'in:Otwell'], + ], [ + 'users.*.name' => [ + 'required' => 'user name is required', + ], + ]); + + $this->assertFalse($v->passes()); + $this->assertSame('validation.in', $v->errors()->all()[0]); + } + public function testSometimesWorksOnNestedArrays() { $trans = $this->getIlluminateArrayTranslator(); From 41558b0706228093fbd3c78199eef72aa1f87dbb Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Tue, 24 Mar 2026 19:31:24 +0100 Subject: [PATCH 027/596] Preserve class type of mocked classes (#59353) --- .../Foundation/Testing/Concerns/InteractsWithContainer.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index 14b4234f6c28..b568d1845636 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -66,9 +66,11 @@ protected function instance($abstract, $instance) /** * Mock an instance of an object in the container. * - * @param string $abstract + * @template TInstance of object + * + * @param class-string $abstract * @param \Closure|null $mock - * @return \Mockery\MockInterface + * @return TInstance&\Mockery\MockInterface */ protected function mock($abstract, ?Closure $mock = null) { From 539d85a2d7d71421804ec2e68678172ee7f7e49a Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 24 Mar 2026 18:34:13 +0000 Subject: [PATCH 028/596] allow vaerdic for backoff (#59354) --- src/Illuminate/Queue/Attributes/Backoff.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/Attributes/Backoff.php b/src/Illuminate/Queue/Attributes/Backoff.php index b04374ee81ca..baabce4fe3e3 100644 --- a/src/Illuminate/Queue/Attributes/Backoff.php +++ b/src/Illuminate/Queue/Attributes/Backoff.php @@ -7,13 +7,20 @@ #[Attribute(Attribute::TARGET_CLASS)] class Backoff { + /** + * The backoff values. + * + * @var array|int + */ + public array|int $backoff; + /** * Create a new attribute instance. * - * @param array|int $backoff + * @param array|int ...$backoff */ - public function __construct(public array|int $backoff) + public function __construct(array|int ...$backoff) { - // + $this->backoff = count($backoff) === 1 ? $backoff[0] : $backoff; } } From 9e48d1fe933e89de628dafa167d2c5778566d4cf Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:42:09 +0000 Subject: [PATCH 029/596] Update version to v13.2.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index c8ac7c9a8742..61a534db25eb 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 = '13.1.1'; + const VERSION = '13.2.0'; /** * The base path for the Laravel installation. From ca551b0da5108aa4fb68ca6f986e33523f4332dc Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:43:57 +0000 Subject: [PATCH 030/596] Update CHANGELOG --- CHANGELOG.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 463836965740..205baf1e013f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.1.1...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.2.0...13.x) + +## [v13.2.0](https://github.com/laravel/framework/compare/v13.1.1...v13.2.0) - 2026-03-24 + +* feat(queue): support enums in `#[Queue]` and `#[Connection]` by [@innocenzi](https://github.com/innocenzi) in https://github.com/laravel/framework/pull/59278 +* Improve raw SQL binding substitution performance by [@gufoe](https://github.com/gufoe) in https://github.com/laravel/framework/pull/59277 +* [13.x] fix: add missing negate for SeeInHtml assertion by [@jesperbeisner](https://github.com/jesperbeisner) in https://github.com/laravel/framework/pull/59303 +* [13.x] Allow for passing enums to attributes by [@riesjart](https://github.com/riesjart) in https://github.com/laravel/framework/pull/59297 +* [13.x] Add releaseOnSignal param to withoutOverlapping by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59298 +* Add symmetrical, expressive attributes by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/59284 +* [13.x] Fix LazyPromise::wait() signature compatibility with Guzzle's PromiseInterface by [@shavonn](https://github.com/shavonn) in https://github.com/laravel/framework/pull/59301 +* [13.x] `schedule:list` display expression in the correct timezone by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/59286 +* Handle exceptions in eventStream to prevent fatal error by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/59292 +* [13.x] Ensure connectUsing works with UnitEnum / FileManager drive docblock by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59306 +* [13.x] Include columns and index in UniqueConstraintViolationException by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59299 +* [13.x] Add TimedOut worker stop reason by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59310 +* Fix Table attribute incrementing not working for Pivot models by [@sadique-cws](https://github.com/sadique-cws) in https://github.com/laravel/framework/pull/59336 +* [13.x] Ensure ScopedBy Attribute works with inheritance by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59332 +* Modify sum callback to include item key by [@mathieutu](https://github.com/mathieutu) in https://github.com/laravel/framework/pull/59322 +* [13.x] Bound error page query listener to prevent memory bloat in Octane by [@JoshSalway](https://github.com/JoshSalway) in https://github.com/laravel/framework/pull/59309 +* [13.x] Allow opting out of worker Job exception reporting by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59308 +* [13.x] Adds mobile safe-area-inset support to exception renderer by [@dr-codswallop](https://github.com/dr-codswallop) in https://github.com/laravel/framework/pull/59341 +* [13.x] Allow passing multiple arrays to has factory method by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59343 +* [13.x] Allow Backoff Attribute to be variadic by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59354 ## [v13.1.1](https://github.com/laravel/framework/compare/v13.1.0...v13.1.1) - 2026-03-18 From 092037c173fe19addf28a634f4ee6b7cc644e276 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 24 Mar 2026 23:27:52 +0000 Subject: [PATCH 031/596] [13.x] Forward releaseOnTerminationSignals through schedule groups (#59357) * Update PendingEventAttributes.php * you get the bag and fumble it i forgot the tests * regression test --- .../Console/Scheduling/PendingEventAttributes.php | 7 +++++-- .../Console/Scheduling/ScheduleGroupTest.php | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php index 55550c95e57e..10eb7b85b8c0 100644 --- a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php +++ b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php @@ -30,14 +30,17 @@ public function __construct( * The expiration time of the underlying cache lock may be specified in minutes. * * @param int $expiresAt + * @param bool $releaseOnTerminationSignals * @return $this */ - public function withoutOverlapping($expiresAt = 1440) + public function withoutOverlapping($expiresAt = 1440, $releaseOnTerminationSignals = true) { $this->withoutOverlapping = true; $this->expiresAt = $expiresAt; + $this->releaseOnTerminationSignals = $releaseOnTerminationSignals; + return $this; } @@ -74,7 +77,7 @@ public function mergeAttributes(Event $event): void } if ($this->withoutOverlapping) { - $event->withoutOverlapping($this->expiresAt); + $event->withoutOverlapping($this->expiresAt, $this->releaseOnTerminationSignals); } if ($this->onOneServer) { diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index b107e1e5f890..d0a9deef3ebf 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -102,6 +102,7 @@ public function testGroupCanApplyAttributeToSchedules(string $property, mixed $v } else { $this->assertSame($value, $events[0]->expiresAt); $this->assertTrue($events[0]->withoutOverlapping); + $this->assertTrue($events[0]->releaseOnTerminationSignals); } } @@ -216,6 +217,20 @@ public function testGroupedPendingEventAttributesWithoutOverlapping() $this->assertSame('0 4 * * 1-5', $events[3]->expression); } + public function testGroupCanOptOutOfReleaseOnTerminationSignals() + { + $schedule = new ScheduleClass; + $schedule->daily() + ->withoutOverlapping(1440, releaseOnTerminationSignals: false) + ->group(function ($schedule) { + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertTrue($events[0]->withoutOverlapping); + $this->assertFalse($events[0]->releaseOnTerminationSignals); + } + public function testGroupAppliesEventMacrosToAllEvents() { Event::macro('sentryMonitor', function () { From 636d4a7e53b3562c8d54962867108bdc31dae9c5 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:28:22 +0000 Subject: [PATCH 032/596] Update facade docblocks --- src/Illuminate/Support/Facades/Schedule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index dc442fbb7991..86a2c02e6933 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -20,7 +20,7 @@ * @method static bool hasMacro(string $name) * @method static void flushMacros() * @method static mixed macroCall(string $method, array $parameters) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes withoutOverlapping(int $expiresAt = 1440) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes withoutOverlapping(int $expiresAt = 1440, bool $releaseOnTerminationSignals = true) * @method static void mergeAttributes(\Illuminate\Console\Scheduling\Event $event) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes user(string $user) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes environments(mixed $environments) From 7106836552f7b1186248eee4840b3eeb161497d3 Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Thu, 26 Mar 2026 00:07:28 +1000 Subject: [PATCH 033/596] [13.x] Fix sub-minute scheduling skips at minute boundaries (#59331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix sub-minute scheduling skipping executions at minute boundaries The repeatEvents() method called endOfMinute() on startedAt directly, which mutated the Carbon instance. This caused the boundary to shift on each iteration, leading to skipped executions. Using copy() before endOfMinute() preserves the original timestamp. Also adds an early return if the minute boundary is crossed mid-iteration. Fixes laravel/framework#57070 Co-Authored-By: Claude Opus 4.6 (1M context) * Add test verifying repeatEvents does not mutate startedAt Confirms the Carbon mutation bug is fixed — endOfMinute() was modifying $this->startedAt in place on each loop iteration. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix StyleCI: remove unused import, use proper class imports Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../Console/Scheduling/ScheduleRunCommand.php | 8 ++++- .../Scheduling/ScheduleRunCommandTest.php | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index f54081571287..eff60b53e5a9 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -240,7 +240,9 @@ protected function repeatEvents($events) { $hasEnteredMaintenanceMode = false; - while (Date::now()->lte($this->startedAt->endOfMinute())) { + $endOfMinute = $this->startedAt->copy()->endOfMinute(); + + while (Date::now()->lte($endOfMinute)) { $paused = $this->isPaused(); foreach ($events as $event) { @@ -252,6 +254,10 @@ protected function repeatEvents($events) continue; } + if (Date::now()->gt($endOfMinute)) { + return; + } + $hasEnteredMaintenanceMode = $hasEnteredMaintenanceMode || $this->laravel->isDownForMaintenance(); if ($hasEnteredMaintenanceMode && ! $event->runsInMaintenanceMode()) { diff --git a/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php index 69fa0d23ea61..796e497d0ab5 100644 --- a/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php @@ -6,10 +6,13 @@ use Illuminate\Console\Events\ScheduledTaskFinished; use Illuminate\Console\Events\ScheduledTaskStarting; use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Console\Scheduling\ScheduleRunCommand; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Event; use Orchestra\Testbench\TestCase; +use ReflectionMethod; +use ReflectionProperty; class ScheduleRunCommandTest extends TestCase { @@ -212,4 +215,31 @@ public function test_command_with_no_explicit_return_in_background_does_not_trig Event::assertDispatched(ScheduledTaskFinished::class); Event::assertNotDispatched(ScheduledTaskFailed::class); } + + public function test_repeat_events_does_not_mutate_started_at() + { + Carbon::setTestNow('2026-03-25 12:00:30'); + + $command = new ScheduleRunCommand; + $this->app->instance(ScheduleRunCommand::class, $command); + + $reflection = new ReflectionProperty($command, 'startedAt'); + $startedAt = $reflection->getValue($command); + + $originalTimestamp = $startedAt->timestamp; + $originalMicro = $startedAt->micro; + + // Call repeatEvents with an empty collection so it exits immediately + $reflection = new ReflectionMethod($command, 'repeatEvents'); + $command->setLaravel($this->app); + + // Set test time past the minute boundary so the while loop exits immediately + Carbon::setTestNow('2026-03-25 12:01:01'); + $reflection->invoke($command, collect()); + + // startedAt should not have been mutated to end of minute + $startedAtAfter = (new ReflectionProperty($command, 'startedAt'))->getValue($command); + $this->assertEquals($originalTimestamp, $startedAtAfter->timestamp); + $this->assertEquals($originalMicro, $startedAtAfter->micro); + } } From c21f6ab2052e45cbeb7be403c427a1ac82766c23 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 26 Mar 2026 14:25:32 +0000 Subject: [PATCH 034/596] 13.x-show-mem-usage-in-verbose (#59379) --- src/Illuminate/Queue/Console/WorkCommand.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index 04ecf2f9d6ca..648984c50c15 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -256,13 +256,14 @@ protected function writeOutputForCli(Job $job, $status) } $runTime = $this->runTimeForHumans($this->latestStartedAt); + $memory = $isVerbose ? round(memory_get_usage(true) / 1024 / 1024, 1).'MB' : ''; $dots = max(terminal()->width() - mb_strlen($job->resolveName()) - ( - $isVerbose ? mb_strlen($job->getJobId()) + mb_strlen($job->getConnectionName()) + mb_strlen($job->getQueue()) + 2 : 0 + $isVerbose ? mb_strlen($job->getJobId()) + mb_strlen($job->getConnectionName()) + mb_strlen($job->getQueue()) + mb_strlen($memory) + 3 : 0 ) - mb_strlen($runTime) - 31, 0); $this->output->write(' '.str_repeat('.', $dots)); - $this->output->write(" $runTime"); + $this->output->write(" {$runTime}".($memory ? " {$memory}" : '').''); $this->output->writeln(match ($status) { 'success' => ' DONE', From c9bf2cd8d0b064b52f0b7886f709a9c71c04d108 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:27:48 -0400 Subject: [PATCH 035/596] Update shared lock key documentation for clarity (#59375) --- src/Illuminate/Queue/Middleware/WithoutOverlapping.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Middleware/WithoutOverlapping.php b/src/Illuminate/Queue/Middleware/WithoutOverlapping.php index 19b4c27fb6f2..19875760e3e1 100644 --- a/src/Illuminate/Queue/Middleware/WithoutOverlapping.php +++ b/src/Illuminate/Queue/Middleware/WithoutOverlapping.php @@ -135,7 +135,7 @@ public function withPrefix(string $prefix) } /** - * Indicate that the lock key should be shared across job classes. + * Indicate that the lock key may be shared across jobs belonging to different classes. * * @return $this */ From 54145eea6956b7fec2ca07bf80e2a4cedd24f968 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:28:10 +0100 Subject: [PATCH 036/596] Fix dependency injection of faked queueing dispatcher (#59378) --- src/Illuminate/Bus/BusServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Bus/BusServiceProvider.php b/src/Illuminate/Bus/BusServiceProvider.php index a48dc86f798f..687db9812d61 100644 --- a/src/Illuminate/Bus/BusServiceProvider.php +++ b/src/Illuminate/Bus/BusServiceProvider.php @@ -33,7 +33,7 @@ public function register() ); $this->app->alias( - Dispatcher::class, QueueingDispatcherContract::class + DispatcherContract::class, QueueingDispatcherContract::class ); } From 8c6960875d9d8fd32f6a6144a177b7316225d9aa Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Fri, 27 Mar 2026 00:31:51 +1000 Subject: [PATCH 037/596] [13.x] Fix incrementEach/decrementEach to scope to model instance (#59376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [12.x] Fix incrementEach/decrementEach updating all rows instead of model instance Fixes laravel/framework#57262. Model-level: Adds incrementEach/decrementEach methods that scope to the model's primary key, fire updating/updated events, and sync in-memory attributes — mirroring the existing incrementOrDecrement pattern. Builder-level: Adds incrementEach/decrementEach methods that call addUpdatedAtColumn, ensuring updated_at is automatically set — consistent with how increment/decrement already behave. Co-Authored-By: sumaiazaman Co-Authored-By: Claude Opus 4.6 (1M context) * Handle class-castable columns and fix syncOriginal scope - Add isClassDeviable/deviateClassCastableAttribute handling per column, matching incrementOrDecrement behavior for Money/custom cast objects - Use syncOriginalAttributes(array_keys($columns)) instead of syncOriginal() to avoid marking unrelated dirty attributes as clean - Set attributes before event check, matching increment() behavior Co-Authored-By: Claude Opus 4.6 (1M context) * Add integration tests for incrementEach/decrementEach on model instances Tests against a real database to verify: - Instance call only affects that row (other rows unchanged) - decrementEach only affects that row - Query builder path still affects all matching rows - Timestamps are updated automatically - Soft-deleted models work (ignores global scopes) - Unrelated dirty attributes are preserved - Changes and previous values sync correctly Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: sumaiazaman Co-authored-by: Claude Opus 4.6 (1M context) --- src/Illuminate/Database/Eloquent/Builder.php | 28 ++++ src/Illuminate/Database/Eloquent/Model.php | 72 ++++++++++- .../Database/DatabaseEloquentBuilderTest.php | 61 +++++++++ tests/Database/DatabaseEloquentModelTest.php | 122 ++++++++++++++++++ .../Database/EloquentUpdateTest.php | 113 ++++++++++++++++ 5 files changed, 395 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index f13cf1a44e17..7a014287e757 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -1355,6 +1355,34 @@ public function decrement($column, $amount = 1, array $extra = []) ); } + /** + * Increment the given column's values by the given amounts. + * + * @param array $columns + * @param array $extra + * @return int + */ + public function incrementEach(array $columns, array $extra = []) + { + return $this->toBase()->incrementEach( + $columns, $this->addUpdatedAtColumn($extra) + ); + } + + /** + * Decrement the given column's values by the given amounts. + * + * @param array $columns + * @param array $extra + * @return int + */ + public function decrementEach(array $columns, array $extra = []) + { + return $this->toBase()->decrementEach( + $columns, $this->addUpdatedAtColumn($extra) + ); + } + /** * Add the "updated at" column to an array of values. * diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index e1df4268d405..33743b3204db 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1214,6 +1214,76 @@ protected function decrementQuietly($column, $amount = 1, array $extra = []) ); } + /** + * Increment each given column's value by the given amounts. + * + * @param array $columns + * @param array $extra + * @return int + */ + protected function incrementEach(array $columns, array $extra = []) + { + return $this->incrementOrDecrementEach($columns, $extra, 'incrementEach'); + } + + /** + * Decrement each given column's value by the given amounts. + * + * @param array $columns + * @param array $extra + * @return int + */ + protected function decrementEach(array $columns, array $extra = []) + { + return $this->incrementOrDecrementEach($columns, $extra, 'decrementEach'); + } + + /** + * Run the incrementEach or decrementEach method on the model. + * + * @param array $columns + * @param array $extra + * @param string $method + * @return int + */ + protected function incrementOrDecrementEach(array $columns, array $extra, string $method) + { + if (! $this->exists) { + return $this->newQueryWithoutRelationships()->{$method}($columns, $extra); + } + + $isIncrement = $method === 'incrementEach'; + $singleMethod = $isIncrement ? 'increment' : 'decrement'; + + foreach ($columns as $column => $amount) { + $this->{$column} = $this->isClassDeviable($column) + ? $this->deviateClassCastableAttribute($singleMethod, $column, $amount) + : $this->{$column} + ($isIncrement ? $amount : $amount * -1); + } + + $this->forceFill($extra); + + if ($this->fireModelEvent('updating') === false) { + return false; + } + + $dbColumns = $columns; + + foreach ($dbColumns as $column => $amount) { + if ($this->isClassDeviable($column)) { + $dbColumns[$column] = (clone $this)->setAttribute($column, $amount)->getAttributeFromArray($column); + } + } + + return tap($this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($dbColumns, $extra), function () use ($columns) { + $this->syncChanges(); + + $this->fireModelEvent('updated', false); + + $this->syncOriginalAttributes(array_keys($columns)); + }); + } + /** * Save the model and all of its relationships. * @@ -2707,7 +2777,7 @@ public function __unset($key) */ public function __call($method, $parameters) { - if (in_array($method, ['increment', 'decrement', 'incrementQuietly', 'decrementQuietly'])) { + if (in_array($method, ['increment', 'decrement', 'incrementQuietly', 'decrementQuietly', 'incrementEach', 'decrementEach'])) { return $this->$method(...$parameters); } diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 471a78d9a7f2..8b1709c0bd24 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -2934,6 +2934,67 @@ protected function getBuilder() return new Builder($this->getMockQueryBuilder()); } + public function testIncrementEachCallsToBaseWithUpdatedAt() + { + $query = m::mock(BaseBuilder::class); + $query->shouldReceive('from')->with('foo_table'); + $query->from = 'foo_table'; + $query->shouldReceive('incrementEach')->once()->withArgs(function ($columns, $extra) { + return $columns === ['votes' => 5] && array_key_exists('foo_table.updated_at', $extra); + })->andReturn(1); + + $builder = new Builder($query); + $model = $this->getMockModel(); + $model->shouldReceive('usesTimestamps')->andReturn(true); + $model->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at'); + $model->shouldReceive('freshTimestampString')->andReturn('2026-03-26 00:00:00'); + $model->shouldReceive('hasSetMutator')->andReturn(false); + $model->shouldReceive('hasAttributeSetMutator')->andReturn(false); + $model->shouldReceive('hasCast')->andReturn(false); + $builder->setModel($model); + + $result = $builder->incrementEach(['votes' => 5]); + $this->assertEquals(1, $result); + } + + public function testDecrementEachCallsToBaseWithUpdatedAt() + { + $query = m::mock(BaseBuilder::class); + $query->shouldReceive('from')->with('foo_table'); + $query->from = 'foo_table'; + $query->shouldReceive('decrementEach')->once()->withArgs(function ($columns, $extra) { + return $columns === ['votes' => 3] && array_key_exists('foo_table.updated_at', $extra); + })->andReturn(1); + + $builder = new Builder($query); + $model = $this->getMockModel(); + $model->shouldReceive('usesTimestamps')->andReturn(true); + $model->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at'); + $model->shouldReceive('freshTimestampString')->andReturn('2026-03-26 00:00:00'); + $model->shouldReceive('hasSetMutator')->andReturn(false); + $model->shouldReceive('hasAttributeSetMutator')->andReturn(false); + $model->shouldReceive('hasCast')->andReturn(false); + $builder->setModel($model); + + $result = $builder->decrementEach(['votes' => 3]); + $this->assertEquals(1, $result); + } + + public function testIncrementEachWithoutTimestamps() + { + $query = m::mock(BaseBuilder::class); + $query->shouldReceive('from')->with('foo_table'); + $query->shouldReceive('incrementEach')->once()->with(['votes' => 1], [])->andReturn(1); + + $builder = new Builder($query); + $model = $this->getMockModel(); + $model->shouldReceive('usesTimestamps')->andReturn(false); + $builder->setModel($model); + + $result = $builder->incrementEach(['votes' => 1]); + $this->assertEquals(1, $result); + } + protected function getMockModel() { $model = m::mock(Model::class); diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 60ecc816b4e3..2ccf8f74b4e9 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -2740,6 +2740,118 @@ public function testDecrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $this->assertTrue($model->isDirty('category')); } + public function testIncrementEachOnExistingModelScopesQueryToModelKey() + { + $model = m::mock(EloquentModelStub::class.'[newQueryWithoutScopes]'); + $model->exists = true; + $model->id = 1; + $model->syncOriginalAttribute('id'); + $model->foo = 2; + $model->bar = 5; + + $model->shouldReceive('newQueryWithoutScopes')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->once()->with('id', '=', 1)->andReturn($query); + $query->shouldReceive('incrementEach')->once()->with(['foo' => 1, 'bar' => 2], [])->andReturn(1); + + $result = $model->publicIncrementEach(['foo' => 1, 'bar' => 2]); + + $this->assertEquals(1, $result); + $this->assertEquals(3, $model->foo); + $this->assertEquals(7, $model->bar); + } + + public function testDecrementEachOnExistingModelScopesQueryToModelKey() + { + $model = m::mock(EloquentModelStub::class.'[newQueryWithoutScopes]'); + $model->exists = true; + $model->id = 1; + $model->syncOriginalAttribute('id'); + $model->foo = 10; + $model->bar = 5; + + $model->shouldReceive('newQueryWithoutScopes')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->once()->with('id', '=', 1)->andReturn($query); + $query->shouldReceive('decrementEach')->once()->with(['foo' => 3, 'bar' => 2], [])->andReturn(1); + + $result = $model->publicDecrementEach(['foo' => 3, 'bar' => 2]); + + $this->assertEquals(1, $result); + $this->assertEquals(7, $model->foo); + $this->assertEquals(3, $model->bar); + } + + public function testIncrementEachWithExtraColumnsOnExistingModel() + { + $model = m::mock(EloquentModelStub::class.'[newQueryWithoutScopes]'); + $model->exists = true; + $model->id = 1; + $model->syncOriginalAttribute('id'); + $model->foo = 2; + + $model->shouldReceive('newQueryWithoutScopes')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->once()->with('id', '=', 1)->andReturn($query); + $query->shouldReceive('incrementEach')->once()->with(['foo' => 5], ['category' => 'test'])->andReturn(1); + + $result = $model->publicIncrementEach(['foo' => 5], ['category' => 'test']); + + $this->assertEquals(1, $result); + $this->assertEquals(7, $model->foo); + $this->assertEquals('test', $model->category); + } + + public function testIncrementEachFiresModelEvents() + { + $model = m::mock(EloquentModelStub::class.'[newQueryWithoutScopes]'); + $model->exists = true; + $model->id = 1; + $model->syncOriginalAttribute('id'); + $model->foo = 1; + + $model->shouldReceive('newQueryWithoutScopes')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->andReturn($query); + $query->shouldReceive('incrementEach')->andReturn(1); + + $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(true); + $events->shouldReceive('dispatch')->once()->with('eloquent.updated: '.get_class($model), $model); + + $model->publicIncrementEach(['foo' => 1]); + } + + public function testIncrementEachReturnsFalseWhenUpdatingEventCancelled() + { + $model = m::mock(EloquentModelStub::class.'[newQueryWithoutScopes]'); + $model->exists = true; + $model->id = 1; + $model->syncOriginalAttribute('id'); + $model->foo = 1; + + $model->shouldReceive('newQueryWithoutScopes')->never(); + + $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(false); + + $result = $model->publicIncrementEach(['foo' => 1]); + + $this->assertFalse($result); + // Note: attributes are set before the event fires, matching increment() behavior. + // The in-memory value changes but the database is not updated. + $this->assertEquals(2, $model->foo); + } + + public function testIncrementEachOnNonExistingModelForwardsToQueryBuilder() + { + $model = m::mock(EloquentModelStub::class.'[newQueryWithoutRelationships]'); + $model->exists = false; + + $model->shouldReceive('newQueryWithoutRelationships')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('incrementEach')->once()->with(['foo' => 1], [])->andReturn(5); + + $result = $model->publicIncrementEach(['foo' => 1]); + + $this->assertEquals(5, $result); + } + public function testRelationshipTouchOwnersIsPropagated() { $relation = $this->getMockBuilder(BelongsTo::class)->onlyMethods(['touch'])->disableOriginalConstructor()->getMock(); @@ -3803,6 +3915,16 @@ public function publicDecrementQuietly($column, $amount = 1, $extra = []) return $this->decrementQuietly($column, $amount, $extra); } + public function publicIncrementEach(array $columns, array $extra = []) + { + return $this->incrementEach($columns, $extra); + } + + public function publicDecrementEach(array $columns, array $extra = []) + { + return $this->decrementEach($columns, $extra); + } + public function belongsToStub() { return $this->belongsTo(EloquentModelSaveStub::class); diff --git a/tests/Integration/Database/EloquentUpdateTest.php b/tests/Integration/Database/EloquentUpdateTest.php index 829091686abf..55b5934e15dc 100644 --- a/tests/Integration/Database/EloquentUpdateTest.php +++ b/tests/Integration/Database/EloquentUpdateTest.php @@ -32,6 +32,15 @@ protected function afterRefreshingDatabase() $table->softDeletes(); $table->timestamps(); }); + + Schema::create('test_model4', function (Blueprint $table) { + $table->increments('id'); + $table->integer('views')->default(0); + $table->integer('likes')->default(0); + $table->string('name')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); } public function testBasicUpdate() @@ -180,6 +189,101 @@ public function testIncrementSyncsPrevious() $this->assertSame(['counter' => 1], $model->getChanges()); $this->assertSame(['counter' => 0], $model->getPrevious()); } + + public function testIncrementEachOnModelInstanceOnlyAffectsThatRow() + { + $post1 = TestUpdateModel4::create(['views' => 10, 'likes' => 5]); + $post2 = TestUpdateModel4::create(['views' => 50, 'likes' => 20]); + $post3 = TestUpdateModel4::create(['views' => 100, 'likes' => 40]); + + $post1->incrementEach(['views' => 1, 'likes' => 2]); + + $this->assertEquals(11, $post1->views); + $this->assertEquals(7, $post1->likes); + + $this->assertEquals(50, $post2->fresh()->views); + $this->assertEquals(20, $post2->fresh()->likes); + $this->assertEquals(100, $post3->fresh()->views); + $this->assertEquals(40, $post3->fresh()->likes); + } + + public function testDecrementEachOnModelInstanceOnlyAffectsThatRow() + { + $post1 = TestUpdateModel4::create(['views' => 10, 'likes' => 5]); + $post2 = TestUpdateModel4::create(['views' => 50, 'likes' => 20]); + + $post1->decrementEach(['views' => 3, 'likes' => 2]); + + $this->assertEquals(7, $post1->views); + $this->assertEquals(3, $post1->likes); + + $this->assertEquals(50, $post2->fresh()->views); + $this->assertEquals(20, $post2->fresh()->likes); + } + + public function testIncrementEachViaQueryBuilderStillAffectsAllMatchingRows() + { + TestUpdateModel4::create(['views' => 10, 'likes' => 5]); + TestUpdateModel4::create(['views' => 50, 'likes' => 20]); + + TestUpdateModel4::incrementEach(['views' => 1]); + + $models = TestUpdateModel4::orderBy('id')->get(); + $this->assertEquals(11, $models[0]->views); + $this->assertEquals(51, $models[1]->views); + } + + public function testIncrementEachOnModelInstanceUpdatesTimestamps() + { + $post = TestUpdateModel4::create(['views' => 0, 'likes' => 0]); + $originalUpdatedAt = $post->updated_at; + + $this->travel(5)->minutes(); + + $post->incrementEach(['views' => 1]); + + $this->assertNotEquals($originalUpdatedAt, $post->fresh()->updated_at); + } + + public function testIncrementEachOnSoftDeletedModelIgnoresGlobalScopes() + { + $post = tap(TestUpdateModel4::create([ + 'views' => 10, 'likes' => 5, + ]), fn ($model) => $model->delete()); + + $post->incrementEach(['views' => 1, 'likes' => 1]); + + $this->assertEquals(11, $post->views); + $this->assertEquals(6, $post->likes); + + $fresh = TestUpdateModel4::withTrashed()->find($post->id); + $this->assertEquals(11, $fresh->views); + $this->assertEquals(6, $fresh->likes); + } + + public function testIncrementEachDoesNotResetUnrelatedDirtyAttributes() + { + $post = TestUpdateModel4::create(['views' => 10, 'likes' => 5, 'name' => 'Original']); + + $post->name = 'Changed'; + $post->incrementEach(['views' => 1]); + + $this->assertTrue($post->isDirty('name')); + $this->assertEquals('Changed', $post->name); + $this->assertFalse($post->isDirty('views')); + } + + public function testIncrementEachSyncsPrevious() + { + $post = TestUpdateModel4::create(['views' => 10, 'likes' => 5]); + + $post->incrementEach(['views' => 1, 'likes' => 2]); + + $this->assertEquals(11, $post->views); + $this->assertEquals(7, $post->likes); + $this->assertArrayHasKey('views', $post->getChanges()); + $this->assertArrayHasKey('likes', $post->getChanges()); + } } class TestUpdateModel1 extends Model @@ -205,3 +309,12 @@ class TestUpdateModel3 extends Model protected $fillable = ['counter']; protected $casts = ['deleted_at' => 'datetime']; } + +class TestUpdateModel4 extends Model +{ + use SoftDeletes; + + public $table = 'test_model4'; + protected $fillable = ['views', 'likes', 'name']; + protected $casts = ['deleted_at' => 'datetime']; +} From ecd254f501eb3d51cbb2afa01db51466aa99d432 Mon Sep 17 00:00:00 2001 From: Mr PHP <102259475+Anthony14FR@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:49:57 +0100 Subject: [PATCH 038/596] Add array value types to Support module docblocks (#59383) --- src/Illuminate/Support/Benchmark.php | 6 ++--- src/Illuminate/Support/BinaryCodec.php | 2 +- src/Illuminate/Support/Composer.php | 10 ++++----- .../Support/ConfigurationUrlParser.php | 22 +++++++++---------- src/Illuminate/Support/DefaultProviders.php | 12 +++++----- src/Illuminate/Support/Env.php | 8 +++---- src/Illuminate/Support/Fluent.php | 2 +- src/Illuminate/Support/Manager.php | 8 +++---- src/Illuminate/Support/MessageBag.php | 12 +++++----- src/Illuminate/Support/Number.php | 2 +- 10 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/Illuminate/Support/Benchmark.php b/src/Illuminate/Support/Benchmark.php index 0204c9746fc0..da9bcd855afa 100644 --- a/src/Illuminate/Support/Benchmark.php +++ b/src/Illuminate/Support/Benchmark.php @@ -12,9 +12,9 @@ class Benchmark /** * Measure a callable or array of callables over the given number of iterations. * - * @param \Closure|array $benchmarkables + * @param \Closure|array $benchmarkables * @param int $iterations - * @return array|float + * @return array|float */ public static function measure(Closure|array $benchmarkables, int $iterations = 1): array|float { @@ -57,7 +57,7 @@ public static function value(callable $callback): array /** * Measure a callable or array of callables over the given number of iterations, then dump and die. * - * @param \Closure|array $benchmarkables + * @param \Closure|array $benchmarkables * @param int $iterations * @return never */ diff --git a/src/Illuminate/Support/BinaryCodec.php b/src/Illuminate/Support/BinaryCodec.php index cf51fbea64fa..81c94299f698 100644 --- a/src/Illuminate/Support/BinaryCodec.php +++ b/src/Illuminate/Support/BinaryCodec.php @@ -78,7 +78,7 @@ public static function decode(?string $value, string $format): ?string /** * Get all available format names. * - * @return list + * @return array */ public static function formats(): array { diff --git a/src/Illuminate/Support/Composer.php b/src/Illuminate/Support/Composer.php index 4f9ddaa6a053..3b55ee6ed487 100644 --- a/src/Illuminate/Support/Composer.php +++ b/src/Illuminate/Support/Composer.php @@ -113,7 +113,7 @@ public function removePackages(array $packages, bool $dev = false, Closure|Outpu /** * Modify the "composer.json" file contents using the given callback. * - * @param callable(array):array $callback + * @param callable(array):array $callback * @return void * * @throws \RuntimeException @@ -136,7 +136,7 @@ public function modify(callable $callback) /** * Regenerate the Composer autoloader files. * - * @param string|array $extra + * @param string|array $extra * @param string|null $composerBinary * @return int */ @@ -164,7 +164,7 @@ public function dumpOptimized($composerBinary = null) * Get the Composer binary / command for the environment. * * @param string|null $composerBinary - * @return array + * @return array */ public function findComposer($composerBinary = null) { @@ -208,8 +208,8 @@ protected function phpBinary() /** * Get a new Symfony process instance. * - * @param array $command - * @param array $env + * @param array $command + * @param array $env * @return \Symfony\Component\Process\Process */ protected function getProcess(array $command, array $env = []) diff --git a/src/Illuminate/Support/ConfigurationUrlParser.php b/src/Illuminate/Support/ConfigurationUrlParser.php index b841b65e41c6..a5b083f72f9c 100644 --- a/src/Illuminate/Support/ConfigurationUrlParser.php +++ b/src/Illuminate/Support/ConfigurationUrlParser.php @@ -9,7 +9,7 @@ class ConfigurationUrlParser /** * The drivers aliases map. * - * @var array + * @var array */ protected static $driverAliases = [ 'mssql' => 'sqlsrv', @@ -24,8 +24,8 @@ class ConfigurationUrlParser /** * Parse the database configuration, hydrating options using a database configuration URL if possible. * - * @param array|string $config - * @return array + * @param array|string $config + * @return array */ public function parseConfiguration($config) { @@ -55,8 +55,8 @@ public function parseConfiguration($config) /** * Get the primary database connection options. * - * @param array $url - * @return array + * @param array $url + * @return array */ protected function getPrimaryOptions($url) { @@ -73,7 +73,7 @@ protected function getPrimaryOptions($url) /** * Get the database driver from the URL. * - * @param array $url + * @param array $url * @return string|null */ protected function getDriver($url) @@ -90,7 +90,7 @@ protected function getDriver($url) /** * Get the database name from the URL. * - * @param array $url + * @param array $url * @return string|null */ protected function getDatabase($url) @@ -103,8 +103,8 @@ protected function getDatabase($url) /** * Get all of the additional database options from the query string. * - * @param array $url - * @return array + * @param array $url + * @return array */ protected function getQueryOptions($url) { @@ -125,7 +125,7 @@ protected function getQueryOptions($url) * Parse the string URL to an array of components. * * @param string $url - * @return array + * @return array * * @throws \InvalidArgumentException */ @@ -170,7 +170,7 @@ protected function parseStringsToNativeTypes($value) /** * Get all of the current drivers' aliases. * - * @return array + * @return array */ public static function getDriverAliases() { diff --git a/src/Illuminate/Support/DefaultProviders.php b/src/Illuminate/Support/DefaultProviders.php index 6430e8439f54..2d6e4acf3910 100644 --- a/src/Illuminate/Support/DefaultProviders.php +++ b/src/Illuminate/Support/DefaultProviders.php @@ -7,12 +7,14 @@ class DefaultProviders /** * The current providers. * - * @var array + * @var array */ protected $providers; /** * Create a new default provider collection. + * + * @param array|null $providers */ public function __construct(?array $providers = null) { @@ -46,7 +48,7 @@ public function __construct(?array $providers = null) /** * Merge the given providers into the provider collection. * - * @param array $providers + * @param array $providers * @return static */ public function merge(array $providers) @@ -59,7 +61,7 @@ public function merge(array $providers) /** * Replace the given providers with other providers. * - * @param array $replacements + * @param array $replacements * @return static */ public function replace(array $replacements) @@ -78,7 +80,7 @@ public function replace(array $replacements) /** * Disable the given providers. * - * @param array $providers + * @param array $providers * @return static */ public function except(array $providers) @@ -92,7 +94,7 @@ public function except(array $providers) /** * Convert the provider collection to an array. * - * @return array + * @return array */ public function toArray() { diff --git a/src/Illuminate/Support/Env.php b/src/Illuminate/Support/Env.php index 97fcb4637966..24eb1a0db7fa 100644 --- a/src/Illuminate/Support/Env.php +++ b/src/Illuminate/Support/Env.php @@ -120,7 +120,7 @@ public static function getOrFail($key) /** * Write an array of key-value pairs to the environment file. * - * @param array $variables + * @param array $variables * @param string $pathToFile * @param bool $overwrite * @return void @@ -178,9 +178,9 @@ public static function writeVariable(string $key, mixed $value, string $pathToFi * * @param string $key * @param mixed $value - * @param array $envLines + * @param array $envLines * @param bool $overwrite - * @return array + * @return array */ protected static function addVariableToEnvContents(string $key, mixed $value, array $envLines, bool $overwrite): array { @@ -293,7 +293,7 @@ protected static function prepareQuotedValue(string $input) * Escape a string using addslashes, excluding the specified characters from being escaped. * * @param string $value - * @param array $except + * @param array $except * @return string */ protected static function addSlashesExceptFor(string $value, array $except = []) diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index a15085727086..15ba52b236da 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -131,7 +131,7 @@ public function scope($key, $default = null) * Get all of the attributes from the fluent instance. * * @param mixed $keys - * @return array + * @return array */ public function all($keys = null) { diff --git a/src/Illuminate/Support/Manager.php b/src/Illuminate/Support/Manager.php index 83cd54328a42..677857d225ef 100755 --- a/src/Illuminate/Support/Manager.php +++ b/src/Illuminate/Support/Manager.php @@ -25,14 +25,14 @@ abstract class Manager /** * The registered custom driver creators. * - * @var array + * @var array */ protected $customCreators = []; /** * The array of created "drivers". * - * @var array + * @var array */ protected $drivers = []; @@ -135,7 +135,7 @@ public function extend($driver, Closure $callback) /** * Get all of the created "drivers". * - * @return array + * @return array */ public function getDrivers() { @@ -181,7 +181,7 @@ public function forgetDrivers() * Dynamically call the default driver instance. * * @param string $method - * @param array $parameters + * @param array $parameters * @return mixed */ public function __call($method, $parameters) diff --git a/src/Illuminate/Support/MessageBag.php b/src/Illuminate/Support/MessageBag.php index 0868ff352a2c..069534d4f602 100755 --- a/src/Illuminate/Support/MessageBag.php +++ b/src/Illuminate/Support/MessageBag.php @@ -112,7 +112,7 @@ public function merge($messages) /** * Determine if messages exist for all of the given keys. * - * @param array|string|null $key + * @param array|string|null $key * @return bool */ public function has($key) @@ -139,7 +139,7 @@ public function has($key) /** * Determine if messages exist for any of the given keys. * - * @param array|string|null $keys + * @param array|string|null $keys * @return bool */ public function hasAny($keys = []) @@ -162,7 +162,7 @@ public function hasAny($keys = []) /** * Determine if messages don't exist for all of the given keys. * - * @param array|string|null $key + * @param array|string|null $key * @return bool */ public function missing($key) @@ -253,7 +253,7 @@ public function all($format = null) * Get all of the unique messages for every key in the message bag. * * @param string|null $format - * @return array + * @return array */ public function unique($format = null) { @@ -403,7 +403,7 @@ public function count(): int /** * Get the instance as an array. * - * @return array + * @return array> */ public function toArray() { @@ -413,7 +413,7 @@ public function toArray() /** * Convert the object into something JSON serializable. * - * @return array + * @return array> */ public function jsonSerialize(): array { diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 9e7918d1e8ff..f07221855442 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -261,7 +261,7 @@ public static function forHumans(int|float $number, int $precision = 0, ?int $ma * @param int|float $number * @param int $precision * @param int|null $maxPrecision - * @param array $units + * @param array $units * @return string|false */ protected static function summarize(int|float $number, int $precision = 0, ?int $maxPrecision = null, array $units = []) From dac16d424b59debb2273910dde88eb7050a2a709 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:51:54 +0000 Subject: [PATCH 039/596] Update version to v12.56.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 30aef88fb12c..113073116981 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.55.1'; + const VERSION = '12.56.0'; /** * The base path for the Laravel installation. From 722a8e5d5cba665f7c3c3acd76928d72fe505763 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:54:05 +0000 Subject: [PATCH 040/596] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 097c18a3c76f..e279ac75be19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.55.1...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.56.0...12.x) + +## [v12.56.0](https://github.com/laravel/framework/compare/v12.55.1...v12.56.0) - 2026-03-26 + +* [12.x] `schedule:list` display expression in the correct timezone by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/59307 +* [12.x] Fix validation wildcard array message type error by [@sadique-cws](https://github.com/sadique-cws) in https://github.com/laravel/framework/pull/59339 +* Preserve class type of mocked classes by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59353 ## [v12.55.1](https://github.com/laravel/framework/compare/v12.55.0...v12.55.1) - 2026-03-18 From 958a6ecc613a6c886659711aacc37ebd3ade522c Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Thu, 26 Mar 2026 17:57:09 +0100 Subject: [PATCH 041/596] Preserve types on partialMock() and spy() (#59384) --- .../Testing/Concerns/InteractsWithContainer.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index b568d1845636..519f23bfcfab 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -80,9 +80,11 @@ protected function mock($abstract, ?Closure $mock = null) /** * Mock a partial instance of an object in the container. * - * @param string $abstract + * @template TInstance of object + * + * @param class-string $abstract * @param \Closure|null $mock - * @return \Mockery\MockInterface + * @return TInstance&\Mockery\MockInterface */ protected function partialMock($abstract, ?Closure $mock = null) { @@ -92,9 +94,11 @@ protected function partialMock($abstract, ?Closure $mock = null) /** * Spy an instance of an object in the container. * - * @param string $abstract + * @template TInstance of object + * + * @param class-string $abstract * @param \Closure|null $mock - * @return \Mockery\MockInterface + * @return TInstance&\Mockery\MockInterface */ protected function spy($abstract, ?Closure $mock = null) { From 8f9f9808854e7c56b6085b283fa452956cf04351 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 26 Mar 2026 17:01:22 +0000 Subject: [PATCH 042/596] [13.x] Add lost connection to WorkerStopReason (#59370) * 13.x-ensure-lost-connections-report-properly * im truly stupid paid fr --- src/Illuminate/Queue/Worker.php | 10 +++++++++- src/Illuminate/Queue/WorkerStopReason.php | 1 + tests/Queue/QueueWorkerTest.php | 23 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 88fd17e72dd9..a39cc2a7cd07 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -85,6 +85,13 @@ class Worker */ public $shouldQuit = false; + /** + * Indicates if the worker lost its connection. + * + * @var bool + */ + public $lostConnection = false; + /** * Indicates if the worker is paused. * @@ -334,6 +341,7 @@ protected function pauseWorker(WorkerOptions $options, $lastRestart) protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $job = null) { return match (true) { + $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], $this->shouldQuit => [static::EXIT_SUCCESS, WorkerStopReason::Interrupted], $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], @@ -458,7 +466,7 @@ protected function runJob($job, $connectionName, WorkerOptions $options) protected function stopWorkerIfLostConnection($e) { if ($this->causedByLostConnection($e)) { - $this->shouldQuit = true; + $this->lostConnection = true; } } diff --git a/src/Illuminate/Queue/WorkerStopReason.php b/src/Illuminate/Queue/WorkerStopReason.php index 52b7cb8ff2f6..8591e94743bc 100644 --- a/src/Illuminate/Queue/WorkerStopReason.php +++ b/src/Illuminate/Queue/WorkerStopReason.php @@ -5,6 +5,7 @@ enum WorkerStopReason: string { case Interrupted = 'interrupted'; + case LostConnection = 'lost_connection'; case MaxJobsExceeded = 'max_jobs'; case MaxMemoryExceeded = 'memory'; case MaxTimeExceeded = 'max_time'; diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index b196db433657..9c666ed25e2a 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -465,6 +465,29 @@ public function testWorkerStoppingIsDispatched() })); } + public function testWorkerStopsWithLostConnectionReason() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmpty = true; + + $worker = $this->getWorker('default', ['queue' => [ + $job = new WorkerFakeJob(function () { + throw new RuntimeException('server has gone away'); + }), + ]]); + + $worker->daemon('default', 'queue', $workerOptions); + + $this->assertTrue($job->fired); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::LostConnection; + })); + } + public function testJobReleasedEvent() { $e = new RuntimeException; From b2dcd15f344911752dec879d4129007e25d1bf4d Mon Sep 17 00:00:00 2001 From: Kyle Milloy Date: Thu, 26 Mar 2026 11:03:45 -0600 Subject: [PATCH 043/596] [13.x] MariaDbSchemaState uses mysql --version for client detection instead of mariadb --version (#59360) * fix: use mariadb command to resolve client version * chore: lint * Update minimum MariaDB version to 10.5.2 * formatting --------- Co-authored-by: Taylor Otwell --- .../Database/Schema/MariaDbSchemaState.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Illuminate/Database/Schema/MariaDbSchemaState.php b/src/Illuminate/Database/Schema/MariaDbSchemaState.php index af56d8eadae4..25c1d3c61d1c 100644 --- a/src/Illuminate/Database/Schema/MariaDbSchemaState.php +++ b/src/Illuminate/Database/Schema/MariaDbSchemaState.php @@ -2,6 +2,8 @@ namespace Illuminate\Database\Schema; +use Symfony\Component\Process\Exception\ProcessFailedException; + class MariaDbSchemaState extends MySqlSchemaState { /** @@ -36,4 +38,29 @@ protected function baseDumpCommand() return $command.' "${:LARAVEL_LOAD_DATABASE}"'; } + + /** + * Detect the MariaDB client version. + * + * @return array{version: string, isMariaDb: bool} + */ + protected function detectClientVersion(): array + { + // Minimum version of MariaDB that supports the mariadb command... + $version = '10.5.2'; + + try { + $versionOutput = $this->makeProcess('mariadb --version')->mustRun()->getOutput(); + + if (preg_match('/(\d+\.\d+\.\d+)/', $versionOutput, $matches)) { + $version = $matches[1]; + } + } catch (ProcessFailedException) { + } + + return [ + 'isMariaDb' => true, + 'version' => $version, + ]; + } } From 45154000d2967a14e6f679ae2c8c0af37a8cdaab Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:13:01 +0600 Subject: [PATCH 044/596] [13.x] Add enum support to QueueManager connection methods (#59389) Align QueueManager with DatabaseManager, FilesystemManager, and RedisManager which already accept enums for connection names via enum_value(). --- src/Illuminate/Contracts/Queue/Factory.php | 2 +- src/Illuminate/Queue/QueueManager.php | 10 ++-- tests/Queue/QueueManagerTest.php | 53 ++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Contracts/Queue/Factory.php b/src/Illuminate/Contracts/Queue/Factory.php index 9a0bdeb12577..606f803ded15 100644 --- a/src/Illuminate/Contracts/Queue/Factory.php +++ b/src/Illuminate/Contracts/Queue/Factory.php @@ -7,7 +7,7 @@ interface Factory /** * Resolve a queue connection instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Queue\Queue */ public function connection($name = null); diff --git a/src/Illuminate/Queue/QueueManager.php b/src/Illuminate/Queue/QueueManager.php index 46353a4d68c6..3a65246b5471 100755 --- a/src/Illuminate/Queue/QueueManager.php +++ b/src/Illuminate/Queue/QueueManager.php @@ -8,6 +8,8 @@ use Illuminate\Support\Queue\Concerns\ResolvesQueueRoutes; use InvalidArgumentException; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Contracts\Queue\Queue */ @@ -139,23 +141,23 @@ public function route(array|string $class, $queue = null, $connection = null) /** * Determine if the driver is connected. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return bool */ public function connected($name = null) { - return isset($this->connections[$name ?: $this->getDefaultDriver()]); + return isset($this->connections[enum_value($name) ?: $this->getDefaultDriver()]); } /** * Resolve a queue connection instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Queue\Queue */ public function connection($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); // If the connection has not been resolved yet we will resolve it now as all // of the connections are resolved when they are actually needed so we do diff --git a/tests/Queue/QueueManagerTest.php b/tests/Queue/QueueManagerTest.php index b8e80d835ac2..639719852a49 100755 --- a/tests/Queue/QueueManagerTest.php +++ b/tests/Queue/QueueManagerTest.php @@ -77,4 +77,57 @@ public function testNullConnectionCanBeResolved() $this->assertSame($queue, $manager->connection('null')); } + + public function testEnumConnectionCanBeResolved() + { + $app = [ + 'config' => [ + 'queue.default' => 'sync', + 'queue.connections.sync' => ['driver' => 'sync'], + ], + 'encrypter' => $encrypter = m::mock(Encrypter::class), + ]; + + $manager = new QueueManager($app); + $connector = m::mock(stdClass::class); + $queue = m::mock(stdClass::class); + $queue->shouldReceive('setConnectionName')->once()->with('sync')->andReturnSelf(); + $connector->shouldReceive('connect')->once()->with(['driver' => 'sync'])->andReturn($queue); + $manager->addConnector('sync', function () use ($connector) { + return $connector; + }); + $queue->shouldReceive('setContainer')->once()->with($app); + + $this->assertSame($queue, $manager->connection(QueueConnectionName::Sync)); + } + + public function testEnumConnectionCanBeChecked() + { + $app = [ + 'config' => [ + 'queue.default' => 'sync', + 'queue.connections.sync' => ['driver' => 'sync'], + ], + 'encrypter' => $encrypter = m::mock(Encrypter::class), + ]; + + $manager = new QueueManager($app); + $connector = m::mock(stdClass::class); + $queue = m::mock(stdClass::class); + $queue->shouldReceive('setConnectionName')->once()->with('sync')->andReturnSelf(); + $connector->shouldReceive('connect')->once()->with(['driver' => 'sync'])->andReturn($queue); + $manager->addConnector('sync', function () use ($connector) { + return $connector; + }); + $queue->shouldReceive('setContainer')->once()->with($app); + + $this->assertFalse($manager->connected(QueueConnectionName::Sync)); + $manager->connection(QueueConnectionName::Sync); + $this->assertTrue($manager->connected(QueueConnectionName::Sync)); + } +} + +enum QueueConnectionName: string +{ + case Sync = 'sync'; } From 439adc0d8e601b62fb15ebb14422173ce32ea814 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:13:33 +0000 Subject: [PATCH 045/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 83a8bd0e629d..e590560be189 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -14,8 +14,8 @@ * @method static void starting(mixed $callback) * @method static void stopping(mixed $callback) * @method static void route(array|string $class, string|null $queue = null, string|null $connection = null) - * @method static bool connected(string|null $name = null) - * @method static \Illuminate\Contracts\Queue\Queue connection(string|null $name = null) + * @method static bool connected(\UnitEnum|string|null $name = null) + * @method static \Illuminate\Contracts\Queue\Queue connection(\UnitEnum|string|null $name = null) * @method static void pause(string $connection, string $queue) * @method static void pauseFor(string $connection, string $queue, \DateTimeInterface|\DateInterval|int $ttl) * @method static void resume(string $connection, string $queue) From cfc86927912f863e1ea02371e529d11c392c8dd0 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Thu, 26 Mar 2026 18:14:47 +0100 Subject: [PATCH 046/596] Setup rector (#59385) Co-authored-by: Lucas Michot --- .gitattributes | 1 + composer.json | 1 + rector.php | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 rector.php diff --git a/.gitattributes b/.gitattributes index 8382fc5c826f..644702b6c1ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,4 +24,5 @@ docker-compose.yml export-ignore phpstan.src.neon.dist export-ignore phpstan.types.neon.dist export-ignore phpunit.xml.dist export-ignore +rector.php export-ignore RELEASE.md export-ignore diff --git a/composer.json b/composer.json index 27b7526184c4..030f911c01dc 100644 --- a/composer.json +++ b/composer.json @@ -85,6 +85,7 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", "predis/predis": "^2.3 || ^3.0", + "rector/rector": "^2.3", "resend/resend-php": "^1.0", "symfony/cache": "^7.4.0 || ^8.0.0", "symfony/http-client": "^7.4.0 || ^8.0.0", diff --git a/rector.php b/rector.php new file mode 100644 index 000000000000..6c88c6a52c53 --- /dev/null +++ b/rector.php @@ -0,0 +1,95 @@ +withRootFiles() + ->withPaths([ + __DIR__.'/config', + __DIR__.'/src', + __DIR__.'/tests', + __DIR__.'/types', + ]) + ->withSkip([ + AddOverrideAttributeToOverriddenMethodsRector::class, + AddTypeToConstRector::class, + ArrayToFirstClassCallableRector::class, + ArrowFunctionDelegatingCallToFirstClassCallableRector::class, + BinaryOpBetweenNumberAndStringRector::class, + ChangeSwitchToMatchRector::class, + ClassConstantToSelfClassRector::class, + ClassOnObjectRector::class, + ClassOnThisVariableObjectRector::class, + ClassPropertyAssignToConstructorPromotionRector::class, + ClosureDelegatingCallToFirstClassCallableRector::class, + ClosureFromCallableToFirstClassCallableRector::class, + ClosureToArrowFunctionRector::class, + ConsistentImplodeRector::class, + DynamicClassConstFetchRector::class, + FunctionFirstClassCallableRector::class, + GetDebugTypeRector::class, + IfIssetToCoalescingRector::class, + IfToSpaceshipRector::class, + NullCoalescingOperatorRector::class, + NullToStrictStringFuncCallArgRector::class, + PowToExpRector::class, + RandomFunctionRector::class, + ReadOnlyClassRector::class, + ReadOnlyPropertyRector::class, + RemoveExtraParametersRector::class, + RemoveUnusedVariableInCatchRector::class, + ReturnNeverTypeRector::class, + StaticCallOnNonStaticToInstanceCallRector::class, + StringClassNameToClassConstantRector::class, + StringableForToStringRector::class, + TernaryToNullCoalescingRector::class, + ThisCallOnStaticMethodToStaticCallRector::class, + 'tests/Foundation/fixtures/bad-syntax-strategy.php', + ]) + ->withPreparedSets( + deadCode: false, + codeQuality: false, + codingStyle: false, + typeDeclarations: false, + typeDeclarationDocblocks: false, + privatization: false, + naming: false, + instanceOf: false, + earlyReturn: false, + ) + ->withPhpSets(php83: true); From 83956d89d4a856851b6a1c7d062c830229627794 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:15:41 +0100 Subject: [PATCH 047/596] Improve `Arr::whereNotNull()` docs (#59411) --- src/Illuminate/Collections/Arr.php | 7 +++++-- types/Support/Arr.php | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index d3e4d89e7147..f5b8bfe1500f 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -1284,8 +1284,11 @@ public static function partition($array, callable $callback) /** * Filter items where the value is not null. * - * @param array $array - * @return array + * @template TKey of array-key + * @template TValue + * + * @param array $array + * @return array */ public static function whereNotNull($array) { diff --git a/types/Support/Arr.php b/types/Support/Arr.php index 64e6e004a6f3..38e15791e9dc 100644 --- a/types/Support/Arr.php +++ b/types/Support/Arr.php @@ -196,3 +196,9 @@ public function jsonSerialize(): mixed assertType('array>', Arr::wrap($value)); /** @var stdClass|stdClass[]|null $value */ assertType('array', Arr::wrap($value)); + +/** @var array $arr */ +assertType('array', Arr::whereNotNull($arr)); + +/** @var list $arr */ +assertType('array, int>', Arr::whereNotNull($arr)); From 5f9a8c7ce1ba075324b3f91ffdef505aaa522e12 Mon Sep 17 00:00:00 2001 From: Bilfeldt Date: Fri, 27 Mar 2026 18:16:34 +0100 Subject: [PATCH 048/596] [13.x] Pass request to afterResponse callback (#59410) * [13.x] Pass request to afterResponse callback Co-Authored-By: Claude Opus 4.6 (1M context) * Fix parameter names in afterResponse callback --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/Illuminate/Http/Client/PendingRequest.php | 6 +++--- tests/Http/HttpClientTest.php | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index d86d6e407ea3..a6722db7d9ad 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -168,7 +168,7 @@ class PendingRequest /** * The callbacks that should execute after the Laravel Response is built. * - * @var \Illuminate\Support\Collection + * @var \Illuminate\Support\Collection */ protected $afterResponseCallbacks; @@ -750,7 +750,7 @@ public function beforeSending($callback) /** * Add a new callback to execute after the response is built. * - * @param (callable(\Illuminate\Http\Client\Response): \Illuminate\Http\Client\Response|null) $callback + * @param (callable(\Illuminate\Http\Client\Response, \Illuminate\Http\Client\Request): \Illuminate\Http\Client\Response|null) $callback * @return $this */ public function afterResponse(callable $callback) @@ -1629,7 +1629,7 @@ protected function newResponse($response) protected function runAfterResponseCallbacks(Response $response) { foreach ($this->afterResponseCallbacks as $callback) { - $returnedResponse = $callback($response); + $returnedResponse = $callback($response, $this->request); if ($returnedResponse instanceof Response) { $response = $returnedResponse; diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 7086b321e8f8..409a186188cc 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -4361,8 +4361,10 @@ public function testAfterResponse() $response = $this->factory ->afterResponse(fn (Response $response): TestResponse => new TestResponse($response->toPsrResponse())) ->afterResponse(fn () => 'abc') - ->afterResponse(function ($r) { - $this->assertInstanceOf(TestResponse::class, $r); + ->afterResponse(function ($response, $request) { + $this->assertInstanceOf(TestResponse::class, $response); + $this->assertInstanceOf(Request::class, $request); + $this->assertSame('http://200.com', (string) $request->url()); }) ->afterResponse(fn (Response $r) => new Response($r->toPsrResponse()->withBody(Utils::streamFor(strtolower($r->body()))))) ->get('http://200.com'); @@ -4393,13 +4395,21 @@ public function testAfterResponseWithAsync() 'http://401.com*' => $this->factory::response('Unauthorized.', 401), ]); - $o = $this->factory->pool(function (Pool $pool): void { - $pool->as('200')->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()))->get('http://200.com'); + $requestReceived = null; + + $o = $this->factory->pool(function (Pool $pool) use (&$requestReceived): void { + $pool->as('200')->afterResponse(function (Response $response, Request $request) use (&$requestReceived) { + $requestReceived = $request; + + return new TestResponse($response->toPsrResponse()); + })->get('http://200.com'); $pool->as('401-throwing')->throw()->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()))->get('http://401.com'); $pool->as('401-response')->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()->withBody(Utils::streamFor('different'))))->get('http://401.com'); }, 0); $this->assertInstanceOf(TestResponse::class, $o['200']); + $this->assertInstanceOf(Request::class, $requestReceived); + $this->assertSame('http://200.com', (string) $requestReceived->url()); $this->assertInstanceOf(TestResponse::class, $o['401-response']); $this->assertEquals('different', $o['401-response']->body()); $this->assertInstanceOf(RequestException::class, $o['401-throwing']); From af5b345721e74144fbac025ee5bf42611d1f5707 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:17:07 +0600 Subject: [PATCH 049/596] [13.x] Add isNotEmpty() method to Uri class (#59408) Add the isNotEmpty() counterpart to isEmpty(), consistent with the pattern used by Stringable, HtmlString, MessageBag, and Fluent. --- src/Illuminate/Support/Uri.php | 8 ++++++++ tests/Support/SupportUriTest.php | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index 44da70853e05..70139936b443 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -409,6 +409,14 @@ public function isEmpty(): bool return trim($this->value()) === ''; } + /** + * Determine if the URI is not an empty string. + */ + public function isNotEmpty(): bool + { + return ! $this->isEmpty(); + } + /** * Dump the string representation of the URI. * diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 8e4a8f2814ab..3874f80c67c4 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -45,6 +45,15 @@ public function test_basic_uri_interactions() $this->assertEquals('taylor:password@laravel.com', $uri->authority()); } + public function test_is_empty_and_is_not_empty() + { + $this->assertTrue(Uri::of('')->isEmpty()); + $this->assertFalse(Uri::of('')->isNotEmpty()); + + $this->assertFalse(Uri::of('https://laravel.com')->isEmpty()); + $this->assertTrue(Uri::of('https://laravel.com')->isNotEmpty()); + } + public function test_complicated_query_string_parsing() { $uri = Uri::of('https://example.com/users?key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&key.6=value&flag_value'); From c03d444fb1771dcb2aca43ecb2ff67bcb6209675 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:17:26 +0600 Subject: [PATCH 050/596] [13.x] Add missing capitalize parameter to Stringable::initials() (#59407) Str::initials() accepts a $capitalize parameter but the Stringable wrapper did not expose it, making it impossible to capitalize initials when using the fluent string API. --- src/Illuminate/Support/Stringable.php | 5 +++-- tests/Support/SupportStringableTest.php | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index 64b4cf5d1960..fbb7fbda1f36 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -894,11 +894,12 @@ public function headline() /** * Convert the given string to only its initials. * + * @param bool $capitalize * @return static */ - public function initials() + public function initials($capitalize = false) { - return new static(Str::initials($this->value)); + return new static(Str::initials($this->value, $capitalize)); } /** diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index fdf70ea9f51e..f31df4b921da 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -1484,6 +1484,9 @@ public function testExactly() public function testInitials() { $this->assertSame('TO', $this->stringable('Taylor Otwell')->initials()->value()); + $this->assertSame('to', $this->stringable('taylor otwell')->initials()->value()); + $this->assertSame('TO', $this->stringable('taylor otwell')->initials(capitalize: true)->value()); + $this->assertSame('JB', $this->stringable('james bond')->initials(capitalize: true)->value()); } public function testToInteger() From 45b8782cd563b8bbaa22ad3ab332d7ec52d9381b Mon Sep 17 00:00:00 2001 From: sadique hussain <32757358+sadique-cws@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:51:20 +0530 Subject: [PATCH 051/596] [13.x] Fix trait initializer collision with Attribute parsing (#59404) Trait initializers calling mergeAppends(), mergeHidden(), mergeVisible(), or mergeFillable() would have their values silently dropped when the model also used PHP Attributes like #[Appends], #[Hidden], #[Visible], or #[Fillable]. This happened because the framework's own initializers used `if (empty($this->property))` guards before assigning from Attributes. When trait initializers ran first and populated the property via merge, the guard would see a non-empty value and skip the Attribute entirely. The fix changes from guarded assignment to always using the corresponding merge method, ensuring that values from both sources are combined regardless of initializer execution order. Fixes #59381 --- .../Eloquent/Concerns/GuardsAttributes.php | 4 +- .../Eloquent/Concerns/HasAttributes.php | 4 +- .../Eloquent/Concerns/HidesAttributes.php | 9 +- .../DatabaseEloquentModelAttributesTest.php | 90 ++++++++++++++++++- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index aa9b2c60c9be..e99bd922f63a 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -45,9 +45,7 @@ trait GuardsAttributes #[Initialize] public function initializeGuardsAttributes() { - if (empty($this->fillable)) { - $this->fillable = static::resolveClassAttribute(Fillable::class, 'columns') ?? []; - } + $this->mergeFillable(static::resolveClassAttribute(Fillable::class, 'columns') ?? []); if ($this->guarded === ['*']) { if (static::resolveClassAttribute(Unguarded::class) !== null) { diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index ad567a6cdadd..a71fcc416cf3 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -214,9 +214,7 @@ protected function initializeHasAttributes() ?? static::resolveClassAttribute(Table::class)->dateFormat ?? null; - if (empty($this->appends)) { - $this->appends = static::resolveClassAttribute(Appends::class, 'columns') ?? []; - } + $this->mergeAppends(static::resolveClassAttribute(Appends::class, 'columns') ?? []); } /** diff --git a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php index 0bc64fc59084..1d3d7e591418 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php @@ -30,13 +30,8 @@ trait HidesAttributes #[Initialize] public function initializeHidesAttributes() { - if (empty($this->hidden)) { - $this->hidden = static::resolveClassAttribute(Hidden::class, 'columns') ?? []; - } - - if (empty($this->visible)) { - $this->visible = static::resolveClassAttribute(Visible::class, 'columns') ?? []; - } + $this->mergeHidden(static::resolveClassAttribute(Hidden::class, 'columns') ?? []); + $this->mergeVisible(static::resolveClassAttribute(Visible::class, 'columns') ?? []); } /** diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index be2c9351051f..cdf560a7086d 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -198,11 +198,11 @@ public function test_fillable_attribute(): void $this->assertSame(['name', 'email'], $model->getFillable()); } - public function test_fillable_property_takes_precedence(): void + public function test_fillable_property_merges_with_attribute(): void { $model = new ModelWithFillableAttributeAndProperty; - $this->assertSame(['title'], $model->getFillable()); + $this->assertEqualsCanonicalizing(['title', 'name', 'email'], $model->getFillable()); } public function test_guarded_attribute(): void @@ -312,6 +312,34 @@ public function test_is_ignoring_touch_with_timestamps_attribute(): void $this->assertTrue(ModelWithTimestampsFalseAttribute::isIgnoringTouch()); $this->assertFalse(ModelWithFillableAttribute::isIgnoringTouch()); } + + public function test_trait_initializer_merges_appends_with_attribute(): void + { + $model = new ModelWithAppendsAttributeAndTrait; + + $this->assertEqualsCanonicalizing(['full_name', 'is_admin', 'url'], $model->getAppends()); + } + + public function test_trait_initializer_merges_hidden_with_attribute(): void + { + $model = new ModelWithHiddenAttributeAndTrait; + + $this->assertEqualsCanonicalizing(['password', 'secret', 'api_token'], $model->getHidden()); + } + + public function test_trait_initializer_merges_visible_with_attribute(): void + { + $model = new ModelWithVisibleAttributeAndTrait; + + $this->assertEqualsCanonicalizing(['id', 'name', 'email'], $model->getVisible()); + } + + public function test_trait_initializer_merges_fillable_with_attribute(): void + { + $model = new ModelWithFillableAttributeAndTrait; + + $this->assertEqualsCanonicalizing(['name', 'email', 'phone'], $model->getFillable()); + } } enum ConnectionUnitEnum @@ -517,3 +545,61 @@ class PivotWithIncrementing extends \Illuminate\Database\Eloquent\Relations\Pivo { // } + +// Traits for testing trait initializer + Attribute collision + +trait AddsUrlAppend +{ + protected function initializeAddsUrlAppend() + { + $this->mergeAppends(['url']); + } +} + +trait AddsApiTokenHidden +{ + protected function initializeAddsApiTokenHidden() + { + $this->mergeHidden(['api_token']); + } +} + +trait AddsEmailVisible +{ + protected function initializeAddsEmailVisible() + { + $this->mergeVisible(['email']); + } +} + +trait AddsPhoneFillable +{ + protected function initializeAddsPhoneFillable() + { + $this->mergeFillable(['phone']); + } +} + +#[Appends(['full_name', 'is_admin'])] +class ModelWithAppendsAttributeAndTrait extends Model +{ + use AddsUrlAppend; +} + +#[Hidden(['password', 'secret'])] +class ModelWithHiddenAttributeAndTrait extends Model +{ + use AddsApiTokenHidden; +} + +#[Visible(['id', 'name'])] +class ModelWithVisibleAttributeAndTrait extends Model +{ + use AddsEmailVisible; +} + +#[Fillable(['name', 'email'])] +class ModelWithFillableAttributeAndTrait extends Model +{ + use AddsPhoneFillable; +} From 3702bd6f77b0c2c8dca45fbb8e02316c8ea0d925 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 27 Mar 2026 17:22:04 +0000 Subject: [PATCH 052/596] [13.x] Add session to supported drivers comment (#59399) * Update cache.php * Update cache.php --------- Co-authored-by: Taylor Otwell --- config/cache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cache.php b/config/cache.php index e84ddb126e3e..807344931eb3 100644 --- a/config/cache.php +++ b/config/cache.php @@ -27,7 +27,7 @@ | same cache driver to group types of items stored in your caches. | | Supported drivers: "array", "database", "file", "memcached", - | "redis", "dynamodb", "octane", + | "redis", "dynamodb", "octane", "session", | "failover", "null" | */ From 09c6232a73d290f7360f92f03d52511a434de400 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 27 Mar 2026 18:24:07 +0100 Subject: [PATCH 053/596] feature/validated-input-file (#59396) --- src/Illuminate/Support/ValidatedInput.php | 14 ++++++++++++++ tests/Support/ValidatedInputTest.php | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/Illuminate/Support/ValidatedInput.php b/src/Illuminate/Support/ValidatedInput.php index 4c863f0d7d67..f089f4561a78 100644 --- a/src/Illuminate/Support/ValidatedInput.php +++ b/src/Illuminate/Support/ValidatedInput.php @@ -97,6 +97,20 @@ public function input($key = null, $default = null) ); } + /** + * Retrieve a file from the validated inputs. + * + * @param string $key + * @param mixed $default + * @return \Illuminate\Http\UploadedFile|null + */ + public function file($key, $default = null) + { + $value = $this->input($key, $default); + + return $value instanceof \Illuminate\Http\UploadedFile ? $value : $default; + } + /** * Dump the items. * diff --git a/tests/Support/ValidatedInputTest.php b/tests/Support/ValidatedInputTest.php index fe821604ba1b..af2e60d7352a 100644 --- a/tests/Support/ValidatedInputTest.php +++ b/tests/Support/ValidatedInputTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Support; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Stringable; @@ -493,6 +494,23 @@ public function test_enums_method() $this->assertEmpty($input->enums('invalid_enum_value', StringBackedEnum::class)); } + public function test_file_method() + { + $file = UploadedFile::fake()->create('document.pdf'); + + $input = new ValidatedInput([ + 'name' => 'Taylor', + 'avatar' => $file, + ]); + + $this->assertInstanceOf(UploadedFile::class, $input->file('avatar')); + $this->assertSame($file, $input->file('avatar')); + $this->assertNull($input->file('name')); + $this->assertNull($input->file('missing')); + $this->assertSame('default', $input->file('missing', 'default')); + $this->assertSame('default', $input->file('name', 'default')); + } + public function test_collect_method() { $input = new ValidatedInput(['users' => [1, 2, 3]]); From 5fa619e36f5e0b76adce609f5d77c0b7e66fa250 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:28:21 +0600 Subject: [PATCH 054/596] [13.x] Add enum support to LogManager channel and driver methods (#59391) * [13.x] Add enum support to LogManager channel and driver methods Align LogManager with DatabaseManager, FilesystemManager, RedisManager, and QueueManager which already accept enums via enum_value(). * Update LogManager.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Log/LogManager.php | 8 +++++--- src/Illuminate/Support/Facades/Log.php | 4 ++-- tests/Log/LogManagerTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 2987694f1941..d88e4bb7fdd1 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -23,6 +23,8 @@ use Psr\Log\LoggerInterface; use Throwable; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Log\Logger */ @@ -106,7 +108,7 @@ public function stack(array $channels, $channel = null) /** * Get a log channel instance. * - * @param string|null $channel + * @param \UnitEnum|string|null $channel * @return \Psr\Log\LoggerInterface */ public function channel($channel = null) @@ -117,12 +119,12 @@ public function channel($channel = null) /** * Get a log driver instance. * - * @param string|null $driver + * @param \UnitEnum|string|null $driver * @return \Psr\Log\LoggerInterface */ public function driver($driver = null) { - return $this->get($this->parseDriver($driver)); + return $this->get($this->parseDriver(enum_value($driver))); } /** diff --git a/src/Illuminate/Support/Facades/Log.php b/src/Illuminate/Support/Facades/Log.php index 2617ef87f090..923fef17d23f 100755 --- a/src/Illuminate/Support/Facades/Log.php +++ b/src/Illuminate/Support/Facades/Log.php @@ -5,8 +5,8 @@ /** * @method static \Psr\Log\LoggerInterface build(array $config) * @method static \Psr\Log\LoggerInterface stack(array $channels, string|null $channel = null) - * @method static \Psr\Log\LoggerInterface channel(string|null $channel = null) - * @method static \Psr\Log\LoggerInterface driver(string|null $driver = null) + * @method static \Psr\Log\LoggerInterface channel(\UnitEnum|string|null $channel = null) + * @method static \Psr\Log\LoggerInterface driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Log\LogManager shareContext(array $context) * @method static array sharedContext() * @method static \Illuminate\Log\LogManager withoutContext(string[]|null $keys = null) diff --git a/tests/Log/LogManagerTest.php b/tests/Log/LogManagerTest.php index 889a67eb9b7c..fc195468ee04 100755 --- a/tests/Log/LogManagerTest.php +++ b/tests/Log/LogManagerTest.php @@ -764,6 +764,26 @@ public function testCustomDriverClosureBoundObjectIsLogManager() $manager->extend(__CLASS__, fn () => $this); $this->assertSame($manager, $manager->channel(__CLASS__)->getLogger()); } + + public function testLogManagerCanResolveBackedEnumChannel() + { + $manager = new LogManager($this->app); + + $logger1 = $manager->channel(LogChannelName::Single); + $logger2 = $manager->channel('single'); + + $this->assertSame($logger1, $logger2); + } + + public function testLogManagerCanResolveBackedEnumDriver() + { + $manager = new LogManager($this->app); + + $logger1 = $manager->driver(LogChannelName::Single); + $logger2 = $manager->driver('single'); + + $this->assertSame($logger1, $logger2); + } } class CustomizeFormatter @@ -793,3 +813,8 @@ public function log($level, \Stringable|string $message, array $context = []): v ]; } } + +enum LogChannelName: string +{ + case Single = 'single'; +} From 65d16321e40c1e9148bf395443d7df3b557b8cee Mon Sep 17 00:00:00 2001 From: Wietse Warendorff Date: Fri, 27 Mar 2026 18:30:22 +0100 Subject: [PATCH 055/596] [13.x] Fix MorphTo eager load matching when ownerKey is null and result key is a non-primitive (#59394) --- .../Database/Eloquent/Relations/MorphTo.php | 2 +- .../Database/DatabaseEloquentMorphToTest.php | 42 +++++ .../Database/EloquentMorphToEagerLoadTest.php | 150 ++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Database/EloquentMorphToEagerLoadTest.php diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index 8fe672d3e62d..2ca72e652183 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -226,7 +226,7 @@ public function match(array $models, EloquentCollection $results, $relation) protected function matchToMorphParents($type, EloquentCollection $results) { foreach ($results as $result) { - $ownerKey = ! is_null($this->ownerKey) ? $this->getDictionaryKey($result->{$this->ownerKey}) : $result->getKey(); + $ownerKey = $this->getDictionaryKey(! is_null($this->ownerKey) ? $result->{$this->ownerKey} : $result->getKey()); if ($ownerKey !== null && isset($this->dictionary[$type][$ownerKey])) { foreach ($this->dictionary[$type][$ownerKey] as $model) { diff --git a/tests/Database/DatabaseEloquentMorphToTest.php b/tests/Database/DatabaseEloquentMorphToTest.php index fe8e7e6b0dda..0b920475190f 100644 --- a/tests/Database/DatabaseEloquentMorphToTest.php +++ b/tests/Database/DatabaseEloquentMorphToTest.php @@ -3,8 +3,10 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Tests\Database\stubs\TestEnum; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -356,6 +358,38 @@ public function testIsNotModelWithAnotherConnection() $this->assertFalse($relation->is($model)); } + public function testMatchToMorphParentsNormalizesKeyWhenOwnerKeyIsNullAndResultKeyIsObject() + { + $uuidObject = new class + { + public function __toString(): string + { + return 'uuid-value'; + } + }; + + $builder = m::mock(Builder::class); + $related = m::mock(Model::class); + $builder->shouldReceive('getModel')->andReturn($related); + + $parent = new EloquentMorphToModelStub; + $parent->morph_type = 'type_1'; + $parent->foreign_key = 'uuid-value'; + + $relation = Relation::noConstraints(function () use ($builder, $parent) { + return new EloquentMorphToAccessibleStub($builder, $parent, 'foreign_key', null, 'morph_type', 'relation'); + }); + + $relation->addEagerConstraints([$parent]); + + $result = m::mock(Model::class); + $result->shouldReceive('getKey')->once()->andReturn($uuidObject); + + $relation->callMatchToMorphParents('type_1', new EloquentCollection([$result])); + + $this->assertSame($result, $parent->getRelation('relation')); + } + protected function getRelationAssociate($parent) { $builder = m::mock(Builder::class); @@ -400,3 +434,11 @@ class EloquentMorphToRelatedStub extends Model { public $table = 'eloquent_morph_to_related_stubs'; } + +class EloquentMorphToAccessibleStub extends MorphTo +{ + public function callMatchToMorphParents($type, EloquentCollection $results): void + { + $this->matchToMorphParents($type, $results); + } +} diff --git a/tests/Integration/Database/EloquentMorphToEagerLoadTest.php b/tests/Integration/Database/EloquentMorphToEagerLoadTest.php new file mode 100644 index 000000000000..c178ae5f49aa --- /dev/null +++ b/tests/Integration/Database/EloquentMorphToEagerLoadTest.php @@ -0,0 +1,150 @@ +increments('id'); + }); + + Schema::create('articles', function (Blueprint $table) { + $table->string('slug')->primary(); + }); + + Schema::create('videos', function (Blueprint $table) { + $table->string('id')->primary(); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->string('commentable_type'); + $table->string('commentable_id'); + }); + + $post = Post::create(); + $article = Article::create(['slug' => ArticleSlug::Review->value]); + $video = Video::create(['id' => '550e8400-e29b-41d4-a716-446655440000']); + + (new Comment)->commentable()->associate($post)->save(); + (new Comment)->commentable()->associate($article)->save(); + + $comment = new Comment; + $comment->commentable_type = Video::class; + $comment->commentable_id = (string) $video->id; + $comment->save(); + } + + public function testEagerLoadingResolvesRelationWithPrimitivePrimaryKey(): void + { + $comments = Comment::with('commentable') + ->where('commentable_type', Post::class) + ->get(); + + $this->assertNotNull($comments[0]->commentable); + $this->assertInstanceOf(Post::class, $comments[0]->commentable); + } + + public function testEagerLoadingResolvesRelationWithBackedEnumPrimaryKey(): void + { + $comments = Comment::with('commentable') + ->where('commentable_type', Article::class) + ->get(); + + $this->assertNotNull($comments[0]->commentable); + $this->assertInstanceOf(Article::class, $comments[0]->commentable); + $this->assertSame(ArticleSlug::Review, $comments[0]->commentable->slug); + } + + public function testEagerLoadingResolvesRelationWithUuidValueObjectPrimaryKey(): void + { + $comments = Comment::with('commentable') + ->where('commentable_type', Video::class) + ->get(); + + $this->assertNotNull($comments[0]->commentable); + $this->assertInstanceOf(Video::class, $comments[0]->commentable); + $this->assertSame('550e8400-e29b-41d4-a716-446655440000', (string) $comments[0]->commentable->id); + } +} + +enum ArticleSlug: string +{ + case Review = 'review'; +} + +class Post extends Model +{ + public $timestamps = false; +} + +class Article extends Model +{ + public $timestamps = false; + + protected $primaryKey = 'slug'; + + protected $keyType = 'string'; + + public $incrementing = false; + + protected $casts = ['slug' => ArticleSlug::class]; + + protected $fillable = ['slug']; +} + +class Comment extends Model +{ + public $timestamps = false; + + public function commentable() + { + return $this->morphTo(); + } +} + +class Uuid +{ + public function __construct(private readonly string $value) + { + } + + public function __toString(): string + { + return $this->value; + } +} + +class UuidCast implements CastsAttributes +{ + public function get(Model $model, string $key, mixed $value, array $attributes): mixed + { + return new Uuid($value); + } + + public function set(Model $model, string $key, mixed $value, array $attributes): mixed + { + return (string) $value; + } +} + +class Video extends Model +{ + public $timestamps = false; + + public $incrementing = false; + + protected $fillable = ['id']; + + protected $keyType = 'string'; + + protected $casts = ['id' => UuidCast::class]; +} From f920a23d35719af5b93810d65e4a1b1de630f858 Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Sat, 28 Mar 2026 03:37:49 +1000 Subject: [PATCH 056/596] [13.x] Remove unnecessary clone in SessionManager to prevent duplicate Redis connections (#59323) Co-authored-by: Claude Opus 4.6 (1M context) --- src/Illuminate/Session/SessionManager.php | 14 ++- tests/Session/SessionManagerTest.php | 116 ++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 tests/Session/SessionManagerTest.php diff --git a/src/Illuminate/Session/SessionManager.php b/src/Illuminate/Session/SessionManager.php index 0b176ef6d01a..2616ba112b85 100755 --- a/src/Illuminate/Session/SessionManager.php +++ b/src/Illuminate/Session/SessionManager.php @@ -137,9 +137,15 @@ protected function createRedisDriver() { $handler = $this->createCacheHandler('redis'); - $handler->getCache()->getStore()->setConnection( - $this->config->get('session.connection') - ); + $connection = $this->config->get('session.connection'); + + if ($connection) { + $handler->getCache()->setStore( + tap(clone $handler->getCache()->getStore(), function ($store) use ($connection) { + $store->setConnection($connection); + }) + ); + } return $this->buildSession($handler); } @@ -176,7 +182,7 @@ protected function createCacheHandler($driver) $store = $this->config->get('session.store') ?: $driver; return new CacheBasedSessionHandler( - clone $this->container->make('cache')->store($store), + $this->container->make('cache')->store($store), $this->config->get('session.lifetime') ); } diff --git a/tests/Session/SessionManagerTest.php b/tests/Session/SessionManagerTest.php new file mode 100644 index 000000000000..13a35a9cc8b2 --- /dev/null +++ b/tests/Session/SessionManagerTest.php @@ -0,0 +1,116 @@ +createApplication('memcached'); + + $manager = new SessionManager($app); + $session = $manager->driver('memcached'); + + $handler = $session->getHandler(); + + $this->assertInstanceOf(CacheBasedSessionHandler::class, $handler); + + // The handler should use the same Repository instance, not a clone + $this->assertSame( + $app->make('cache')->store('memcached'), + $handler->getCache() + ); + } + + public function testRedisSessionWithoutConnectionSharesCacheRepository() + { + $app = $this->createApplication('redis'); + + $manager = new SessionManager($app); + $session = $manager->driver('redis'); + + $handler = $session->getHandler(); + + $this->assertInstanceOf(CacheBasedSessionHandler::class, $handler); + + // Without session.connection, the handler should share the cache Repository + $this->assertSame( + $app->make('cache')->store('redis'), + $handler->getCache() + ); + } + + public function testRedisSessionWithConnectionDoesNotMutateSharedStore() + { + $app = $this->createApplication('redis', 'session'); + + $sharedStore = $app->make('cache')->store('redis')->getStore(); + $originalConnection = (new \ReflectionProperty($sharedStore, 'connection'))->getValue($sharedStore); + + $manager = new SessionManager($app); + $session = $manager->driver('redis'); + + $handler = $session->getHandler(); + + // The shared cache store's connection should not be mutated + $currentConnection = (new \ReflectionProperty($sharedStore, 'connection'))->getValue($sharedStore); + $this->assertSame($originalConnection, $currentConnection); + + // The session handler's store should have the session connection + $sessionStore = $handler->getCache()->getStore(); + $sessionConnection = (new \ReflectionProperty($sessionStore, 'connection'))->getValue($sessionStore); + $this->assertSame('session', $sessionConnection); + } + + protected function createApplication(string $driver, ?string $sessionConnection = null): Container + { + $app = new Container; + Container::setInstance($app); + + $config = new Repository([ + 'session' => [ + 'driver' => $driver, + 'lifetime' => 120, + 'connection' => $sessionConnection, + 'store' => null, + ], + 'cache' => [ + 'default' => $driver, + 'stores' => [ + 'memcached' => ['driver' => 'array'], + 'redis' => ['driver' => 'redis', 'connection' => 'default'], + ], + 'prefix' => 'test', + ], + ]); + + $app->instance('config', $config); + $app->singleton('cache', function ($app) { + return new \Illuminate\Cache\CacheManager($app); + }); + + $app->singleton('redis', function () { + $redis = m::mock(\Illuminate\Contracts\Redis\Factory::class); + $redis->shouldReceive('connection')->andReturn( + m::mock(\Illuminate\Redis\Connections\Connection::class) + ); + + return $redis; + }); + + return $app; + } +} From 0eeb3ffed881a649768d5fd1fa4ae0e04ec0d6fd Mon Sep 17 00:00:00 2001 From: Wong Ban Korh Date: Sat, 28 Mar 2026 01:48:25 +0800 Subject: [PATCH 057/596] [13.x] Use FQCN for Str in exception renderer blade templates (#59412) Replace unqualified calls with in exception renderer blade templates to prevent errors when class aliases are not registered. --- .../renderer/components/previous-exceptions.blade.php | 2 +- .../resources/exceptions/renderer/components/trace.blade.php | 2 +- .../Foundation/resources/exceptions/renderer/markdown.blade.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/previous-exceptions.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/previous-exceptions.blade.php index fc9cc1ed9cf8..2bf03deb84e9 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/previous-exceptions.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/previous-exceptions.blade.php @@ -5,7 +5,7 @@
-

Previous {{ Str::plural('exception', $exception->previousExceptions()->count()) }}

+

Previous {{ \Illuminate\Support\Str::plural('exception', $exception->previousExceptions()->count()) }}

diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/trace.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/trace.blade.php index dd79c41bf75a..329d31aa881d 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/trace.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/trace.blade.php @@ -8,7 +8,7 @@

Exception trace

@if ($exception->previousExceptions()->isNotEmpty()) - {{ $exception->previousExceptions()->count() }} previous {{ Str::plural('exception', $exception->previousExceptions()->count()) }} + {{ $exception->previousExceptions()->count() }} previous {{ \Illuminate\Support\Str::plural('exception', $exception->previousExceptions()->count()) }} @endif
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php index 0067b9c4c310..6003f3e7c58f 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php @@ -13,7 +13,7 @@ @endforeach @if ($exception->previousExceptions()->isNotEmpty()) -## Previous {{ Str::plural('exception', $exception->previousExceptions()->count()) }} +## Previous {{ \Illuminate\Support\Str::plural('exception', $exception->previousExceptions()->count()) }} @foreach ($exception->previousExceptions() as $index => $previous) ### {{ $index + 1 }}. {{ $previous->class() }} From 1ffb542158d41984661dfaa9881c9e5d3b0736bd Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Sat, 28 Mar 2026 15:03:17 -0400 Subject: [PATCH 058/596] Allow variadic args for model attributes (#59421) --- .../Database/Eloquent/Attributes/Appends.php | 10 ++- .../Database/Eloquent/Attributes/Fillable.php | 10 ++- .../Database/Eloquent/Attributes/Guarded.php | 10 ++- .../Database/Eloquent/Attributes/Hidden.php | 10 ++- .../Database/Eloquent/Attributes/Touches.php | 10 ++- .../Database/Eloquent/Attributes/Visible.php | 10 ++- .../DatabaseEloquentModelAttributesTest.php | 78 +++++++++++++++++++ 7 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Attributes/Appends.php b/src/Illuminate/Database/Eloquent/Attributes/Appends.php index 6b696e3123ec..b889323458d5 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Appends.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Appends.php @@ -7,12 +7,18 @@ #[Attribute(Attribute::TARGET_CLASS)] class Appends { + /** + * @var array + */ + public array $columns; + /** * Create a new attribute instance. * - * @param array $columns + * @param array|string ...$columns */ - public function __construct(public array $columns) + public function __construct(array|string ...$columns) { + $this->columns = is_array($columns[0]) ? $columns[0] : $columns; } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/Fillable.php b/src/Illuminate/Database/Eloquent/Attributes/Fillable.php index e6558e95bc8b..014d857f23e5 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Fillable.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Fillable.php @@ -7,12 +7,18 @@ #[Attribute(Attribute::TARGET_CLASS)] class Fillable { + /** + * @var array + */ + public array $columns; + /** * Create a new attribute instance. * - * @param array $columns + * @param array|string ...$columns */ - public function __construct(public array $columns) + public function __construct(array|string ...$columns) { + $this->columns = is_array($columns[0]) ? $columns[0] : $columns; } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/Guarded.php b/src/Illuminate/Database/Eloquent/Attributes/Guarded.php index d2f9c34e8d8d..fe96b962f449 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Guarded.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Guarded.php @@ -7,12 +7,18 @@ #[Attribute(Attribute::TARGET_CLASS)] class Guarded { + /** + * @var array + */ + public array $columns; + /** * Create a new attribute instance. * - * @param array $columns + * @param array|string ...$columns */ - public function __construct(public array $columns) + public function __construct(array|string ...$columns) { + $this->columns = is_array($columns[0]) ? $columns[0] : $columns; } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/Hidden.php b/src/Illuminate/Database/Eloquent/Attributes/Hidden.php index a7dbfbc4469e..9bf2d384960e 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Hidden.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Hidden.php @@ -7,12 +7,18 @@ #[Attribute(Attribute::TARGET_CLASS)] class Hidden { + /** + * @var array + */ + public array $columns; + /** * Create a new attribute instance. * - * @param array $columns + * @param array|string ...$columns */ - public function __construct(public array $columns) + public function __construct(array|string ...$columns) { + $this->columns = is_array($columns[0]) ? $columns[0] : $columns; } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/Touches.php b/src/Illuminate/Database/Eloquent/Attributes/Touches.php index b016bd5ea1b1..b49645915aab 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Touches.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Touches.php @@ -7,12 +7,18 @@ #[Attribute(Attribute::TARGET_CLASS)] class Touches { + /** + * @var array + */ + public array $relations; + /** * Create a new attribute instance. * - * @param array $relations + * @param array|string ...$relations */ - public function __construct(public array $relations) + public function __construct(array|string ...$relations) { + $this->relations = is_array($relations[0]) ? $relations[0] : $relations; } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/Visible.php b/src/Illuminate/Database/Eloquent/Attributes/Visible.php index b9c87575a4ac..3b7a879d0aec 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/Visible.php +++ b/src/Illuminate/Database/Eloquent/Attributes/Visible.php @@ -7,12 +7,18 @@ #[Attribute(Attribute::TARGET_CLASS)] class Visible { + /** + * @var array + */ + public array $columns; + /** * Create a new attribute instance. * - * @param array $columns + * @param array|string ...$columns */ - public function __construct(public array $columns) + public function __construct(array|string ...$columns) { + $this->columns = is_array($columns[0]) ? $columns[0] : $columns; } } diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index cdf560a7086d..8279ac4dc5ad 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -198,6 +198,13 @@ public function test_fillable_attribute(): void $this->assertSame(['name', 'email'], $model->getFillable()); } + public function test_fillable_attribute_variadic(): void + { + $model = new ModelWithFillableAttributeVariadic; + + $this->assertSame(['name', 'email'], $model->getFillable()); + } + public function test_fillable_property_merges_with_attribute(): void { $model = new ModelWithFillableAttributeAndProperty; @@ -212,6 +219,13 @@ public function test_guarded_attribute(): void $this->assertSame(['id', 'secret'], $model->getGuarded()); } + public function test_guarded_attribute_variadic(): void + { + $model = new ModelWithGuardedAttributeVariadic; + + $this->assertSame(['id', 'secret'], $model->getGuarded()); + } + public function test_guarded_property_takes_precedence(): void { $model = new ModelWithGuardedAttributeAndProperty; @@ -241,6 +255,13 @@ public function test_hidden_attribute(): void $this->assertSame(['password', 'secret'], $model->getHidden()); } + public function test_hidden_attribute_variadic(): void + { + $model = new ModelWithHiddenAttributeVariadic; + + $this->assertSame(['password', 'secret'], $model->getHidden()); + } + public function test_visible_attribute(): void { $model = new ModelWithVisibleAttribute; @@ -248,6 +269,13 @@ public function test_visible_attribute(): void $this->assertSame(['id', 'name'], $model->getVisible()); } + public function test_visible_attribute_variadic(): void + { + $model = new ModelWithVisibleAttributeVariadic; + + $this->assertSame(['id', 'name'], $model->getVisible()); + } + public function test_appends_attribute(): void { $model = new ModelWithAppendsAttribute; @@ -255,6 +283,13 @@ public function test_appends_attribute(): void $this->assertSame(['full_name', 'is_admin'], $model->getAppends()); } + public function test_appends_attribute_variadic(): void + { + $model = new ModelWithAppendsAttributeVariadic; + + $this->assertSame(['full_name', 'is_admin'], $model->getAppends()); + } + public function test_touches_attribute(): void { $model = new ModelWithTouchesAttribute; @@ -262,6 +297,13 @@ public function test_touches_attribute(): void $this->assertSame(['post', 'author'], $model->getTouchedRelations()); } + public function test_touches_attribute_variadic(): void + { + $model = new ModelWithTouchesAttributeVariadic; + + $this->assertSame(['post', 'author'], $model->getTouchedRelations()); + } + public function test_merge_fillable_works_with_attribute(): void { $model = new ModelWithFillableAttribute; @@ -442,6 +484,12 @@ class ModelWithFillableAttribute extends Model // } +#[Fillable('name', 'email')] +class ModelWithFillableAttributeVariadic extends Model +{ + // +} + #[Fillable(['name', 'email'])] class ModelWithFillableAttributeAndProperty extends Model { @@ -454,6 +502,12 @@ class ModelWithGuardedAttribute extends Model // } +#[Guarded('id', 'secret')] +class ModelWithGuardedAttributeVariadic extends Model +{ + // +} + #[Guarded(['id', 'secret'])] class ModelWithGuardedAttributeAndProperty extends Model { @@ -483,24 +537,48 @@ class ModelWithHiddenAttribute extends Model // } +#[Hidden('password', 'secret')] +class ModelWithHiddenAttributeVariadic extends Model +{ + // +} + #[Visible(['id', 'name'])] class ModelWithVisibleAttribute extends Model { // } +#[Visible('id', 'name')] +class ModelWithVisibleAttributeVariadic extends Model +{ + // +} + #[Appends(['full_name', 'is_admin'])] class ModelWithAppendsAttribute extends Model { // } +#[Appends('full_name', 'is_admin')] +class ModelWithAppendsAttributeVariadic extends Model +{ + // +} + #[Touches(['post', 'author'])] class ModelWithTouchesAttribute extends Model { // } +#[Touches('post', 'author')] +class ModelWithTouchesAttributeVariadic extends Model +{ + // +} + #[DateFormat('Y-m-d')] class ModelWithDedicatedDateFormatAttribute extends Model { From 8222fc56bba94eaa8a4999bfc5b5a86cf59014d5 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sat, 28 Mar 2026 19:13:13 +0000 Subject: [PATCH 059/596] [13.x] CollectedBy Attribute should follow inheritence (#59419) * 13.x collectedby inheritance * wip --- src/Illuminate/Database/Eloquent/HasCollection.php | 7 ++++++- tests/Database/DatabaseEloquentModelTest.php | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/HasCollection.php b/src/Illuminate/Database/Eloquent/HasCollection.php index d430f0099b81..a2ef15bdffe4 100644 --- a/src/Illuminate/Database/Eloquent/HasCollection.php +++ b/src/Illuminate/Database/Eloquent/HasCollection.php @@ -45,10 +45,15 @@ public function resolveCollectionFromAttribute() { $reflectionClass = new ReflectionClass(static::class); + $isEloquentGrandchild = is_subclass_of(static::class, Model::class) + && get_parent_class(static::class) !== Model::class; + $attributes = $reflectionClass->getAttributes(CollectedBy::class); if (! isset($attributes[0]) || ! isset($attributes[0]->getArguments()[0])) { - return; + return $isEloquentGrandchild + ? (new (get_parent_class(static::class)))->resolveCollectionFromAttribute() + : null; } return $attributes[0]->getArguments()[0]; diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 2ccf8f74b4e9..9d4e9dd66e7c 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3772,6 +3772,14 @@ public function testCollectedByAttribute() $this->assertInstanceOf(CustomEloquentCollection::class, $collection); } + public function testCollectedByAttributeIsInherited() + { + $model = new EloquentChildModelWithCollectedByAttribute; + $collection = $model->newCollection([$model]); + + $this->assertInstanceOf(CustomEloquentCollection::class, $collection); + } + public function testUseFactoryAttribute() { $model = new EloquentModelWithUseFactoryAttribute; @@ -4678,6 +4686,10 @@ class EloquentModelWithCollectedByAttribute extends Model { } +class EloquentChildModelWithCollectedByAttribute extends EloquentModelWithCollectedByAttribute +{ +} + class CustomEloquentCollection extends Collection { } From 691715e436c991b3972b3697647c1444bd47e1ad Mon Sep 17 00:00:00 2001 From: Ali Hamze Date: Sat, 28 Mar 2026 15:13:51 -0400 Subject: [PATCH 060/596] [13.x] Fix deprecation notice in JSON:API resources (#59418) * [13.x] Fix deprecation notice in JSON:API resources * Update JsonApiRequest.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index 4500c4afe414..51224a8ea9f3 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -62,6 +62,10 @@ public function sparseIncluded(?string $key = null): ?array return transform($this->cachedSparseIncluded[$key] ?? null, function ($value) { return Collection::wrap($value) ->transform(function ($item) { + if (! is_string($item) || $item === '') { + return null; + } + $item = implode('.', Arr::take(explode('.', $item), JsonApiResource::$maxRelationshipDepth - 1)); return ! empty($item) ? $item : null; From c0dc1d5ff9c507d2fe071bba539545d40999980e Mon Sep 17 00:00:00 2001 From: Joe Theuerkauf <105436210+jtheuerkauf@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:14:43 -0400 Subject: [PATCH 061/596] Fix missing UnitEnum support in ModelNotFoundException (#59423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: use enum_value() in ModelNotFoundException to support UnitEnum Extends the enum handling in setModel() to cover UnitEnum as well as BackedEnum. The previous fix (using instanceof BackedEnum) missed UnitEnum cases, which have no ->value but do have a ->name. Using enum_value() — the framework helper — handles both cleanly: - BackedEnum: returns ->value - UnitEnum: returns ->name - Everything else: passes through unchanged Adds tests for both enum types. * Fix #59147 - enum_value() should be a Closure * Adjusted previous fix, passing enum_value(...) instead of 'enum_value' to array_map() to fix test results See: https://github.com/laravel/framework/pull/59147 --------- Co-authored-by: Isaac Hunja --- .../Eloquent/ModelNotFoundException.php | 8 ++---- .../Database/DatabaseEloquentBuilderTest.php | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/ModelNotFoundException.php b/src/Illuminate/Database/Eloquent/ModelNotFoundException.php index 7ee1a30cdf66..3ef8dfde231c 100755 --- a/src/Illuminate/Database/Eloquent/ModelNotFoundException.php +++ b/src/Illuminate/Database/Eloquent/ModelNotFoundException.php @@ -2,10 +2,11 @@ namespace Illuminate\Database\Eloquent; -use BackedEnum; use Illuminate\Database\RecordsNotFoundException; use Illuminate\Support\Arr; +use function Illuminate\Support\enum_value; + /** * @template TModel of \Illuminate\Database\Eloquent\Model */ @@ -36,10 +37,7 @@ public function setModel($model, $ids = []) { $this->model = $model; - $this->ids = array_map( - fn ($id) => $id instanceof BackedEnum ? $id->value : $id, - Arr::wrap($ids) - ); + $this->ids = array_map(enum_value(...), Arr::wrap($ids)); $this->message = "No query results for model [{$model}]"; diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 6e38006969fb..1a27a831abc2 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -143,6 +143,24 @@ public function testFindOrFailMethodThrowsModelNotFoundException() $builder->findOrFail('bar', ['column']); } + public function testFindOrFailMethodThrowsModelNotFoundExceptionWithBackedEnum() + { + $exception = new ModelNotFoundException; + $exception->setModel('Foo', EloquentBuilderTestBackedEnum::Bar); + + $this->assertSame('No query results for model [Foo] bar', $exception->getMessage()); + $this->assertSame(['bar'], $exception->getIds()); + } + + public function testFindOrFailMethodThrowsModelNotFoundExceptionWithUnitEnum() + { + $exception = new ModelNotFoundException; + $exception->setModel('Foo', EloquentBuilderTestUnitEnum::Baz); + + $this->assertSame('No query results for model [Foo] Baz', $exception->getMessage()); + $this->assertSame(['Baz'], $exception->getIds()); + } + public function testFindOrFailMethodWithManyThrowsModelNotFoundException() { $this->expectException(ModelNotFoundException::class); @@ -3187,3 +3205,13 @@ public function parent() return $this->belongsTo(self::class, 'parent_id', 'id', 'parent'); } } + +enum EloquentBuilderTestBackedEnum: string +{ + case Bar = 'bar'; +} + +enum EloquentBuilderTestUnitEnum +{ + case Baz; +} From 854b7270bc1c3410bcd3ee68faa48ac0c86aa20d Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:15:46 +0600 Subject: [PATCH 062/596] [13.x] Add withoutFragment() method to Uri class (#59413) Add the missing counterpart to withFragment(), consistent with the existing withoutQuery() method pattern. Uses withFragment(null) to fully remove the fragment component from the URI. --- src/Illuminate/Support/Uri.php | 8 ++++++++ tests/Support/SupportUriTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index 70139936b443..ab9258b2ea85 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -334,6 +334,14 @@ public function withFragment(string $fragment): static return new static($this->uri->withFragment($fragment)); } + /** + * Remove the fragment from the URI. + */ + public function withoutFragment(): static + { + return new static($this->uri->withFragment(null)); + } + /** * Create a redirect HTTP response for the given URI. */ diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 3874f80c67c4..dd91e1f6d432 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -54,6 +54,31 @@ public function test_is_empty_and_is_not_empty() $this->assertTrue(Uri::of('https://laravel.com')->isNotEmpty()); } + public function test_without_fragment() + { + $uri = Uri::of('https://laravel.com/docs/installation#introduction'); + + $this->assertEquals('introduction', $uri->fragment()); + + $withoutFragment = $uri->withoutFragment(); + + $this->assertNull($withoutFragment->fragment()); + $this->assertEquals('https://laravel.com/docs/installation', $withoutFragment->value()); + + // Original URI should be unchanged (immutability). + $this->assertEquals('introduction', $uri->fragment()); + } + + public function test_without_fragment_on_uri_without_fragment() + { + $uri = Uri::of('https://laravel.com/docs'); + + $withoutFragment = $uri->withoutFragment(); + + $this->assertNull($withoutFragment->fragment()); + $this->assertEquals('https://laravel.com/docs', $withoutFragment->value()); + } + public function test_complicated_query_string_parsing() { $uri = Uri::of('https://example.com/users?key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&key.6=value&flag_value'); From d8d65fdfe91f40d50c3c12645e2c4d4e7ee9a221 Mon Sep 17 00:00:00 2001 From: Felix Bernhard Date: Sat, 28 Mar 2026 20:16:13 +0100 Subject: [PATCH 063/596] [13.x] Fix macros with static closures (#59414) * support non-static calls of static closures * add tests for all static/non-static combinations --- src/Illuminate/Macroable/Traits/Macroable.php | 8 ++++- tests/Support/SupportMacroableTest.php | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Macroable/Traits/Macroable.php b/src/Illuminate/Macroable/Traits/Macroable.php index 5490f1ea2b13..2ee06e177062 100644 --- a/src/Illuminate/Macroable/Traits/Macroable.php +++ b/src/Illuminate/Macroable/Traits/Macroable.php @@ -6,6 +6,8 @@ use Closure; use ReflectionClass; use ReflectionMethod; +use RuntimeException; +use Throwable; trait Macroable { @@ -120,7 +122,11 @@ public function __call($method, $parameters) $macro = static::$macros[$method]; if ($macro instanceof Closure) { - $macro = $macro->bindTo($this, static::class); + try { + $macro = $macro->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $macro = $macro->bindTo(null, static::class); + } } return $macro(...$parameters); diff --git a/tests/Support/SupportMacroableTest.php b/tests/Support/SupportMacroableTest.php index 78864c76b57c..7d89779e1fe8 100644 --- a/tests/Support/SupportMacroableTest.php +++ b/tests/Support/SupportMacroableTest.php @@ -159,6 +159,42 @@ public function testMethodConflictDoesNotThrowException() $this->assertSame('newMethod', $this->macroable::existingMethod()); } + + public function testStaticCallOfNonStaticClosure() + { + $this->macroable::macro('nonStaticClosure', function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable::nonStaticClosure()); + } + + public function testNonStaticCallOfNonStaticClosure() + { + $this->macroable::macro('nonStaticClosure', function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable->nonStaticClosure()); + } + + public function testStaticCallOfStaticClosure() + { + $this->macroable::macro('staticClosure', static function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable::staticClosure()); + } + + public function testNonStaticCallOfStaticClosure() + { + $this->macroable::macro('staticClosure', static function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable->staticClosure()); + } } class EmptyMacroable From e49e5982ff55574e1b1d43f9b838bc6efb0d965b Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:49:47 +0600 Subject: [PATCH 064/596] [13.x] Fix sum() docblock to include key parameter in callback signature (#59444) The sum() implementation was updated in #59322 to pass both value and key to the callback, but the docblock was not updated to reflect this. IDEs and static analysis tools show incorrect parameter hints. --- src/Illuminate/Collections/Enumerable.php | 2 +- src/Illuminate/Collections/Traits/EnumeratesValues.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 50bedff0cd16..0edd48b59c7b 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -1115,7 +1115,7 @@ public function sortKeysUsing(callable $callback); /** * Get the sum of the given values. * - * @param (callable(TValue): mixed)|string|null $callback + * @param (callable(TValue, TKey): mixed)|string|null $callback * @return mixed */ public function sum($callback = null); diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index d0f6102971ee..71160412ef90 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -572,7 +572,7 @@ public function percentage(callable $callback, int $precision = 2) * * @template TReturnType * - * @param (callable(TValue): TReturnType)|string|null $callback + * @param (callable(TValue, TKey): TReturnType)|string|null $callback * @return ($callback is callable ? TReturnType : mixed) */ public function sum($callback = null) From 9c888ac1f9d8f16124a651b578b915d8bd191634 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:31:52 +0600 Subject: [PATCH 065/596] [13.x] Add assertHasNoAttachments() method to Mailable (#59443) The Mailable class has assertHasAttachment(), assertHasAttachedData(), and assertHasAttachmentFromStorage() but no way to assert that a mailable has no attachments at all. This is useful when testing that certain emails are sent without attachments. --- src/Illuminate/Mail/Mailable.php | 22 ++++++++++++++++++++++ tests/Mail/MailMailableTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index a711923d8c37..92c55e17198e 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -1550,6 +1550,28 @@ public function assertSeeInOrderInText($strings) * @param array $options * @return $this */ + public function assertHasNoAttachments() + { + $this->renderForAssertions(); + + PHPUnit::assertEmpty( + $this->attachments, + 'Expected no attachments, but found ['.count($this->attachments).'] file attachment(s).' + ); + + PHPUnit::assertEmpty( + $this->rawAttachments, + 'Expected no attachments, but found ['.count($this->rawAttachments).'] raw data attachment(s).' + ); + + PHPUnit::assertEmpty( + $this->diskAttachments, + 'Expected no attachments, but found ['.count($this->diskAttachments).'] storage attachment(s).' + ); + + return $this; + } + public function assertHasAttachment($file, array $options = []) { $this->renderForAssertions(); diff --git a/tests/Mail/MailMailableTest.php b/tests/Mail/MailMailableTest.php index b55fa1be0dd6..92b00976d6b8 100644 --- a/tests/Mail/MailMailableTest.php +++ b/tests/Mail/MailMailableTest.php @@ -1019,6 +1019,36 @@ public function testItCanCheckForStorageBasedAttachments(): void $this->assertFalse($mailable->hasAttachmentFromStorageDisk('disk', '/path/to/foo.jpg', 'bar.jpg', ['mime' => 'text/html'])); } + public function testAssertHasNoAttachments(): void + { + $this->stubMailer(); + + $mailable = new class() extends Mailable + { + public function build() + { + // + } + }; + + $mailable->assertHasNoAttachments(); + + $mailableWithAttachment = new class() extends Mailable + { + public function build() + { + $this->attach('/path/to/foo.jpg'); + } + }; + + try { + $mailableWithAttachment->assertHasNoAttachments(); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertStringContainsString('Expected no attachments', $e->getMessage()); + } + } + public function testAssertHasAttachment(): void { $this->stubMailer(); From 930e42faffdd8632dde2945c90f9b630d5186656 Mon Sep 17 00:00:00 2001 From: Kevin Bui Date: Tue, 31 Mar 2026 00:32:27 +1100 Subject: [PATCH 066/596] [13.x] Add a driver method to the MailFake class (#59448) * Add a driver method to the MailFake class. * Remove an empty line. --- .../Support/Testing/Fakes/MailFake.php | 11 +++++++++ tests/Support/SupportTestingMailFakeTest.php | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Illuminate/Support/Testing/Fakes/MailFake.php b/src/Illuminate/Support/Testing/Fakes/MailFake.php index e3eb25b572c2..8be5000d4e4b 100644 --- a/src/Illuminate/Support/Testing/Fakes/MailFake.php +++ b/src/Illuminate/Support/Testing/Fakes/MailFake.php @@ -430,6 +430,17 @@ public function mailer($name = null) return $this; } + /** + * Get a mailer driver instance. + * + * @param string|null $driver + * @return \Illuminate\Contracts\Mail\Mailer + */ + public function driver($driver = null) + { + return $this->mailer($driver); + } + /** * Begin the process of mailing a mailable class instance. * diff --git a/tests/Support/SupportTestingMailFakeTest.php b/tests/Support/SupportTestingMailFakeTest.php index 3121c79fd0d3..547931f91507 100644 --- a/tests/Support/SupportTestingMailFakeTest.php +++ b/tests/Support/SupportTestingMailFakeTest.php @@ -410,6 +410,30 @@ public function testAssertMailer() return $mail->usesMailer('mailjet'); }); } + + public function testDriverMethod() + { + $this->fake->driver('ses')->to('taylor@laravel.com')->send($this->mailable); + + $this->fake->assertSent(MailableStub::class, function ($mail) { + return $mail->hasTo('taylor@laravel.com') && + $mail->usesMailer('ses'); + }); + + $this->fake->driver('sendgrid')->to('taylor@laravel.com')->queue($this->mailable); + + $this->fake->assertQueued(MailableStub::class, function ($mail) { + return $mail->hasTo('taylor@laravel.com') && + $mail->usesMailer('sendgrid'); + }); + + $this->fake->driver('mailjet')->to('taylor@laravel.com')->queue($this->mailable); + + $this->fake->assertQueued(MailableStub::class, function ($mail) { + return $mail->hasTo('taylor@laravel.com') && + $mail->usesMailer('mailjet'); + }); + } } class MailableStub extends Mailable From 1da2839a083f2934583c7e6b45c5b0ebe12806b6 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:33:13 +0600 Subject: [PATCH 067/596] [13.x] Cache getLockForPopping() result in DatabaseQueue (#59435) The lock type is determined by database engine and version, which never change during a queue worker's lifetime. Cache the result to avoid repeated PDO attribute lookups, Stringable allocations, regex parsing, and version_compare calls on every job pop. Fixes #59350 --- src/Illuminate/Queue/DatabaseQueue.php | 17 ++++++++++++++--- tests/Queue/QueueDatabaseQueueUnitTest.php | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index ba72e6170ed5..162072158232 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -44,6 +44,13 @@ class DatabaseQueue extends Queue implements QueueContract, ClearableQueue */ protected $retryAfter = 60; + /** + * The cached lock type for popping jobs. + * + * @var string|bool|null + */ + protected $lockForPopping = null; + /** * Create a new database queue instance. * @@ -338,6 +345,10 @@ protected function getNextAvailableJob($queue) */ protected function getLockForPopping() { + if ($this->lockForPopping !== null) { + return $this->lockForPopping; + } + $databaseEngine = $this->database->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME); $databaseVersion = $this->database->getConfig('version') ?? $this->database->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); @@ -354,14 +365,14 @@ protected function getLockForPopping() ($databaseEngine === 'pgsql' && version_compare($databaseVersion, '9.5', '>=')) || ($databaseEngine === 'vitess' && version_compare($databaseVersion, '19.0', '>=')) ) { - return 'FOR UPDATE SKIP LOCKED'; + return $this->lockForPopping = 'FOR UPDATE SKIP LOCKED'; } if ($databaseEngine === 'sqlsrv') { - return 'with(rowlock,updlock,readpast)'; + return $this->lockForPopping = 'with(rowlock,updlock,readpast)'; } - return true; + return $this->lockForPopping = true; } /** diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 8ca3fa06ca3f..839e6db12344 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -196,6 +196,27 @@ public function testBuildDatabaseRecordWithPayloadAtTheEnd() $this->assertArrayHasKey('payload', $record); $this->assertArrayHasKey('payload', array_slice($record, -1, 1, true)); } + + public function testGetLockForPoppingIsCached() + { + $database = m::mock(Connection::class); + $queue = new DatabaseQueue($database, 'table', 'default'); + + $pdo = m::mock(\PDO::class); + $pdo->shouldReceive('getAttribute')->with(\PDO::ATTR_DRIVER_NAME)->once()->andReturn('mysql'); + $pdo->shouldReceive('getAttribute')->with(\PDO::ATTR_SERVER_VERSION)->once()->andReturn('8.0.36'); + + $database->shouldReceive('getPdo')->andReturn($pdo); + $database->shouldReceive('getConfig')->with('version')->andReturn(null); + + $method = new \ReflectionMethod($queue, 'getLockForPopping'); + + $result1 = $method->invoke($queue); + $result2 = $method->invoke($queue); + + $this->assertSame('FOR UPDATE SKIP LOCKED', $result1); + $this->assertSame($result1, $result2); + } } class MyTestJob From 6e57d4c55fe624c20feee5a897dd8e8be9ba933d Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 30 Mar 2026 13:59:12 -0500 Subject: [PATCH 068/596] prefer `new Collection()` over `collect()` helper (#59453) - slightly better performance - shorter call stack - consistency continuation of !55059 --- .../CronExpressionTimezoneConverter.php | 5 +++-- .../Scheduling/ScheduleListCommand.php | 22 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php b/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php index 04ee0b88279f..d15b8185ad79 100644 --- a/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php +++ b/src/Illuminate/Console/Scheduling/CronExpressionTimezoneConverter.php @@ -4,6 +4,7 @@ use DateTimeZone; use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; class CronExpressionTimezoneConverter { @@ -145,7 +146,7 @@ protected static function shiftAndGroup($field, $offset, $mod, $min = 0) $groups[$carry][] = $new; } - return collect($groups)->map(function ($values) { + return (new Collection($groups))->map(function ($values) { sort($values); return implode(',', $values); @@ -167,7 +168,7 @@ protected static function shiftField($field, $offset, $mod, $min = 0) return $field; } - $shifted = collect(explode(',', $field)) + $shifted = (new Collection(explode(',', $field))) ->map(fn ($v) => (((int) $v + $offset - $min) % $mod + $mod) % $mod + $min) ->sort(); diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index c1fa4783053b..bc42dfcf20cc 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -96,7 +96,7 @@ protected function displayJson(Collection $events, DateTimeZone $timezone) } } - return collect(CronExpressionTimezoneConverter::forEvent($event, $timezone))->map(fn ($expression) => [ + return (new Collection(CronExpressionTimezoneConverter::forEvent($event, $timezone)))->map(fn ($expression) => [ 'expression' => $expression, 'command' => $command, 'description' => $event->description ?? null, @@ -126,13 +126,13 @@ protected function displayForCli(Collection $events, DateTimeZone $timezone) $repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events); $events = $events->flatMap(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) { - return collect(CronExpressionTimezoneConverter::forEvent($event, $timezone))->map( - fn ($expression) => $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone, $expression) + return (new Collection(CronExpressionTimezoneConverter::forEvent($event, $timezone)))->map( + fn ($expression) => $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone, $expression), ); }); $this->line( - $events->flatten()->filter()->prepend('')->push('')->toArray() + $events->flatten()->filter()->prepend('')->push('')->toArray(), ); } @@ -144,7 +144,7 @@ protected function displayForCli(Collection $events, DateTimeZone $timezone) */ private function getCronExpressionSpacing($events, DateTimeZone $timezone) { - $rows = $events->flatMap(fn ($event) => collect(CronExpressionTimezoneConverter::forEvent($event, $timezone)) + $rows = $events->flatMap(fn ($event) => (new Collection(CronExpressionTimezoneConverter::forEvent($event, $timezone))) ->map(fn ($expression) => array_map(mb_strlen(...), preg_split("/\s+/", $expression)))); return (new Collection($rows[0] ?? []))->keys()->map(fn ($key) => $rows->max($key))->all(); @@ -207,7 +207,7 @@ private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatEx $hasMutex = $event->mutex->exists($event) ? 'Has Mutex › ' : ''; $dots = str_repeat('.', max( - $terminalWidth - mb_strwidth($expression.$repeatExpression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0 + $terminalWidth - mb_strwidth($expression.$repeatExpression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0, )); // Highlight the parameters... @@ -221,12 +221,12 @@ private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatEx $dots, $hasMutex, $nextDueDateLabel, - $nextDueDate + $nextDueDate, ), $this->output->isVerbose() && mb_strlen($description) > 1 ? sprintf( ' %s%s %s', str_repeat(' ', mb_strlen($expression) + 2), '⇁', - $description + $description, ) : '']; } @@ -279,7 +279,7 @@ private function getNextDueDateForEvent($event, DateTimeZone $timezone) $nextDueDate = Carbon::instance( (new CronExpression($event->expression)) ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) - ->setTimezone($timezone) + ->setTimezone($timezone), ); if (! $event->isRepeatable()) { @@ -289,7 +289,7 @@ private function getNextDueDateForEvent($event, DateTimeZone $timezone) $previousDueDate = Carbon::instance( (new CronExpression($event->expression)) ->getPreviousRunDate(Carbon::now()->setTimezone($event->timezone), allowCurrentDate: true) - ->setTimezone($timezone) + ->setTimezone($timezone), ); $now = Carbon::now()->setTimezone($event->timezone); @@ -335,7 +335,7 @@ private function getClosureLocation(CallbackEvent $event) return sprintf( '%s:%s', str_replace($this->laravel->basePath().DIRECTORY_SEPARATOR, '', $function->getFileName() ?: ''), - $function->getStartLine() + $function->getStartLine(), ); } From ddb3414cf2f0bf705ca0f7f1ae94b12342174692 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 30 Mar 2026 14:00:54 -0500 Subject: [PATCH 069/596] remove unnecessary `array_flip()` calls (#59452) the `class_uses_recursive()` function returns an array where both the key and the value are the fully qualified class name, so an `array_flip` returns exactly what it was given. ```php $results = class_uses_recursive(User::class); dd($results === array_flip($results)); //true ``` --- src/Illuminate/Database/Seeder.php | 2 +- .../Testing/Concerns/InteractsWithTestCaseLifecycle.php | 2 +- src/Illuminate/Foundation/Testing/TestCase.php | 2 +- src/Illuminate/Testing/Concerns/TestDatabases.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index cac50afb579d..557d10d4cfef 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -184,7 +184,7 @@ public function __invoke(array $parameters = []) ? $this->container->call([$this, 'run'], $parameters) : $this->run(...$parameters); - $uses = array_flip(class_uses_recursive(static::class)); + $uses = class_uses_recursive(static::class); if (isset($uses[WithoutModelEvents::class])) { $callback = $this->withoutModelEvents($callback); diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 1cb79428978f..ae592732602a 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -217,7 +217,7 @@ protected function tearDownTheTestEnvironment(): void */ protected function setUpTraits() { - $uses = $this->traitsUsedByTest ?? array_flip(class_uses_recursive(static::class)); + $uses = $this->traitsUsedByTest ?? class_uses_recursive(static::class); if (isset($uses[RefreshDatabase::class])) { $this->refreshDatabase(); diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 0b5acfd37662..57100d43e732 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -36,7 +36,7 @@ public function createApplication() { $app = require Application::inferBasePath().'/bootstrap/app.php'; - $this->traitsUsedByTest = array_flip(class_uses_recursive(static::class)); + $this->traitsUsedByTest = class_uses_recursive(static::class); if (isset(CachedState::$cachedConfig) && isset($this->traitsUsedByTest[WithCachedConfig::class])) { diff --git a/src/Illuminate/Testing/Concerns/TestDatabases.php b/src/Illuminate/Testing/Concerns/TestDatabases.php index a273ed1299f4..284ce135d4f4 100644 --- a/src/Illuminate/Testing/Concerns/TestDatabases.php +++ b/src/Illuminate/Testing/Concerns/TestDatabases.php @@ -44,7 +44,7 @@ protected function bootTestDatabase() }); ParallelTesting::setUpTestCase(function ($testCase) { - $uses = array_flip(class_uses_recursive(get_class($testCase))); + $uses = class_uses_recursive(get_class($testCase)); $databaseTraits = [ Testing\DatabaseMigrations::class, From 40c69209aa5bfc6994b430fb2fd3c0df85d27b6a Mon Sep 17 00:00:00 2001 From: Felix Bernhard Date: Mon, 30 Mar 2026 21:05:19 +0200 Subject: [PATCH 070/596] fix macros with static closures (#59449) * support non-static calls of static closures * add tests for all static/non-static combinations (cherry picked from commit d8d65fdfe91f40d50c3c12645e2c4d4e7ee9a221) --- src/Illuminate/Macroable/Traits/Macroable.php | 8 ++++- tests/Support/SupportMacroableTest.php | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Macroable/Traits/Macroable.php b/src/Illuminate/Macroable/Traits/Macroable.php index 5490f1ea2b13..2ee06e177062 100644 --- a/src/Illuminate/Macroable/Traits/Macroable.php +++ b/src/Illuminate/Macroable/Traits/Macroable.php @@ -6,6 +6,8 @@ use Closure; use ReflectionClass; use ReflectionMethod; +use RuntimeException; +use Throwable; trait Macroable { @@ -120,7 +122,11 @@ public function __call($method, $parameters) $macro = static::$macros[$method]; if ($macro instanceof Closure) { - $macro = $macro->bindTo($this, static::class); + try { + $macro = $macro->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $macro = $macro->bindTo(null, static::class); + } } return $macro(...$parameters); diff --git a/tests/Support/SupportMacroableTest.php b/tests/Support/SupportMacroableTest.php index 78864c76b57c..7d89779e1fe8 100644 --- a/tests/Support/SupportMacroableTest.php +++ b/tests/Support/SupportMacroableTest.php @@ -159,6 +159,42 @@ public function testMethodConflictDoesNotThrowException() $this->assertSame('newMethod', $this->macroable::existingMethod()); } + + public function testStaticCallOfNonStaticClosure() + { + $this->macroable::macro('nonStaticClosure', function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable::nonStaticClosure()); + } + + public function testNonStaticCallOfNonStaticClosure() + { + $this->macroable::macro('nonStaticClosure', function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable->nonStaticClosure()); + } + + public function testStaticCallOfStaticClosure() + { + $this->macroable::macro('staticClosure', static function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable::staticClosure()); + } + + public function testNonStaticCallOfStaticClosure() + { + $this->macroable::macro('staticClosure', static function () { + return 'Taylor'; + }); + + $this->assertSame('Taylor', $this->macroable->staticClosure()); + } } class EmptyMacroable From 6ae99c953372887e265235e8270575fefceee3ff Mon Sep 17 00:00:00 2001 From: Len Woodward Date: Mon, 30 Mar 2026 12:06:46 -0700 Subject: [PATCH 071/596] [13.x] Make Collection methods compatible with extended subclass constructors (#59455) Co-authored-by: Claude Opus 4.6 (1M context) --- src/Illuminate/Collections/Collection.php | 185 ++++++++++-------- src/Illuminate/Collections/LazyCollection.php | 17 +- .../Collections/Traits/EnumeratesValues.php | 26 +-- tests/Support/SupportCollectionTest.php | 163 +++++++++++++++ 4 files changed, 288 insertions(+), 103 deletions(-) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 39e5ba543a85..9ff0b9a6c520 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -44,6 +44,17 @@ public function __construct($items = []) $this->items = $this->getArrayableItems($items); } + /** + * Create a new instance of the collection. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + * @return static + */ + protected function newInstance($items = []) + { + return new static($items); + } + /** * Create a collection with the given range. * @@ -52,9 +63,9 @@ public function __construct($items = []) * @param int $step * @return static */ - public static function range($from, $to, $step = 1) + public static function range($from, $to, $step = 1, ...$args) { - return new static(range($from, $to, $step)); + return new static(range($from, $to, $step), ...$args); } /** @@ -101,9 +112,9 @@ public function median($key = null) return $values->get($middle); } - return (new static([ + return $this->newInstance([ $values->get($middle - 1), $values->get($middle), - ]))->average(); + ])->average(); } /** @@ -120,7 +131,7 @@ public function mode($key = null) $collection = isset($key) ? $this->pluck($key) : $this; - $counts = new static; + $counts = $this->newInstance(); $collection->each(fn ($value) => $counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1); @@ -139,7 +150,7 @@ public function mode($key = null) */ public function collapse() { - return new static(Arr::collapse($this->items)); + return $this->newInstance(Arr::collapse($this->items)); } /** @@ -150,7 +161,7 @@ public function collapse() public function collapseWithKeys() { if (! $this->items) { - return new static; + return $this->newInstance(); } $results = []; @@ -166,10 +177,10 @@ public function collapseWithKeys() } if (! $results) { - return new static; + return $this->newInstance(); } - return new static(array_replace(...$results)); + return $this->newInstance(array_replace(...$results)); } /** @@ -250,7 +261,7 @@ public function doesntContainStrict($key, $operator = null, $value = null) */ public function crossJoin(...$lists) { - return new static(Arr::crossJoin( + return $this->newInstance(Arr::crossJoin( $this->items, ...array_map($this->getArrayableItems(...), $lists) )); } @@ -263,7 +274,7 @@ public function crossJoin(...$lists) */ public function diff($items) { - return new static(array_diff($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_diff($this->items, $this->getArrayableItems($items))); } /** @@ -275,7 +286,7 @@ public function diff($items) */ public function diffUsing($items, callable $callback) { - return new static(array_udiff($this->items, $this->getArrayableItems($items), $callback)); + return $this->newInstance(array_udiff($this->items, $this->getArrayableItems($items), $callback)); } /** @@ -286,7 +297,7 @@ public function diffUsing($items, callable $callback) */ public function diffAssoc($items) { - return new static(array_diff_assoc($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_diff_assoc($this->items, $this->getArrayableItems($items))); } /** @@ -298,7 +309,7 @@ public function diffAssoc($items) */ public function diffAssocUsing($items, callable $callback) { - return new static(array_diff_uassoc($this->items, $this->getArrayableItems($items), $callback)); + return $this->newInstance(array_diff_uassoc($this->items, $this->getArrayableItems($items), $callback)); } /** @@ -309,7 +320,7 @@ public function diffAssocUsing($items, callable $callback) */ public function diffKeys($items) { - return new static(array_diff_key($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_diff_key($this->items, $this->getArrayableItems($items))); } /** @@ -321,7 +332,7 @@ public function diffKeys($items) */ public function diffKeysUsing($items, callable $callback) { - return new static(array_diff_ukey($this->items, $this->getArrayableItems($items), $callback)); + return $this->newInstance(array_diff_ukey($this->items, $this->getArrayableItems($items), $callback)); } /** @@ -341,7 +352,7 @@ public function duplicates($callback = null, $strict = false) $compare = $this->duplicateComparator($strict); - $duplicates = new static; + $duplicates = $this->newInstance(); foreach ($items as $key => $value) { if ($uniqueItems->isNotEmpty() && $compare($value, $uniqueItems->first())) { @@ -391,7 +402,7 @@ protected function duplicateComparator($strict) public function except($keys) { if (is_null($keys)) { - return new static($this->items); + return $this->newInstance($this->items); } if ($keys instanceof Enumerable) { @@ -400,7 +411,7 @@ public function except($keys) $keys = func_get_args(); } - return new static(Arr::except($this->items, $keys)); + return $this->newInstance(Arr::except($this->items, $keys)); } /** @@ -412,10 +423,10 @@ public function except($keys) public function filter(?callable $callback = null) { if ($callback) { - return new static(Arr::where($this->items, $callback)); + return $this->newInstance(Arr::where($this->items, $callback)); } - return new static(array_filter($this->items)); + return $this->newInstance(array_filter($this->items)); } /** @@ -440,7 +451,7 @@ public function first(?callable $callback = null, $default = null) */ public function flatten($depth = INF) { - return new static(Arr::flatten($this->items, $depth)); + return $this->newInstance(Arr::flatten($this->items, $depth)); } /** @@ -450,7 +461,7 @@ public function flatten($depth = INF) */ public function flip() { - return new static(array_flip($this->items)); + return $this->newInstance(array_flip($this->items)); } /** @@ -540,14 +551,14 @@ public function groupBy($groupBy, $preserveKeys = false) }; if (! array_key_exists($groupKey, $results)) { - $results[$groupKey] = new static; + $results[$groupKey] = $this->newInstance(); } $results[$groupKey]->offsetSet($preserveKeys ? $key : null, $value); } } - $result = new static($results); + $result = $this->newInstance($results); if (! empty($nextGroups)) { return $result->map->groupBy($nextGroups, $preserveKeys); @@ -580,7 +591,7 @@ public function keyBy($keyBy) $results[$resolvedKey] = $item; } - return new static($results); + return $this->newInstance($results); } /** @@ -643,7 +654,7 @@ public function implode($value, $glue = null) */ public function intersect($items) { - return new static(array_intersect($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_intersect($this->items, $this->getArrayableItems($items))); } /** @@ -655,7 +666,7 @@ public function intersect($items) */ public function intersectUsing($items, callable $callback) { - return new static(array_uintersect($this->items, $this->getArrayableItems($items), $callback)); + return $this->newInstance(array_uintersect($this->items, $this->getArrayableItems($items), $callback)); } /** @@ -666,7 +677,7 @@ public function intersectUsing($items, callable $callback) */ public function intersectAssoc($items) { - return new static(array_intersect_assoc($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_intersect_assoc($this->items, $this->getArrayableItems($items))); } /** @@ -678,7 +689,7 @@ public function intersectAssoc($items) */ public function intersectAssocUsing($items, callable $callback) { - return new static(array_intersect_uassoc($this->items, $this->getArrayableItems($items), $callback)); + return $this->newInstance(array_intersect_uassoc($this->items, $this->getArrayableItems($items), $callback)); } /** @@ -689,7 +700,7 @@ public function intersectAssocUsing($items, callable $callback) */ public function intersectByKeys($items) { - return new static(array_intersect_key( + return $this->newInstance(array_intersect_key( $this->items, $this->getArrayableItems($items) )); } @@ -759,7 +770,7 @@ public function join($glue, $finalGlue = '') return $this->last(); } - $collection = new static($this->items); + $collection = $this->newInstance($this->items); $finalItem = $collection->pop(); @@ -773,7 +784,7 @@ public function join($glue, $finalGlue = '') */ public function keys() { - return new static(array_keys($this->items)); + return $this->newInstance(array_keys($this->items)); } /** @@ -799,7 +810,7 @@ public function last(?callable $callback = null, $default = null) */ public function pluck($value, $key = null) { - return new static(Arr::pluck($this->items, $value, $key)); + return $this->newInstance(Arr::pluck($this->items, $value, $key)); } /** @@ -812,7 +823,7 @@ public function pluck($value, $key = null) */ public function map(callable $callback) { - return new static(Arr::map($this->items, $callback)); + return $this->newInstance(Arr::map($this->items, $callback)); } /** @@ -844,7 +855,7 @@ public function mapToDictionary(callable $callback) $dictionary[$key][] = $value; } - return new static($dictionary); + return $this->newInstance($dictionary); } /** @@ -860,7 +871,7 @@ public function mapToDictionary(callable $callback) */ public function mapWithKeys(callable $callback) { - return new static(Arr::mapWithKeys($this->items, $callback)); + return $this->newInstance(Arr::mapWithKeys($this->items, $callback)); } /** @@ -873,7 +884,7 @@ public function mapWithKeys(callable $callback) */ public function merge($items) { - return new static(array_merge($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_merge($this->items, $this->getArrayableItems($items))); } /** @@ -886,7 +897,7 @@ public function merge($items) */ public function mergeRecursive($items) { - return new static(array_merge_recursive($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_merge_recursive($this->items, $this->getArrayableItems($items))); } /** @@ -897,7 +908,7 @@ public function mergeRecursive($items) */ public function multiply(int $multiplier) { - $new = new static; + $new = $this->newInstance(); for ($i = 0; $i < $multiplier; $i++) { $new->push(...$this->items); @@ -916,7 +927,7 @@ public function multiply(int $multiplier) */ public function combine($values) { - return new static(array_combine($this->all(), $this->getArrayableItems($values))); + return $this->newInstance(array_combine($this->all(), $this->getArrayableItems($values))); } /** @@ -927,7 +938,7 @@ public function combine($values) */ public function union($items) { - return new static($this->items + $this->getArrayableItems($items)); + return $this->newInstance($this->items + $this->getArrayableItems($items)); } /** @@ -957,7 +968,7 @@ public function nth($step, $offset = 0) $position++; } - return new static($new); + return $this->newInstance($new); } /** @@ -969,7 +980,7 @@ public function nth($step, $offset = 0) public function only($keys) { if (is_null($keys)) { - return new static($this->items); + return $this->newInstance($this->items); } if ($keys instanceof Enumerable) { @@ -978,7 +989,7 @@ public function only($keys) $keys = is_array($keys) ? $keys : func_get_args(); - return new static(Arr::only($this->items, $keys)); + return $this->newInstance(Arr::only($this->items, $keys)); } /** @@ -990,7 +1001,7 @@ public function only($keys) public function select($keys) { if (is_null($keys)) { - return new static($this->items); + return $this->newInstance($this->items); } if ($keys instanceof Enumerable) { @@ -999,7 +1010,7 @@ public function select($keys) $keys = is_array($keys) ? $keys : func_get_args(); - return new static(Arr::select($this->items, $keys)); + return $this->newInstance(Arr::select($this->items, $keys)); } /** @@ -1011,7 +1022,7 @@ public function select($keys) public function pop($count = 1) { if ($count < 1) { - return new static; + return $this->newInstance(); } if ($count === 1) { @@ -1019,7 +1030,7 @@ public function pop($count = 1) } if ($this->isEmpty()) { - return new static; + return $this->newInstance(); } $results = []; @@ -1030,7 +1041,7 @@ public function pop($count = 1) $results[] = array_pop($this->items); } - return new static($results); + return $this->newInstance($results); } /** @@ -1086,7 +1097,7 @@ public function unshift(...$values) */ public function concat($source) { - $result = new static($this); + $result = $this->newInstance($this); foreach ($source as $item) { $result->push($item); @@ -1139,10 +1150,10 @@ public function random($number = null, $preserveKeys = false) } if (is_callable($number)) { - return new static(Arr::random($this->items, $number($this), $preserveKeys)); + return $this->newInstance(Arr::random($this->items, $number($this), $preserveKeys)); } - return new static(Arr::random($this->items, $number, $preserveKeys)); + return $this->newInstance(Arr::random($this->items, $number, $preserveKeys)); } /** @@ -1153,7 +1164,7 @@ public function random($number = null, $preserveKeys = false) */ public function replace($items) { - return new static(array_replace($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_replace($this->items, $this->getArrayableItems($items))); } /** @@ -1164,7 +1175,7 @@ public function replace($items) */ public function replaceRecursive($items) { - return new static(array_replace_recursive($this->items, $this->getArrayableItems($items))); + return $this->newInstance(array_replace_recursive($this->items, $this->getArrayableItems($items))); } /** @@ -1174,7 +1185,7 @@ public function replaceRecursive($items) */ public function reverse() { - return new static(array_reverse($this->items, true)); + return $this->newInstance(array_reverse($this->items, true)); } /** @@ -1260,7 +1271,7 @@ public function shift($count = 1) } if ($count === 0) { - return new static; + return $this->newInstance(); } if ($count === 1) { @@ -1275,7 +1286,7 @@ public function shift($count = 1) $results[] = array_shift($this->items); } - return new static($results); + return $this->newInstance($results); } /** @@ -1285,7 +1296,7 @@ public function shift($count = 1) */ public function shuffle() { - return new static(Arr::shuffle($this->items)); + return $this->newInstance(Arr::shuffle($this->items)); } /** @@ -1329,7 +1340,7 @@ public function skip($count) */ public function skipUntil($value) { - return new static($this->lazy()->skipUntil($value)->all()); + return $this->newInstance($this->lazy()->skipUntil($value)->all()); } /** @@ -1340,7 +1351,7 @@ public function skipUntil($value) */ public function skipWhile($value) { - return new static($this->lazy()->skipWhile($value)->all()); + return $this->newInstance($this->lazy()->skipWhile($value)->all()); } /** @@ -1352,7 +1363,7 @@ public function skipWhile($value) */ public function slice($offset, $length = null) { - return new static(array_slice($this->items, $offset, $length, true)); + return $this->newInstance(array_slice($this->items, $offset, $length, true)); } /** @@ -1370,10 +1381,10 @@ public function split($numberOfGroups) } if ($this->isEmpty()) { - return new static; + return $this->newInstance(); } - $groups = new static; + $groups = $this->newInstance(); $groupSize = floor($this->count() / $numberOfGroups); @@ -1389,7 +1400,7 @@ public function split($numberOfGroups) } if ($size) { - $groups->push(new static(array_slice($this->items, $start, $size))); + $groups->push($this->newInstance(array_slice($this->items, $start, $size))); $start += $size; } @@ -1504,16 +1515,16 @@ public function firstOrFail($key = null, $operator = null, $value = null) public function chunk($size, $preserveKeys = true) { if ($size <= 0) { - return new static; + return $this->newInstance(); } $chunks = []; foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { - $chunks[] = new static($chunk); + $chunks[] = $this->newInstance($chunk); } - return new static($chunks); + return $this->newInstance($chunks); } /** @@ -1524,7 +1535,7 @@ public function chunk($size, $preserveKeys = true) */ public function chunkWhile(callable $callback) { - return new static( + return $this->newInstance( $this->lazy()->chunkWhile($callback)->mapInto(static::class) ); } @@ -1543,7 +1554,7 @@ public function sort($callback = null) ? uasort($items, $callback) : asort($items, $callback ?? SORT_REGULAR); - return new static($items); + return $this->newInstance($items); } /** @@ -1558,7 +1569,7 @@ public function sortDesc($options = SORT_REGULAR) arsort($items, $options); - return new static($items); + return $this->newInstance($items); } /** @@ -1596,7 +1607,7 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) $results[$key] = $this->items[$key]; } - return new static($results); + return $this->newInstance($results); } /** @@ -1653,7 +1664,7 @@ protected function sortByMany(array $comparisons = [], int $options = SORT_REGUL } }); - return new static($items); + return $this->newInstance($items); } /** @@ -1691,7 +1702,7 @@ public function sortKeys($options = SORT_REGULAR, $descending = false) $descending ? krsort($items, $options) : ksort($items, $options); - return new static($items); + return $this->newInstance($items); } /** @@ -1717,7 +1728,7 @@ public function sortKeysUsing(callable $callback) uksort($items, $callback); - return new static($items); + return $this->newInstance($items); } /** @@ -1731,10 +1742,10 @@ public function sortKeysUsing(callable $callback) public function splice($offset, $length = null, $replacement = []) { if (func_num_args() === 1) { - return new static(array_splice($this->items, $offset)); + return $this->newInstance(array_splice($this->items, $offset)); } - return new static(array_splice($this->items, $offset, $length, $this->getArrayableItems($replacement))); + return $this->newInstance(array_splice($this->items, $offset, $length, $this->getArrayableItems($replacement))); } /** @@ -1760,7 +1771,7 @@ public function take($limit) */ public function takeUntil($value) { - return new static($this->lazy()->takeUntil($value)->all()); + return $this->newInstance($this->lazy()->takeUntil($value)->all()); } /** @@ -1771,7 +1782,7 @@ public function takeUntil($value) */ public function takeWhile($value) { - return new static($this->lazy()->takeWhile($value)->all()); + return $this->newInstance($this->lazy()->takeWhile($value)->all()); } /** @@ -1799,7 +1810,7 @@ public function transform(callable $callback) */ public function dot($depth = INF) { - return new static(Arr::dot($this->all(), '', $depth)); + return $this->newInstance(Arr::dot($this->all(), '', $depth)); } /** @@ -1809,7 +1820,7 @@ public function dot($depth = INF) */ public function undot() { - return new static(Arr::undot($this->all())); + return $this->newInstance(Arr::undot($this->all())); } /** @@ -1822,7 +1833,7 @@ public function undot() public function unique($key = null, $strict = false) { if (is_null($key) && $strict === false) { - return new static(array_unique($this->items, SORT_REGULAR)); + return $this->newInstance(array_unique($this->items, SORT_REGULAR)); } $callback = $this->valueRetriever($key); @@ -1845,7 +1856,7 @@ public function unique($key = null, $strict = false) */ public function values() { - return new static(array_values($this->items)); + return $this->newInstance(array_values($this->items)); } /** @@ -1863,9 +1874,9 @@ public function zip($items) { $arrayableItems = array_map(fn ($items) => $this->getArrayableItems($items), func_get_args()); - $params = array_merge([fn () => new static(func_get_args()), $this->items], $arrayableItems); + $params = array_merge([fn () => $this->newInstance(func_get_args()), $this->items], $arrayableItems); - return new static(array_map(...$params)); + return $this->newInstance(array_map(...$params)); } /** @@ -1879,7 +1890,7 @@ public function zip($items) */ public function pad($size, $value) { - return new static(array_pad($this->items, $size, $value)); + return $this->newInstance(array_pad($this->items, $size, $value)); } /** @@ -1908,7 +1919,7 @@ public function count(): int #[\Override] public function countBy($countBy = null) { - return new static($this->lazy()->countBy($countBy)->all()); + return $this->newInstance($this->lazy()->countBy($countBy)->all()); } /** diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 1c56f101112f..dc65a68d677e 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -59,6 +59,17 @@ public function __construct($source = null) } } + /** + * Create a new instance of the collection. + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|(Closure(): \Generator)|self|array|null $items + * @return static + */ + protected function newInstance($items = []) + { + return new static($items); + } + /** * Create a new collection instance if the value isn't one already. * @@ -68,9 +79,9 @@ public function __construct($source = null) * @param \Illuminate\Contracts\Support\Arrayable|iterable|(Closure(): \Generator)|self|array|null $items * @return static */ - public static function make($items = []) + public static function make($items = [], ...$args) { - return new static($items); + return new static($items, ...$args); } /** @@ -83,7 +94,7 @@ public static function make($items = []) * * @throws \InvalidArgumentException */ - public static function range($from, $to, $step = 1) + public static function range($from, $to, $step = 1, ...$args) { if ($step == 0) { throw new InvalidArgumentException('Step value cannot be zero.'); diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index 71160412ef90..2b950148273a 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -116,9 +116,9 @@ trait EnumeratesValues * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items * @return static */ - public static function make($items = []) + public static function make($items = [], ...$args) { - return new static($items); + return new static($items, ...$args); } /** @@ -129,11 +129,11 @@ public static function make($items = []) * @param iterable|TWrapValue $value * @return static */ - public static function wrap($value) + public static function wrap($value, ...$args) { return $value instanceof Enumerable - ? new static($value) - : new static(Arr::wrap($value)); + ? new static($value, ...$args) + : new static(Arr::wrap($value), ...$args); } /** @@ -155,9 +155,9 @@ public static function unwrap($value) * * @return static */ - public static function empty() + public static function empty(...$args) { - return new static([]); + return new static([], ...$args); } /** @@ -169,13 +169,13 @@ public static function empty() * @param (callable(int): TTimesValue)|null $callback * @return static */ - public static function times($number, ?callable $callback = null) + public static function times($number, ?callable $callback = null, ...$args) { if ($number < 1) { - return new static; + return new static([], ...$args); } - return static::range(1, $number) + return static::range(1, $number, 1, ...$args) ->unless($callback == null) ->map($callback); } @@ -188,9 +188,9 @@ public static function times($number, ?callable $callback = null) * @param int $flags * @return static */ - public static function fromJson($json, $depth = 512, $flags = 0) + public static function fromJson($json, $depth = 512, $flags = 0, ...$args) { - return new static(json_decode($json, true, $depth, $flags)); + return new static(json_decode($json, true, $depth, $flags), ...$args); } /** @@ -545,7 +545,7 @@ public function partition($key, $operator = null, $value = null) [$passed, $failed] = Arr::partition($this->getIterator(), $callback); - return new static([new static($passed), new static($failed)]); + return $this->newInstance([$this->newInstance($passed), $this->newInstance($failed)]); } /** diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index bc007ef8eee2..3506655b6868 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -6052,6 +6052,153 @@ public function testPercentageReturnsNullForEmptyCollections($collection) $this->assertNull($collection->percentage(fn ($value) => $value === 1)); } + public function testNewInstanceIsUsedByCollectionMethods() + { + $collection = new TestCollectionWithExtraState([1, 2, 3, 4, 5], 'my-tag'); + + // filter + $filtered = $collection->filter(fn ($v) => $v > 3); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $filtered); + $this->assertSame('my-tag', $filtered->tag); + $this->assertSame([3 => 4, 4 => 5], $filtered->all()); + + // filter returning empty + $empty = $collection->filter(fn ($v) => $v > 100); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $empty); + $this->assertSame('my-tag', $empty->tag); + $this->assertEmpty($empty->all()); + + // reject + $rejected = $collection->reject(fn ($v) => $v <= 2); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $rejected); + $this->assertSame('my-tag', $rejected->tag); + + // map + $mapped = $collection->map(fn ($v) => $v * 2); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $mapped); + $this->assertSame('my-tag', $mapped->tag); + $this->assertSame([2, 4, 6, 8, 10], $mapped->all()); + + // values + $values = $filtered->values(); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $values); + $this->assertSame('my-tag', $values->tag); + + // unique + $duped = new TestCollectionWithExtraState([1, 1, 2, 2, 3], 'u-tag'); + $unique = $duped->unique(); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $unique); + $this->assertSame('u-tag', $unique->tag); + + // keys + $keys = $collection->keys(); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $keys); + $this->assertSame('my-tag', $keys->tag); + + // sort + $sorted = $collection->sort(); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $sorted); + $this->assertSame('my-tag', $sorted->tag); + + // slice + $sliced = $collection->slice(1, 2); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $sliced); + $this->assertSame('my-tag', $sliced->tag); + + // chunk + $chunks = $collection->chunk(2); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $chunks); + $this->assertSame('my-tag', $chunks->tag); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $chunks->first()); + $this->assertSame('my-tag', $chunks->first()->tag); + + // merge + $merged = $collection->merge([6, 7]); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $merged); + $this->assertSame('my-tag', $merged->tag); + + // diff + $diff = $collection->diff([1, 2]); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $diff); + $this->assertSame('my-tag', $diff->tag); + + // partition + [$pass, $fail] = $collection->partition(fn ($v) => $v > 3); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $pass); + $this->assertSame('my-tag', $pass->tag); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $fail); + $this->assertSame('my-tag', $fail->tag); + + // pluck (with associative data) + $assoc = new TestCollectionWithExtraState([ + ['name' => 'Taylor'], ['name' => 'Nuno'], + ], 'p-tag'); + $plucked = $assoc->pluck('name'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $plucked); + $this->assertSame('p-tag', $plucked->tag); + + // reverse + $reversed = $collection->reverse(); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $reversed); + $this->assertSame('my-tag', $reversed->tag); + + // flatten + $nested = new TestCollectionWithExtraState([[1, 2], [3, 4]], 'f-tag'); + $flat = $nested->flatten(); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $flat); + $this->assertSame('f-tag', $flat->tag); + + // pad + $padded = $collection->pad(7, 0); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $padded); + $this->assertSame('my-tag', $padded->tag); + } + + public function testStaticFactoryMethodsForwardExtraArguments() + { + // make + $made = TestCollectionWithExtraState::make([1, 2, 3], 'make-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $made); + $this->assertSame('make-tag', $made->tag); + $this->assertSame([1, 2, 3], $made->all()); + + // wrap + $wrapped = TestCollectionWithExtraState::wrap([4, 5], 'wrap-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $wrapped); + $this->assertSame('wrap-tag', $wrapped->tag); + $this->assertSame([4, 5], $wrapped->all()); + + // empty + $empty = TestCollectionWithExtraState::empty('empty-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $empty); + $this->assertSame('empty-tag', $empty->tag); + $this->assertEmpty($empty->all()); + + // range + $range = TestCollectionWithExtraState::range(1, 3, 1, 'range-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $range); + $this->assertSame('range-tag', $range->tag); + $this->assertSame([1, 2, 3], $range->all()); + + // times + $times = TestCollectionWithExtraState::times(3, fn ($i) => $i * 10, 'times-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $times); + $this->assertSame('times-tag', $times->tag); + $this->assertSame([10, 20, 30], $times->all()); + + // times with zero + $timesZero = TestCollectionWithExtraState::times(0, null, 'zero-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $timesZero); + $this->assertSame('zero-tag', $timesZero->tag); + $this->assertEmpty($timesZero->all()); + + // fromJson + $json = TestCollectionWithExtraState::fromJson('["a","b"]', 512, 0, 'json-tag'); + $this->assertInstanceOf(TestCollectionWithExtraState::class, $json); + $this->assertSame('json-tag', $json->tag); + $this->assertSame(['a', 'b'], $json->all()); + } + /** * Provides each collection class, respectively. * @@ -6201,6 +6348,22 @@ class TestCollectionSubclass extends Collection // } +class TestCollectionWithExtraState extends Collection +{ + public string $tag; + + public function __construct($items = [], string $tag = '') + { + parent::__construct($items); + $this->tag = $tag; + } + + protected function newInstance($items = []): static + { + return new static($items, $this->tag); + } +} + enum StaffEnum { case Taylor; From ee90c44192804d7b4fdbe37777a3490d7e22cbff Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:26:52 -0400 Subject: [PATCH 072/596] [13.x] `WithoutFramework` test attribute (#59432) * without booting framework attribute * rename * best effort * tell * formatting --------- Co-authored-by: Taylor Otwell --- .../Testing/Attributes/UnitTest.php | 13 +++++++++ .../Foundation/Testing/TestCase.php | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/Illuminate/Foundation/Testing/Attributes/UnitTest.php diff --git a/src/Illuminate/Foundation/Testing/Attributes/UnitTest.php b/src/Illuminate/Foundation/Testing/Attributes/UnitTest.php new file mode 100644 index 000000000000..c8233b973d61 --- /dev/null +++ b/src/Illuminate/Foundation/Testing/Attributes/UnitTest.php @@ -0,0 +1,13 @@ +withoutBootingFramework()) { + return; + } + $this->setUpTheTestEnvironment(); } @@ -82,9 +89,29 @@ protected function refreshApplication() */ protected function tearDown(): void { + if ($this->withoutBootingFramework()) { + return; + } + $this->tearDownTheTestEnvironment(); } + /** + * Determine if the test method should boot the framework. + * + * @return bool + * + * @throws \ReflectionException + */ + protected function withoutBootingFramework(): bool + { + try { + return (new ReflectionMethod(static::class, $this->name()))->getAttributes(UnitTest::class) !== []; + } catch (Throwable) { + return false; + } + } + /** * Clean up the testing environment before the next test case. * From c2509e37025ab0e2a19a18040282fa86450a218a Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 30 Mar 2026 17:02:57 -0500 Subject: [PATCH 073/596] prefer `isset()` over `in_array()` for better performance (#59457) the `class_uses_recursive()` method returns an array where both the key and value are the same fully qualified class name. by switching to `isset()` we get O(1) performance, while `in_array()` gives O(n) performance. - also remove some single use temporary variables - passes multiple arguments to `isset()` rather than multiple separate calls --- src/Illuminate/Bus/PendingBatch.php | 2 +- src/Illuminate/Console/GeneratorCommand.php | 4 ++-- src/Illuminate/Database/Eloquent/Model.php | 8 ++++---- .../Database/Eloquent/Relations/BelongsToMany.php | 2 +- src/Illuminate/Database/Schema/Blueprint.php | 4 +--- src/Illuminate/Database/Seeder.php | 4 +--- src/Illuminate/Events/CallQueuedListener.php | 2 +- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 2 +- src/Illuminate/Queue/CallQueuedHandler.php | 9 ++++----- src/Illuminate/Queue/Jobs/Job.php | 2 +- 10 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index e56c01a8d547..2f0e439201bf 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -104,7 +104,7 @@ protected function ensureJobIsBatchable(object|array $job): void return; } - if (! (static::$batchableClasses[$job::class] ?? false) && ! in_array(Batchable::class, class_uses_recursive($job))) { + if (! (static::$batchableClasses[$job::class] ?? false) && ! isset(class_uses_recursive($job)[Batchable::class])) { static::$batchableClasses[$job::class] = false; throw new RuntimeException(sprintf('Attempted to batch job [%s], but it does not use the Batchable trait.', $job::class)); diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index a79972595872..bc38acebba0f 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -130,7 +130,7 @@ public function __construct(Filesystem $files) { parent::__construct(); - if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { + if (isset(class_uses_recursive($this)[CreatesMatchingTest::class])) { $this->addTestOptions(); } @@ -186,7 +186,7 @@ public function handle() $info = $this->type; - if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { + if (isset(class_uses_recursive($this)[CreatesMatchingTest::class])) { $this->handleTestCreation($path); } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 33743b3204db..ce67d769914f 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2077,7 +2077,7 @@ public function refresh() $this->load((new BaseCollection($this->relations))->reject( fn ($relation) => $relation instanceof Pivot - || (is_object($relation) && in_array(AsPivot::class, class_uses_recursive($relation), true)) + || (is_object($relation) && isset(class_uses_recursive($relation)[AsPivot::class])) )->keys()->all()); $this->syncOriginal(); @@ -2547,7 +2547,7 @@ public function setPerPage($perPage) */ public static function isSoftDeletable(): bool { - return static::$isSoftDeletable[static::class] ??= in_array(SoftDeletes::class, class_uses_recursive(static::class)); + return static::$isSoftDeletable[static::class] ??= isset(class_uses_recursive(static::class)[SoftDeletes::class]); } /** @@ -2555,7 +2555,7 @@ public static function isSoftDeletable(): bool */ protected function isPrunable(): bool { - return self::$isPrunable[static::class] ??= in_array(Prunable::class, class_uses_recursive(static::class)) || static::isMassPrunable(); + return self::$isPrunable[static::class] ??= isset(class_uses_recursive(static::class)[Prunable::class]) || static::isMassPrunable(); } /** @@ -2563,7 +2563,7 @@ protected function isPrunable(): bool */ protected function isMassPrunable(): bool { - return self::$isMassPrunable[static::class] ??= in_array(MassPrunable::class, class_uses_recursive(static::class)); + return self::$isMassPrunable[static::class] ??= isset(class_uses_recursive(static::class)[MassPrunable::class]); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index c5d5637d28f7..b58536a39c29 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -194,7 +194,7 @@ protected function resolveTableName($table) return $table; } - if (in_array(AsPivot::class, class_uses_recursive($model))) { + if (isset(class_uses_recursive($model)[AsPivot::class])) { $this->using($table); } diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 84757edc472b..5e1f8f68c507 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -1060,9 +1060,7 @@ public function foreignIdFor($model, $column = null) ->referencesModelColumn($model->getKeyName()); } - $modelTraits = class_uses_recursive($model); - - if (in_array(HasUlids::class, $modelTraits, true)) { + if (isset(class_uses_recursive($model)[HasUlids::class])) { return $this->foreignUlid($column, 26) ->table($model->getTable()) ->referencesModelColumn($model->getKeyName()); diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index 557d10d4cfef..8031ce07ebcf 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -184,9 +184,7 @@ public function __invoke(array $parameters = []) ? $this->container->call([$this, 'run'], $parameters) : $this->run(...$parameters); - $uses = class_uses_recursive(static::class); - - if (isset($uses[WithoutModelEvents::class])) { + if (isset(class_uses_recursive(static::class)[WithoutModelEvents::class])) { $callback = $this->withoutModelEvents($callback); } diff --git a/src/Illuminate/Events/CallQueuedListener.php b/src/Illuminate/Events/CallQueuedListener.php index 3fbfa6664bbd..aa6d53c367ce 100644 --- a/src/Illuminate/Events/CallQueuedListener.php +++ b/src/Illuminate/Events/CallQueuedListener.php @@ -198,7 +198,7 @@ public function uniqueVia(): ?Cache */ protected function setJobInstanceIfNecessary(Job $job, $instance) { - if (in_array(InteractsWithQueue::class, class_uses_recursive($instance))) { + if (isset(class_uses_recursive($instance)[InteractsWithQueue::class])) { $instance->setJob($job); } diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index de806b1a7352..dd7f5640cd79 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -280,7 +280,7 @@ function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) return; } elseif ($relatedModel instanceof Pivot || - in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { + isset(class_uses_recursive($relatedModel)[AsPivot::class])) { yield $relationName => new MissingValue; return; diff --git a/src/Illuminate/Queue/CallQueuedHandler.php b/src/Illuminate/Queue/CallQueuedHandler.php index ef7c4d1104bb..c545afc25e82 100644 --- a/src/Illuminate/Queue/CallQueuedHandler.php +++ b/src/Illuminate/Queue/CallQueuedHandler.php @@ -164,7 +164,7 @@ protected function resolveHandler($job, $command) */ protected function setJobInstanceIfNecessary(Job $job, $instance) { - if (in_array(InteractsWithQueue::class, class_uses_recursive($instance))) { + if (isset(class_uses_recursive($instance)[InteractsWithQueue::class])) { $instance->setJob($job); } @@ -194,8 +194,7 @@ protected function ensureSuccessfulBatchJobIsRecorded($command) { $uses = class_uses_recursive($command); - if (! in_array(Batchable::class, $uses) || - ! in_array(InteractsWithQueue::class, $uses)) { + if (! isset($uses[Batchable::class], $uses[InteractsWithQueue::class])) { return; } @@ -293,7 +292,7 @@ protected function ensureUniqueJobLockIsReleasedViaContext() */ protected function ensureSuccessfulBatchJobIsRecordedForMissingModel(Job $job, string $class) { - if (! in_array(Batchable::class, class_uses_recursive($class), true)) { + if (! isset(class_uses_recursive($class)[Batchable::class])) { return; } @@ -358,7 +357,7 @@ public function failed(array $data, $e, string $uuid, ?Job $job = null) */ protected function ensureFailedBatchJobIsRecorded(string $uuid, $command, $e) { - if (! in_array(Batchable::class, class_uses_recursive($command))) { + if (! isset(class_uses_recursive($command)[Batchable::class])) { return; } diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index b2d051d630d6..bed7d611bd4e 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -194,7 +194,7 @@ public function fail($e = null) // the proper value. Otherwise, the current transaction will never commit. if ($e instanceof TimeoutExceededException && $commandName && - in_array(Batchable::class, class_uses_recursive($commandName))) { + isset(class_uses_recursive($commandName)[Batchable::class])) { $batchRepository = $this->resolve(BatchRepository::class); try { From a238828701a3b166c061373675fc83099a4a6f70 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 30 Mar 2026 17:03:13 -0500 Subject: [PATCH 074/596] remove temporary variable (#59456) - the variable is only used once - readability is comparable - multiline ternary adds consistency and better diffs --- src/Illuminate/Session/Middleware/StartSession.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Session/Middleware/StartSession.php b/src/Illuminate/Session/Middleware/StartSession.php index 748e8a55f336..d0cd6e36165e 100644 --- a/src/Illuminate/Session/Middleware/StartSession.php +++ b/src/Illuminate/Session/Middleware/StartSession.php @@ -266,11 +266,11 @@ protected function getSessionLifetimeInSeconds() */ protected function getCookieExpirationDate() { - $expiresOnClose = $this->manager->getSessionConfig()['expire_on_close']; - - return $expiresOnClose ? 0 : Date::instance( - Carbon::now()->addSeconds($this->getSessionLifetimeInSeconds()) - ); + return $this->manager->getSessionConfig()['expire_on_close'] + ? 0 + : Date::instance( + Carbon::now()->addSeconds($this->getSessionLifetimeInSeconds()) + ); } /** From 387657af86b5f58a6b270c60fd5f05514338aff3 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Tue, 31 Mar 2026 16:52:00 +0200 Subject: [PATCH 075/596] Correct Storage::fake() return type (#59469) --- src/Illuminate/Support/Facades/Storage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Facades/Storage.php b/src/Illuminate/Support/Facades/Storage.php index 2d214390b438..6a12ab40e92b 100644 --- a/src/Illuminate/Support/Facades/Storage.php +++ b/src/Illuminate/Support/Facades/Storage.php @@ -96,7 +96,7 @@ class Storage extends Facade * * @param \UnitEnum|string|null $disk * @param array $config - * @return \Illuminate\Contracts\Filesystem\Filesystem + * @return \Illuminate\Filesystem\LocalFilesystemAdapter */ public static function fake($disk = null, array $config = []) { @@ -128,7 +128,7 @@ public static function fake($disk = null, array $config = []) * * @param \UnitEnum|string|null $disk * @param array $config - * @return \Illuminate\Contracts\Filesystem\Filesystem + * @return \Illuminate\Filesystem\LocalFilesystemAdapter */ public static function persistentFake($disk = null, array $config = []) { From 0d3fc77521c73402641f980678692b4a9fa43580 Mon Sep 17 00:00:00 2001 From: N'Bayramberdiyev Date: Tue, 31 Mar 2026 17:52:40 +0300 Subject: [PATCH 076/596] [12.x] Fix callable type for freezeTime, freezeSecond, and travelTo (#59466) * Fix callable type for freezeTime, freezeSecond, and travelTo callbacks * Remove travelTo type test with narrowed Carbon callback * Use template for travelTo callback date type --- .../Foundation/Testing/Concerns/InteractsWithTime.php | 9 +++++---- types/Foundation/Testing/InteractsWithTime.php | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php index 66719a9d236d..7090c2a2f880 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php @@ -12,7 +12,7 @@ trait InteractsWithTime * * Freeze time. * - * @param (callable(): TReturn)|null $callback + * @param (callable(\Illuminate\Support\Carbon): TReturn)|null $callback * @return ($callback is null ? \Illuminate\Support\Carbon : TReturn) */ public function freezeTime($callback = null) @@ -27,7 +27,7 @@ public function freezeTime($callback = null) * * Freeze time at the beginning of the current second. * - * @param (callable(): TReturn)|null $callback + * @param (callable(\Illuminate\Support\Carbon): TReturn)|null $callback * @return ($callback is null ? \Illuminate\Support\Carbon : TReturn) */ public function freezeSecond($callback = null) @@ -50,11 +50,12 @@ public function travel($value) /** * @template TReturn of mixed + * @template TDate of \DateTimeInterface|\Closure|\Illuminate\Support\Carbon|string|bool|null * * Travel to another time. * - * @param \DateTimeInterface|\Closure|\Illuminate\Support\Carbon|string|bool|null $date - * @param (callable(): TReturn)|null $callback + * @param TDate $date + * @param (callable(TDate): TReturn)|null $callback * @return ($callback is null ? void : TReturn) */ public function travelTo($date, $callback = null) diff --git a/types/Foundation/Testing/InteractsWithTime.php b/types/Foundation/Testing/InteractsWithTime.php index aa82e1d69fe1..6576c82c55bd 100644 --- a/types/Foundation/Testing/InteractsWithTime.php +++ b/types/Foundation/Testing/InteractsWithTime.php @@ -15,9 +15,11 @@ public function test(): void { assertType(Carbon::class, $this->freezeTime()); assertType('42', $this->freezeTime(fn () => 42)); + assertType('42', $this->freezeTime(fn (Carbon $date) => 42)); assertType(Carbon::class, $this->freezeSecond()); assertType('42', $this->freezeSecond(fn () => 42)); + assertType('42', $this->freezeSecond(fn (Carbon $date) => 42)); // @phpstan-ignore method.void assertType('null', $this->travelTo(Carbon::now(), function () { From de0673823b6e916c97c8327c9d8d4c42084d495f Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 1 Apr 2026 08:31:36 +0100 Subject: [PATCH 077/596] [13.x] Add BatchStarted event (#59458) * event * batch bits * test * method bro otwell would not approve --- src/Illuminate/Bus/Batch.php | 27 ++++++++ src/Illuminate/Bus/Events/BatchStarted.php | 18 ++++++ tests/Bus/BusBatchTest.php | 75 ++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 src/Illuminate/Bus/Events/BatchStarted.php diff --git a/src/Illuminate/Bus/Batch.php b/src/Illuminate/Bus/Batch.php index d98f2cc0bb2d..6b02a30279d8 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -6,6 +6,7 @@ use Closure; use Illuminate\Bus\Events\BatchCanceled; use Illuminate\Bus\Events\BatchFinished; +use Illuminate\Bus\Events\BatchStarted; use Illuminate\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Queue\Factory as QueueFactory; @@ -218,6 +219,16 @@ public function processedJobs() return $this->totalJobs - $this->pendingJobs; } + /** + * Determine if this is the first job processed in the batch. + * + * @return bool + */ + protected function isFirstJobProcessed(UpdatedBatchJobCounts $counts): bool + { + return $this->totalJobs - $counts->pendingJobs + $counts->failedJobs === 1; + } + /** * Get the percentage of jobs that have been processed (between 0-100). * @@ -237,6 +248,14 @@ public function recordSuccessfulJob(string $jobId) { $counts = $this->decrementPendingJobs($jobId); + if ($this->isFirstJobProcessed($counts)) { + $container = Container::getInstance(); + + if ($container->bound(Dispatcher::class)) { + $container->make(Dispatcher::class)->dispatch(new BatchStarted($this)); + } + } + if ($this->hasProgressCallbacks()) { $this->invokeCallbacks('progress'); } @@ -342,6 +361,14 @@ public function recordFailedJob(string $jobId, $e) { $counts = $this->incrementFailedJobs($jobId); + if ($this->isFirstJobProcessed($counts)) { + $container = Container::getInstance(); + + if ($container->bound(Dispatcher::class)) { + $container->make(Dispatcher::class)->dispatch(new BatchStarted($this)); + } + } + if ($counts->failedJobs === 1 && ! $this->allowsFailures()) { $this->cancel($e); } diff --git a/src/Illuminate/Bus/Events/BatchStarted.php b/src/Illuminate/Bus/Events/BatchStarted.php new file mode 100644 index 000000000000..2671b36ddb97 --- /dev/null +++ b/src/Illuminate/Bus/Events/BatchStarted.php @@ -0,0 +1,18 @@ +add([$job]); + $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + return $event instanceof BatchStarted && $event->batch === $batch; + })); + $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { return $event instanceof BatchFinished && $event->batch === $batch; })); @@ -284,6 +289,76 @@ public function test_batch_finished_event_is_dispatched() $batch->recordSuccessfulJob('test-id'); } + public function test_batch_started_event_is_dispatched() + { + Container::getInstance()->instance(EventDispatcher::class, $events = m::mock(EventDispatcher::class)); + + $queue = m::mock(Factory::class); + $batch = $this->createTestBatch($queue); + + $job = new class + { + use Batchable; + }; + + $secondJob = new class + { + use Batchable; + }; + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once(); + + $batch = $batch->add([$job, $secondJob]); + + $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + return $event instanceof BatchStarted && $event->batch === $batch; + })); + + $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) { + return $event instanceof BatchFinished; + })); + + $batch->recordSuccessfulJob('test-id-1'); + $batch->recordSuccessfulJob('test-id-2'); + } + + public function test_batch_started_event_is_dispatched_when_first_job_fails() + { + Container::getInstance()->instance(EventDispatcher::class, $events = m::mock(EventDispatcher::class)); + + $queue = m::mock(Factory::class); + $batch = $this->createTestBatch($queue, $allowFailures = true); + + $job = new class + { + use Batchable; + }; + + $secondJob = new class + { + use Batchable; + }; + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once(); + + $batch = $batch->add([$job, $secondJob]); + + $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + return $event instanceof BatchStarted && $event->batch === $batch; + })); + + $batch->recordFailedJob('test-id-1', new RuntimeException('Something went wrong.')); + $batch->recordFailedJob('test-id-2', new RuntimeException('Something else went wrong.')); + } + public function test_failed_jobs_can_be_recorded_while_not_allowing_failures() { $queue = m::mock(Factory::class); From 628e7cb77ad29f1d67598a982e1045b339315824 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 1 Apr 2026 00:32:30 -0700 Subject: [PATCH 078/596] formatting --- src/Illuminate/Bus/Batch.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Bus/Batch.php b/src/Illuminate/Bus/Batch.php index 6b02a30279d8..6d9f5846dc01 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -219,16 +219,6 @@ public function processedJobs() return $this->totalJobs - $this->pendingJobs; } - /** - * Determine if this is the first job processed in the batch. - * - * @return bool - */ - protected function isFirstJobProcessed(UpdatedBatchJobCounts $counts): bool - { - return $this->totalJobs - $counts->pendingJobs + $counts->failedJobs === 1; - } - /** * Get the percentage of jobs that have been processed (between 0-100). * @@ -402,6 +392,16 @@ public function incrementFailedJobs(string $jobId) return $this->repository->incrementFailedJobs($this->id, $jobId); } + /** + * Determine if this is the first job processed in the batch. + * + * @return bool + */ + protected function isFirstJobProcessed(UpdatedBatchJobCounts $counts): bool + { + return $this->totalJobs - $counts->pendingJobs + $counts->failedJobs === 1; + } + /** * Determine if the batch has "catch" callbacks. * From 3f46645ef6c5655a8afa065ab0517e9e2be6a564 Mon Sep 17 00:00:00 2001 From: Nipun Khajuria Date: Wed, 1 Apr 2026 20:48:23 +0530 Subject: [PATCH 079/596] Preserve URI fragment when decoding query string (#59481) --- src/Illuminate/Support/Uri.php | 2 +- tests/Support/SupportUriTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index ab9258b2ea85..03ddc3ec216e 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -390,7 +390,7 @@ public function decode(): string return $this->value(); } - return Str::replace(Str::after($this->value(), '?'), $this->query()->decode(), $this->value()); + return Str::replace($this->query()->value(), $this->query()->decode(), $this->value()); } /** diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index dd91e1f6d432..9e15c23c28ae 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -178,6 +178,13 @@ public function test_decoding_the_entire_uri() $this->assertEquals('https://laravel.com/docs/11.x/installation?tags[0]=first&tags[1]=second', $uri->decode()); } + public function test_decoding_the_entire_uri_preserves_the_fragment() + { + $uri = Uri::of('https://laravel.com/docs/11.x/routing?q=laravel%20docs#route-model-binding'); + + $this->assertEquals('https://laravel.com/docs/11.x/routing?q=laravel docs#route-model-binding', $uri->decode()); + } + public function test_with_query_if_missing() { // Test adding new parameters while preserving existing ones From 8843a5e3f931e005e7f3c9046bc83f6e664c53f5 Mon Sep 17 00:00:00 2001 From: Casper Bloemendaal Date: Wed, 1 Apr 2026 17:19:04 +0200 Subject: [PATCH 080/596] fix: allow returning Stringable objects in casts()-method (#59479) * fix: allow returning Stringable objects in casts()-method * Update HasAttributes.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index a71fcc416cf3..8f719b767e70 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1722,7 +1722,7 @@ public function getCasts() /** * Get the attributes that should be cast. * - * @return array + * @return array */ protected function casts() { From 79c3dc6f842c709501323c42bff6e961063d4c4f Mon Sep 17 00:00:00 2001 From: "Kay W." Date: Wed, 1 Apr 2026 23:19:24 +0800 Subject: [PATCH 081/596] [12.x] Support string abstract in mock/partialMock/spy PHPDoc (#59477) * Support string abstract in mock/partialMock/spy PHPDoc with conditional return types * Fix StyleCI formatting --- .../Concerns/InteractsWithContainer.php | 12 +++---- .../Testing/InteractsWithContainer.php | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 types/Foundation/Testing/InteractsWithContainer.php diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index 519f23bfcfab..1987539a2a47 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -68,9 +68,9 @@ protected function instance($abstract, $instance) * * @template TInstance of object * - * @param class-string $abstract + * @param string|class-string $abstract * @param \Closure|null $mock - * @return TInstance&\Mockery\MockInterface + * @return ($abstract is class-string ? TInstance&\Mockery\MockInterface : \Mockery\MockInterface) */ protected function mock($abstract, ?Closure $mock = null) { @@ -82,9 +82,9 @@ protected function mock($abstract, ?Closure $mock = null) * * @template TInstance of object * - * @param class-string $abstract + * @param string|class-string $abstract * @param \Closure|null $mock - * @return TInstance&\Mockery\MockInterface + * @return ($abstract is class-string ? TInstance&\Mockery\MockInterface : \Mockery\MockInterface) */ protected function partialMock($abstract, ?Closure $mock = null) { @@ -96,9 +96,9 @@ protected function partialMock($abstract, ?Closure $mock = null) * * @template TInstance of object * - * @param class-string $abstract + * @param string|class-string $abstract * @param \Closure|null $mock - * @return TInstance&\Mockery\MockInterface + * @return ($abstract is class-string ? TInstance&\Mockery\MockInterface : \Mockery\MockInterface) */ protected function spy($abstract, ?Closure $mock = null) { diff --git a/types/Foundation/Testing/InteractsWithContainer.php b/types/Foundation/Testing/InteractsWithContainer.php new file mode 100644 index 000000000000..787c7f3d2a98 --- /dev/null +++ b/types/Foundation/Testing/InteractsWithContainer.php @@ -0,0 +1,35 @@ +mock(User::class)); + assertType('Mockery\MockInterface&User', $this->mock(User::class, function ($mock) { + })); + + assertType('Mockery\MockInterface&User', $this->partialMock(User::class)); + assertType('Mockery\MockInterface&User', $this->partialMock(User::class, function ($mock) { + })); + + assertType('Mockery\MockInterface&User', $this->spy(User::class)); + assertType('Mockery\MockInterface&User', $this->spy(User::class, function ($mock) { + })); + + assertType('Mockery\MockInterface', $this->mock('my.service')); + assertType('Mockery\MockInterface', $this->partialMock('my.service')); + assertType('Mockery\MockInterface', $this->spy('my.service')); + } +} From 3f651af0353cc04262fb3595052b5bdf3fa10ae2 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:20:15 +0200 Subject: [PATCH 082/596] [13.x] Fix manager breaking when called with static closure (#59470) * Fix manager breaking when called with static closure * Also handle managers that do not extend the manager base class * Fix code style --- src/Illuminate/Auth/AuthManager.php | 10 ++++++- .../Broadcasting/BroadcastManager.php | 8 +++++- src/Illuminate/Cache/CacheManager.php | 10 ++++++- .../Filesystem/FilesystemManager.php | 10 ++++++- src/Illuminate/Support/Manager.php | 9 ++++++- tests/Auth/AuthenticateMiddlewareTest.php | 8 ++++++ tests/Cache/CacheManagerTest.php | 19 ++++++++++++++ tests/Filesystem/FilesystemManagerTest.php | 17 ++++++++++++ .../Broadcasting/BroadcastManagerTest.php | 26 ++++++++++++++++--- tests/Integration/Support/ManagerTest.php | 10 +++++++ 10 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index f3934b1dc6c8..e3fadf3354e7 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -5,6 +5,8 @@ use Closure; use Illuminate\Contracts\Auth\Factory as FactoryContract; use InvalidArgumentException; +use RuntimeException; +use Throwable; /** * @mixin \Illuminate\Contracts\Auth\Guard @@ -271,7 +273,13 @@ public function resolveUsersUsing(Closure $userResolver) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index dd1ef8b19a47..fa94658a325c 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -504,7 +504,13 @@ public function purge($name = null) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 3d769c9d402a..bd7a0c0f7ce1 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -11,6 +11,8 @@ use InvalidArgumentException; use Mockery; use Mockery\LegacyMockInterface; +use RuntimeException; +use Throwable; /** * @mixin \Illuminate\Cache\Repository @@ -528,7 +530,13 @@ public function purge($name = null) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index dbe18a435e7d..c840fe09d5ac 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -20,6 +20,8 @@ use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; use League\Flysystem\UnixVisibility\PortableVisibilityConverter; use League\Flysystem\Visibility; +use RuntimeException; +use Throwable; use function Illuminate\Support\enum_value; @@ -439,7 +441,13 @@ public function purge($name = null) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/src/Illuminate/Support/Manager.php b/src/Illuminate/Support/Manager.php index 677857d225ef..4a82fa665277 100755 --- a/src/Illuminate/Support/Manager.php +++ b/src/Illuminate/Support/Manager.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Container\Container; use InvalidArgumentException; +use Throwable; abstract class Manager { @@ -127,7 +128,13 @@ protected function callCustomCreator($driver) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index 798806151f1c..ddbb57a863fd 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -154,6 +154,14 @@ public function testCustomDriverClosureBoundObjectIsAuthManager() $this->assertSame($this->auth, $this->auth->guard(__CLASS__)); } + public function testCustomDriverStatic() + { + $driver = new stdClass; + + $this->auth->extend(__CLASS__, fn () => $driver); + $this->assertSame($driver, $this->auth->guard(__CLASS__)); + } + /** * Create a new config repository instance. * diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index 0fbd727c5c86..b48b8903c85c 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -12,6 +12,7 @@ use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; +use stdClass; class CacheManagerTest extends TestCase { @@ -30,6 +31,24 @@ public function testCustomDriverClosureBoundObjectIsCacheManager() $this->assertSame($manager, $manager->store(__CLASS__)); } + public function testCustomDriverStaticClosure() + { + $manager = new CacheManager($this->getApp([ + 'cache' => [ + 'stores' => [ + __CLASS__ => [ + 'driver' => __CLASS__, + ], + ], + ], + ])); + + $driver = new stdClass; + + $manager->extend(__CLASS__, static fn () => $driver); + $this->assertSame($driver, $manager->store(__CLASS__)); + } + public function testCustomDriverOverridesInternalDrivers() { $userConfig = [ diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 0084bb53bf05..772cbe603399 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -10,6 +10,7 @@ use League\Flysystem\UnableToReadFile; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; +use stdClass; class FilesystemManagerTest extends TestCase { @@ -230,6 +231,22 @@ public function testCustomDriverClosureBoundObjectIsFilesystemManager() $this->assertSame($manager, $manager->disk(__CLASS__)); } + public function testCustomDriverStaticClosure() + { + $manager = new FilesystemManager(tap(new Application, static function ($app) { + $app['config'] = [ + 'filesystems.disks.'.__CLASS__ => [ + 'driver' => __CLASS__, + ], + ]; + })); + + $driver = new stdClass; + + $manager->extend(__CLASS__, static fn () => $driver); + $this->assertSame($driver, $manager->disk(__CLASS__)); + } + // public function testKeepTrackOfAdapterDecoration() // { // try { diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index 9a66b7c84d46..b6d7e504813d 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -17,7 +17,9 @@ use Illuminate\Support\Facades\Queue; use InvalidArgumentException; use Orchestra\Testbench\TestCase; +use Psr\Log\LoggerInterface; use RuntimeException; +use stdClass; class BroadcastManagerTest extends TestCase { @@ -156,6 +158,24 @@ public function testCustomDriverClosureBoundObjectIsBroadcastManager() $this->assertSame($manager, $manager->connection(__CLASS__)); } + public function testCustomDriverStaticClosure() + { + $manager = new BroadcastManager($this->getApp([ + 'broadcasting' => [ + 'connections' => [ + __CLASS__ => [ + 'driver' => __CLASS__, + ], + ], + ], + ])); + + $driver = new stdClass; + + $manager->extend(__CLASS__, static fn () => $driver); + $this->assertSame($driver, $manager->connection(__CLASS__)); + } + public function testThrowExceptionWhenDriverCreationFails() { $userConfig = [ @@ -169,8 +189,8 @@ public function testThrowExceptionWhenDriverCreationFails() ]; $app = $this->getApp($userConfig); - $app->singleton(\Psr\Log\LoggerInterface::class, function () { - throw new \RuntimeException('Logger service not available'); + $app->singleton(LoggerInterface::class, function () { + throw new RuntimeException('Logger service not available'); }); $broadcastManager = new BroadcastManager($app); @@ -181,7 +201,7 @@ public function testThrowExceptionWhenDriverCreationFails() } catch (RuntimeException $e) { $this->assertStringContainsString('Failed to create broadcaster for connection "log_connection_1"', $e->getMessage()); $this->assertStringContainsString('Logger service not available', $e->getMessage()); - $this->assertInstanceOf(\RuntimeException::class, $e->getPrevious()); + $this->assertInstanceOf(RuntimeException::class, $e->getPrevious()); } } diff --git a/tests/Integration/Support/ManagerTest.php b/tests/Integration/Support/ManagerTest.php index 902b400737a0..87e9b52f0261 100644 --- a/tests/Integration/Support/ManagerTest.php +++ b/tests/Integration/Support/ManagerTest.php @@ -5,6 +5,7 @@ use Illuminate\Tests\Integration\Support\Fixtures\NullableManager; use InvalidArgumentException; use Orchestra\Testbench\TestCase; +use stdClass; class ManagerTest extends TestCase { @@ -21,4 +22,13 @@ public function testCustomDriverClosureBoundObjectIsManager() $manager->extend(__CLASS__, fn () => $this); $this->assertSame($manager, $manager->driver(__CLASS__)); } + + public function testCustomDriverStaticClosure() + { + $manager = new NullableManager($this->app); + $driver = new stdClass; + + $manager->extend(__CLASS__, static fn () => $driver); + $this->assertSame($driver, $manager->driver(__CLASS__)); + } } From a2ee58c04ec989acfdffa78990777839f7c8d098 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 1 Apr 2026 16:30:33 +0100 Subject: [PATCH 083/596] Prevents installed package from executing malicious code via `postinstall` (#59485) --- .../Foundation/Console/BroadcastingInstallCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 4c32db5d83f4..7e7a7bc27f67 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -407,7 +407,7 @@ protected function installNodeDependencies() if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - 'pnpm add --save-dev laravel-echo pusher-js', + 'pnpm add --save-dev laravel-echo pusher-js --ignore-scripts', 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { @@ -422,7 +422,7 @@ protected function installNodeDependencies() ]; } else { $commands = [ - 'npm install --save-dev laravel-echo pusher-js', + 'npm install --save-dev laravel-echo pusher-js --ignore-scripts', 'npm run build', ]; } From 118b7063c44a2f3421d1646f5ddf08defcfd1db3 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:39:53 +0000 Subject: [PATCH 084/596] Update version to v13.3.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 61a534db25eb..0c58bcda00a1 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 = '13.2.0'; + const VERSION = '13.3.0'; /** * The base path for the Laravel installation. From 78566358dd99d63f3040145c0f612d4c7a50d0c2 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:41:34 +0000 Subject: [PATCH 085/596] Update CHANGELOG --- CHANGELOG.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 205baf1e013f..bfc1c39517f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,51 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.2.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.3.0...13.x) + +## [v13.3.0](https://github.com/laravel/framework/compare/v13.2.0...v13.3.0) - 2026-04-01 + +* [13.x] Forward releaseOnTerminationSignals through schedule groups by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59357 +* [13.x] Fix sub-minute scheduling skips at minute boundaries by [@JoshSalway](https://github.com/JoshSalway) in https://github.com/laravel/framework/pull/59331 +* [13.x] Display memory usage in verbose queue worker output by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59379 +* [13.x] Update WithoutOverlapping@shared() for clarity by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59375 +* [13.x] Fix dependency injection of faked queueing dispatcher by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/59378 +* [13.x] Fix incrementEach/decrementEach to scope to model instance by [@JoshSalway](https://github.com/JoshSalway) in https://github.com/laravel/framework/pull/59376 +* [13.x] Add array value types to Support module docblocks by [@Anthony14FR](https://github.com/Anthony14FR) in https://github.com/laravel/framework/pull/59383 +* [13.x] Add lost connection to WorkerStopReason by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59370 +* [13.x] MariaDbSchemaState uses mysql --version for client detection instead of mariadb --version by [@kylemilloy](https://github.com/kylemilloy) in https://github.com/laravel/framework/pull/59360 +* [13.x] Add enum support to QueueManager connection methods by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59389 +* [13.x] Setup rector by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59385 +* [13.x] Improve `Arr::whereNotNull()` docs by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/59411 +* [13.x] Pass request to afterResponse callback by [@bilfeldt](https://github.com/bilfeldt) in https://github.com/laravel/framework/pull/59410 +* [13.x] Add isNotEmpty() method to Uri class by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59408 +* [13.x] Add missing capitalize parameter to Stringable::initials() by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59407 +* [13.x] Fix trait initializer collision with Attribute parsing by [@sadique-cws](https://github.com/sadique-cws) in https://github.com/laravel/framework/pull/59404 +* [13.x] Add session to supported drivers comment by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59399 +* [13.x] Add `->file()` method to `$request->safe()` by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/59396 +* [13.x] Add enum support to LogManager channel and driver methods by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59391 +* [13.x] Fix MorphTo eager load matching when ownerKey is null and result key is a non-primitive by [@wietsewarendorff](https://github.com/wietsewarendorff) in https://github.com/laravel/framework/pull/59394 +* [13.x] Remove unnecessary clone in SessionManager to prevent duplicate Redis connections by [@JoshSalway](https://github.com/JoshSalway) in https://github.com/laravel/framework/pull/59323 +* [13.x] Use FQCN for Str in exception renderer blade templates by [@bankorh](https://github.com/bankorh) in https://github.com/laravel/framework/pull/59412 +* Allow variadic args for model attributes by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/59421 +* [13.x] CollectedBy Attribute should follow inheritence by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59419 +* [13.x] Fix deprecation notice in JSON:API resources by [@alihamze](https://github.com/alihamze) in https://github.com/laravel/framework/pull/59418 +* [13.x] Add withoutFragment() method to Uri class by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59413 +* [13.x] Fix macros with static closures by [@FeBe95](https://github.com/FeBe95) in https://github.com/laravel/framework/pull/59414 +* [13.x] Fix sum() docblock to include key parameter in callback signature by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59444 +* [13.x] Add assertHasNoAttachments() method to Mailable by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59443 +* [13.x] Add a driver method to the MailFake class by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/59448 +* [13.x] Cache getLockForPopping() result in DatabaseQueue by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59435 +* [13.x] prefer `new Collection()` over `collect()` helper by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/59453 +* [13.x] remove unnecessary `array_flip()` calls by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/59452 +* Make Collection methods compatible with extended subclass constructors by [@ProjektGopher](https://github.com/ProjektGopher) in https://github.com/laravel/framework/pull/59455 +* [13.x] `UnitTest` test attribute by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59432 +* [13.x] prefer `isset()` over `in_array()` for better performance by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/59457 +* [13.x] remove temporary variable by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/59456 +* [13.x] Add BatchStarted event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59458 +* [13.x] Preserve URI fragment when decoding query string by [@Nipun404](https://github.com/Nipun404) in https://github.com/laravel/framework/pull/59481 +* fix: allow returning Stringable objects in casts()-method by [@Bloemendaal](https://github.com/Bloemendaal) in https://github.com/laravel/framework/pull/59479 +* [13.x] Fix manager breaking when called with static closure by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/59470 +* Prevents installed package from executing malicious code via `postinstall` in `install:broadcasting` command by [@duncanmcclean](https://github.com/duncanmcclean) in https://github.com/laravel/framework/pull/59485 ## [v13.2.0](https://github.com/laravel/framework/compare/v13.1.1...v13.2.0) - 2026-03-24 From 3a87ff6905c1ab31e6546ba9a3784c60717f48db Mon Sep 17 00:00:00 2001 From: Alex Fadez <11855162+fadez@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:58:55 +0300 Subject: [PATCH 086/596] [13.x] Fix missing `Illuminate\Queue\Attributes\Delay` attribute (#59504) --- src/Illuminate/Events/Dispatcher.php | 3 ++- src/Illuminate/Queue/Attributes/Delay.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Queue/Attributes/Delay.php diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index cbff331daf3f..9ad62d1f785d 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -20,6 +20,7 @@ use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; use Illuminate\Queue\Attributes\Backoff; use Illuminate\Queue\Attributes\Connection; +use Illuminate\Queue\Attributes\Delay; use Illuminate\Queue\Attributes\DeleteWhenMissingModels; use Illuminate\Queue\Attributes\FailOnTimeout; use Illuminate\Queue\Attributes\MaxExceptions; @@ -679,7 +680,7 @@ protected function queueHandler($class, $method, $arguments) $delay = method_exists($listener, 'withDelay') ? (isset($arguments[0]) ? $listener->withDelay($arguments[0]) : $listener->withDelay()) - : $listener->delay ?? null; + : $this->getAttributeValue($listener, Delay::class, 'delay'); if (is_null($queue)) { $queue = $this->resolveQueueFromQueueRoute($listener) ?? null; diff --git a/src/Illuminate/Queue/Attributes/Delay.php b/src/Illuminate/Queue/Attributes/Delay.php new file mode 100644 index 000000000000..6aa7dd4b12e7 --- /dev/null +++ b/src/Illuminate/Queue/Attributes/Delay.php @@ -0,0 +1,19 @@ + Date: Thu, 2 Apr 2026 18:59:17 +0200 Subject: [PATCH 087/596] fix-interval-scientific-notation (#59502) --- .../Support/Traits/InteractsWithData.php | 2 +- tests/Http/HttpRequestTest.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Traits/InteractsWithData.php b/src/Illuminate/Support/Traits/InteractsWithData.php index b31e00505521..99616e6c9058 100644 --- a/src/Illuminate/Support/Traits/InteractsWithData.php +++ b/src/Illuminate/Support/Traits/InteractsWithData.php @@ -347,7 +347,7 @@ public function interval($key, $unit = null) $unit = $unit instanceof Unit ? $unit : Unit::fromName($unit); - return $unit->interval((float) $value); + return CarbonInterval::fromString(number_format((float) $value, 10, '.', '').' '.$unit->name); } /** diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 3f1f1029ce7e..d3b589fb68e5 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -858,6 +858,37 @@ public function testIntervalMethod() $this->assertSame(45, $request->interval('as_minutes', Unit::Second)->seconds); } + public function testIntervalMethodWithScientificNotationFloats() + { + $request = Request::create('/', 'GET', [ + 'small_float' => '0.000053', + 'very_small' => '0.000001', + 'scientific' => '5.3E-5', + 'normal_float' => '1.5', + 'integer' => '90', + ]); + + // Small floats that PHP would render in scientific notation (e.g. 5.3E-5) + $interval = $request->interval('small_float', Unit::Millisecond); + $this->assertInstanceOf(CarbonInterval::class, $interval); + + $interval = $request->interval('very_small', Unit::Second); + $this->assertInstanceOf(CarbonInterval::class, $interval); + + // Scientific notation string passed directly + $interval = $request->interval('scientific', Unit::Millisecond); + $this->assertInstanceOf(CarbonInterval::class, $interval); + + // Normal values should still work + $interval = $request->interval('normal_float', Unit::Hour); + $this->assertInstanceOf(CarbonInterval::class, $interval); + $this->assertSame(1, $interval->hours); + + $interval = $request->interval('integer', Unit::Minute); + $this->assertInstanceOf(CarbonInterval::class, $interval); + $this->assertSame(90, $interval->minutes); + } + public function testEnumMethod() { $request = Request::create('/', 'GET', [ From a2287e1fd076ad9b44dd5c015f2cdcb2bbf62af3 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:01:46 +0200 Subject: [PATCH 088/596] Add pint.json to export-ignore (#59497) --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 644702b6c1ff..e1a955ec0bec 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,5 +24,6 @@ docker-compose.yml export-ignore phpstan.src.neon.dist export-ignore phpstan.types.neon.dist export-ignore phpunit.xml.dist export-ignore +pint.json export-ignore rector.php export-ignore RELEASE.md export-ignore From 312a44b9ef79c3bc12f7263f03450fc7abb26bd8 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:02:01 +0600 Subject: [PATCH 089/596] [13.x] Add --ignore-scripts to yarn in BroadcastingInstallCommand (#59494) PR #59485 added --ignore-scripts to pnpm and npm install commands to prevent malicious postinstall scripts. The same flag was missed for yarn which also runs postinstall scripts by default. --- .../Foundation/Console/BroadcastingInstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 7e7a7bc27f67..33bc8eec309a 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -412,7 +412,7 @@ protected function installNodeDependencies() ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - 'yarn add --dev laravel-echo pusher-js', + 'yarn add --dev laravel-echo pusher-js --ignore-scripts', 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { From 5f395dd65c5ec1fdba0771c2e3e8d62d4bb5bf62 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:02:33 +0600 Subject: [PATCH 090/596] [13.x] Fix static closure binding in remaining manager classes (#59493) PR #59470 fixed static closure handling in extend() for AuthManager, BroadcastManager, CacheManager, FilesystemManager, and the base Manager class. The same fix was missed in RedisManager, LogManager, and MultipleInstanceManager which still use the old bindTo($this, $this) pattern that breaks with static closures. --- src/Illuminate/Log/LogManager.php | 9 ++++++++- src/Illuminate/Redis/RedisManager.php | 10 +++++++++- src/Illuminate/Support/MultipleInstanceManager.php | 9 ++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index d88e4bb7fdd1..b6762bdbd5b5 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -21,6 +21,7 @@ use Monolog\Processor\ProcessorInterface; use Monolog\Processor\PsrLogMessageProcessor; use Psr\Log\LoggerInterface; +use RuntimeException; use Throwable; use function Illuminate\Support\enum_value; @@ -600,7 +601,13 @@ public function setDefaultDriver($name) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index ce3817f357ab..d67464887d66 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -10,6 +10,8 @@ use Illuminate\Support\Arr; use Illuminate\Support\ConfigurationUrlParser; use InvalidArgumentException; +use RuntimeException; +use Throwable; use function Illuminate\Support\enum_value; @@ -263,7 +265,13 @@ public function purge($name = null) */ public function extend($driver, Closure $callback) { - $this->customCreators[$driver] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$driver] = $callback; return $this; } diff --git a/src/Illuminate/Support/MultipleInstanceManager.php b/src/Illuminate/Support/MultipleInstanceManager.php index 66fff08e3c9a..6b1d8d0bc3c8 100644 --- a/src/Illuminate/Support/MultipleInstanceManager.php +++ b/src/Illuminate/Support/MultipleInstanceManager.php @@ -5,6 +5,7 @@ use Closure; use InvalidArgumentException; use RuntimeException; +use Throwable; abstract class MultipleInstanceManager { @@ -198,7 +199,13 @@ public function purge($name = null) */ public function extend($name, Closure $callback) { - $this->customCreators[$name] = $callback->bindTo($this, $this); + try { + $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; + } catch (Throwable) { + $callback = $callback->bindTo(null, static::class); + } + + $this->customCreators[$name] = $callback; return $this; } From 3dcfeb179e5bc9944f0d49dc6814738ed557b9a9 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 2 Apr 2026 18:04:14 +0100 Subject: [PATCH 091/596] [13.x] Fix CollectedBy attribute not resolving through abstract parent classes (#59488) * 13.x-fix-collectby-for-abstract * add regression test for no attr too --- .../Database/Eloquent/HasCollection.php | 19 +++++------ tests/Database/DatabaseEloquentModelTest.php | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/HasCollection.php b/src/Illuminate/Database/Eloquent/HasCollection.php index a2ef15bdffe4..e0aeb104abe5 100644 --- a/src/Illuminate/Database/Eloquent/HasCollection.php +++ b/src/Illuminate/Database/Eloquent/HasCollection.php @@ -43,19 +43,16 @@ public function newCollection(array $models = []) */ public function resolveCollectionFromAttribute() { - $reflectionClass = new ReflectionClass(static::class); + $reflection = new ReflectionClass(static::class); - $isEloquentGrandchild = is_subclass_of(static::class, Model::class) - && get_parent_class(static::class) !== Model::class; + do { + $attributes = $reflection->getAttributes(CollectedBy::class); - $attributes = $reflectionClass->getAttributes(CollectedBy::class); + if (isset($attributes[0], $attributes[0]->getArguments()[0])) { + return $attributes[0]->getArguments()[0]; + } + } while ($reflection = $reflection->getParentClass()); - if (! isset($attributes[0]) || ! isset($attributes[0]->getArguments()[0])) { - return $isEloquentGrandchild - ? (new (get_parent_class(static::class)))->resolveCollectionFromAttribute() - : null; - } - - return $attributes[0]->getArguments()[0]; + return null; } } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 9d4e9dd66e7c..0f0e117bd877 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3780,6 +3780,22 @@ public function testCollectedByAttributeIsInherited() $this->assertInstanceOf(CustomEloquentCollection::class, $collection); } + public function testCollectedByAttributeIsInheritedThroughAbstractParent() + { + $model = new EloquentConcreteChildOfAbstractModel; + $collection = $model->newCollection([$model]); + + $this->assertInstanceOf(CustomEloquentCollection::class, $collection); + } + + public function testNewCollectionWorksForConcreteModelExtendingAbstractModel() + { + $model = new EloquentConcreteChildModel; + $collection = $model->newCollection([$model]); + + $this->assertInstanceOf(Collection::class, $collection); + } + public function testUseFactoryAttribute() { $model = new EloquentModelWithUseFactoryAttribute; @@ -4690,6 +4706,22 @@ class EloquentChildModelWithCollectedByAttribute extends EloquentModelWithCollec { } +abstract class EloquentAbstractModel extends EloquentModelWithCollectedByAttribute +{ +} + +class EloquentConcreteChildOfAbstractModel extends EloquentAbstractModel +{ +} + +abstract class EloquentAbstractParentModel extends Model +{ +} + +class EloquentConcreteChildModel extends EloquentAbstractParentModel +{ +} + class CustomEloquentCollection extends Collection { } From f374d48bc97144bf2ed6e76b1fa2743e615ad446 Mon Sep 17 00:00:00 2001 From: Niduranga Jayarathna Date: Thu, 2 Apr 2026 23:09:10 +0530 Subject: [PATCH 092/596] [13.x] Fix: Allow runtime property overrides (onQueue) to take precedence over class attributes (#59468) * [13.x] Fix: Allow runtime property overrides (onQueue) to take precedence over class attributes * fix: test name refactor * Restore runtime property precedence using default value detection * Restore runtime property precedence using default value detection * clean dual refection boject make * clean dual refection boject make * docs: narrow type for attributeClass in ReadsClassAttributes * docs: narrow type for attributeClass in ReadsClassAttributes * docs: narrow type for attributeClass in ReadsClassAttributes * docs: restore return type and refine parameter types in ReadsClassAttributes * Update ReadsClassAttributes.php --------- Co-authored-by: Taylor Otwell --- .../Support/Traits/ReadsClassAttributes.php | 18 +-- .../Notifications/NotificationSenderTest.php | 133 ++++++++++++++++-- 2 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/Illuminate/Support/Traits/ReadsClassAttributes.php b/src/Illuminate/Support/Traits/ReadsClassAttributes.php index d21e062de6a3..f5955c8e3764 100644 --- a/src/Illuminate/Support/Traits/ReadsClassAttributes.php +++ b/src/Illuminate/Support/Traits/ReadsClassAttributes.php @@ -11,16 +11,22 @@ trait ReadsClassAttributes * Get a configuration value from an attribute, falling back to a property. * * @param object $target - * @param string $attributeClass + * @param class-string $attributeClass * @param string|null $property * @param mixed $default * @return mixed */ protected function getAttributeValue($target, string $attributeClass, ?string $property = null, $default = null) { - try { - $reflection = new ReflectionClass($target); + $reflection = new ReflectionClass($target); + + $defaultProperties = $reflection->getDefaultProperties(); + + if (isset($target->{$property}) && $target->{$property} !== ($defaultProperties[$property] ?? null)) { + return $target->{$property}; + } + try { do { $attributes = $reflection->getAttributes($attributeClass); @@ -32,11 +38,7 @@ protected function getAttributeValue($target, string $attributeClass, ?string $p // } - if ($property !== null) { - return $target->{$property} ?? $default; - } - - return $default; + return $target->{$property} ?? $default; } /** diff --git a/tests/Notifications/NotificationSenderTest.php b/tests/Notifications/NotificationSenderTest.php index 0705db1d747e..3a6c567caa94 100644 --- a/tests/Notifications/NotificationSenderTest.php +++ b/tests/Notifications/NotificationSenderTest.php @@ -13,6 +13,7 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notification; use Illuminate\Notifications\NotificationSender; +use Illuminate\Queue\Attributes\Queue; use Mockery as m; use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Exception\HttpTransportException; @@ -21,7 +22,7 @@ class NotificationSenderTest extends TestCase { - public function testItCanSendQueuedNotificationsWithAStringVia() + public function test_it_can_send_queued_notifications_with_a_string_via() { $notifiable = m::mock(Notifiable::class); $manager = m::mock(ChannelManager::class); @@ -38,7 +39,7 @@ public function testItCanSendQueuedNotificationsWithAStringVia() $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); } - public function testItCanSendQueuedNotificationsWithAnArrayVia() + public function test_it_can_send_queued_notifications_with_an_array_via() { $notifiable = m::mock(Notifiable::class); $manager = m::mock(ChannelManager::class); @@ -63,7 +64,7 @@ public function testItCanSendQueuedNotificationsWithAnArrayVia() $sender->send($notifiable, new DummyQueuedNotificationWithArrayVia); } - public function testItCanSendNotificationsWithAnEmptyStringVia() + public function test_it_can_send_notifications_with_an_empty_string_via() { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); @@ -77,7 +78,7 @@ public function testItCanSendNotificationsWithAnEmptyStringVia() $sender->sendNow($notifiable, new DummyNotificationWithEmptyStringVia); } - public function testItCannotSendNotificationsViaDatabaseForAnonymousNotifiables() + public function test_it_cannot_send_notifications_via_database_for_anonymous_notifiables() { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); @@ -92,7 +93,7 @@ public function testItCannotSendNotificationsViaDatabaseForAnonymousNotifiables( $sender->sendNow($notifiable, new DummyNotificationWithDatabaseVia); } - public function testItCanSendQueuedNotificationsThroughMiddleware() + public function test_it_can_send_queued_notifications_through_middleware() { $notifiable = m::mock(Notifiable::class); $manager = m::mock(ChannelManager::class); @@ -112,7 +113,7 @@ public function testItCanSendQueuedNotificationsThroughMiddleware() $sender->send($notifiable, new DummyNotificationWithMiddleware); } - public function testItCanSendQueuedMultiChannelNotificationsThroughDifferentMiddleware() + public function test_it_can_send_queued_multi_channel_notifications_through_different_middleware() { $notifiable = m::mock(Notifiable::class); $manager = m::mock(ChannelManager::class); @@ -143,7 +144,7 @@ public function testItCanSendQueuedMultiChannelNotificationsThroughDifferentMidd $sender->send($notifiable, new DummyMultiChannelNotificationWithConditionalMiddleware); } - public function testItCanSendQueuedWithViaConnectionsNotifications() + public function test_it_can_send_queued_with_via_connections_notifications() { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); @@ -168,7 +169,7 @@ public function testItCanSendQueuedWithViaConnectionsNotifications() $sender->send($notifiable, new DummyNotificationWithViaConnections); } - public function testItCanSendQueuedWithViaQueuesNotifications() + public function test_it_can_send_queued_with_via_queues_notifications() { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); @@ -193,7 +194,7 @@ public function testItCanSendQueuedWithViaQueuesNotifications() $sender->send($notifiable, new DummyNotificationWithViaQueues); } - public function testItCanSendQueuedNotificationsWithQueueRoute() + public function test_it_can_send_queued_notifications_with_queue_route() { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); @@ -216,11 +217,11 @@ public function testItCanSendQueuedNotificationsWithQueueRoute() $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); } - public function testNotificationFailedSentWithoutHttpTransportException() + public function test_notification_failed_sent_without_http_transport_exception() { $this->expectException(TransportException::class); - $notifiable = new AnonymousNotifiable(); + $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); $manager->shouldReceive('driver')->andReturn($driver = m::mock()); $response = m::mock(ResponseInterface::class); @@ -236,10 +237,10 @@ public function testNotificationFailedSentWithoutHttpTransportException() $sender = new NotificationSender($manager, $bus, $events); - $sender->sendNow($notifiable, new DummyNotificationWithViaConnections(), ['mail']); + $sender->sendNow($notifiable, new DummyNotificationWithViaConnections, ['mail']); } - public function testItPreservesNotificationStateMutatedInViaMethod() + public function test_it_preserves_notification_state_mutated_in_via_method() { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); @@ -258,6 +259,112 @@ public function testItPreservesNotificationStateMutatedInViaMethod() $sender->sendNow($notifiable, new DummyNotificationWithViaMutation); } + + public function test_it_queue_overrides_queue_attribute() + { + $notification = new #[Queue('attribute-queue')] class extends Notification implements ShouldQueue + { + use Queueable; + + public function via($notifiable): string + { + return 'mail'; + } + }; + + $notification->onQueue('manual-queue'); + + $notifiable = m::mock(Notifiable::class); + $manager = m::mock(ChannelManager::class); + $manager->shouldReceive('getContainer')->andReturn(app()); + $manager->shouldReceive('resolveQueueFromQueueRoute')->andReturn(null); + $manager->shouldReceive('resolveConnectionFromQueueRoute')->andReturn(null); + + $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen'); + + $bus = m::mock(BusDispatcher::class); + $bus->shouldReceive('dispatch') + ->once() + ->withArgs(function ($job) { + return $job->queue === 'manual-queue'; + }); + + $sender = new NotificationSender($manager, $bus, $events); + + $sender->send($notifiable, $notification); + } + + public function test_it_queue_attribute_is_used_when_on_queue_is_not_called() + { + $notification = new #[Queue('attribute-queue')] class extends Notification implements ShouldQueue + { + use Queueable; + + public function via($notifiable): string + { + return 'mail'; + } + }; + + $notifiable = m::mock(Notifiable::class); + $manager = m::mock(ChannelManager::class); + $manager->shouldReceive('getContainer')->andReturn(app()); + $manager->shouldReceive('resolveQueueFromQueueRoute')->andReturn(null); + $manager->shouldReceive('resolveConnectionFromQueueRoute')->andReturn(null); + + $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen'); + + $bus = m::mock(BusDispatcher::class); + $bus->shouldReceive('dispatch') + ->once() + ->withArgs(function ($job) { + return $job->queue === 'attribute-queue'; + }); + + $sender = new NotificationSender($manager, $bus, $events); + + $sender->send($notifiable, $notification); + } + + public function test_it_constructor_override_takes_precedence_over_queue_attribute() + { + $notification = new #[Queue('attribute-queue')] class extends Notification implements ShouldQueue + { + use Queueable; + + public function __construct() + { + $this->queue = 'constructor-override-queue'; + } + + public function via($notifiable): string + { + return 'mail'; + } + }; + + $notifiable = m::mock(Notifiable::class); + $manager = m::mock(ChannelManager::class); + $manager->shouldReceive('getContainer')->andReturn(app()); + $manager->shouldReceive('resolveQueueFromQueueRoute')->andReturn(null); + $manager->shouldReceive('resolveConnectionFromQueueRoute')->andReturn(null); + + $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen'); + + $bus = m::mock(BusDispatcher::class); + $bus->shouldReceive('dispatch') + ->once() + ->withArgs(function ($job) { + return $job->queue === 'constructor-override-queue'; + }); + + $sender = new NotificationSender($manager, $bus, $events); + + $sender->send($notifiable, $notification); + } } class DummyQueuedNotificationWithStringVia extends Notification implements ShouldQueue From 59c7486c5489cb94b27c9df2e38c79db8091c396 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:15:36 +0600 Subject: [PATCH 093/596] [13.x] Use #[Delay] attribute in Bus Dispatcher (#59514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Bus Dispatcher reads $command->delay directly instead of using getAttributeValue() with Delay::class, meaning #[Delay(30)] on job classes has no effect when dispatched via the Bus. The queue name already uses getAttributeValue — delay should too. --- src/Illuminate/Bus/Dispatcher.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Bus/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index 69b02e010a56..89e1e008dbcd 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\PendingChain; use Illuminate\Pipeline\Pipeline; use Illuminate\Queue\Attributes\Connection; +use Illuminate\Queue\Attributes\Delay; use Illuminate\Queue\Attributes\Queue as QueueAttribute; use Illuminate\Queue\Attributes\ReadsQueueAttributes; use Illuminate\Queue\InteractsWithQueue; @@ -251,8 +252,10 @@ protected function pushCommandToQueue($queue, $command) ?? $this->resolveQueueFromQueueRoute($command) ?? null; - if (isset($command->delay)) { - return $queue->later($command->delay, $command, queue: $queueName); + $delay = $this->getAttributeValue($command, Delay::class, 'delay'); + + if (isset($delay)) { + return $queue->later($delay, $command, queue: $queueName); } return $queue->push($command, queue: $queueName); From 423f6601b203e9c6da3d2bd91bb48b06d340b5a5 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:15:53 +0600 Subject: [PATCH 094/596] [13.x] Use Delay attribute in NotificationSender (#59513) PR #59504 added the #[Delay] attribute and wired it into the Event Dispatcher. NotificationSender was not updated and still reads $notification->delay directly, meaning #[Delay(30)] on notification classes has no effect. Use getAttributeValue() consistently. --- src/Illuminate/Notifications/NotificationSender.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Notifications/NotificationSender.php b/src/Illuminate/Notifications/NotificationSender.php index 186309779565..c647737a8440 100644 --- a/src/Illuminate/Notifications/NotificationSender.php +++ b/src/Illuminate/Notifications/NotificationSender.php @@ -10,6 +10,7 @@ use Illuminate\Notifications\Events\NotificationSending; use Illuminate\Notifications\Events\NotificationSent; use Illuminate\Queue\Attributes\Connection; +use Illuminate\Queue\Attributes\Delay; use Illuminate\Queue\Attributes\Queue as QueueAttribute; use Illuminate\Queue\Attributes\ReadsQueueAttributes; use Illuminate\Support\Collection; @@ -250,11 +251,9 @@ protected function queueNotification($notifiables, $notification) $queue = $notification->viaQueues()[$channel] ?? $queue; } - $delay = $notification->delay; - - if (method_exists($notification, 'withDelay')) { - $delay = $notification->withDelay($notifiable, $channel) ?? null; - } + $delay = method_exists($notification, 'withDelay') + ? ($notification->withDelay($notifiable, $channel) ?? null) + : $this->getAttributeValue($notification, Delay::class, 'delay'); $messageGroup = $notification->messageGroup ?? (method_exists($notification, 'messageGroup') ? $notification->messageGroup() : null); From d34c8a7ac613479aba96a0b80580e78c8922f182 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Fri, 3 Apr 2026 17:17:43 +0200 Subject: [PATCH 095/596] Add overflow option to Carbon plus and minus (#59509) Co-authored-by: Lucas Michot --- src/Illuminate/Support/Carbon.php | 26 ++++++++++++++++---------- tests/Support/SupportCarbonTest.php | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Support/Carbon.php b/src/Illuminate/Support/Carbon.php index 80d5fb3dc853..81f9fce89733 100644 --- a/src/Illuminate/Support/Carbon.php +++ b/src/Illuminate/Support/Carbon.php @@ -45,12 +45,15 @@ public function plus( int $hours = 0, int $minutes = 0, int $seconds = 0, - int $microseconds = 0 + int $microseconds = 0, + ?bool $overflow = null ): static { - return $this->add(" - $years years $months months $weeks weeks $days days - $hours hours $minutes minutes $seconds seconds $microseconds microseconds - "); + return $this->add('years', $years, $overflow) + ->add('months', $months, $overflow) + ->add(" + $weeks weeks $days days + $hours hours $minutes minutes $seconds seconds $microseconds microseconds + "); } /** @@ -64,11 +67,14 @@ public function minus( int $hours = 0, int $minutes = 0, int $seconds = 0, - int $microseconds = 0 + int $microseconds = 0, + ?bool $overflow = null ): static { - return $this->sub(" - $years years $months months $weeks weeks $days days - $hours hours $minutes minutes $seconds seconds $microseconds microseconds - "); + return $this->sub('years', $years, $overflow) + ->sub('months', $months, $overflow) + ->sub(" + $weeks weeks $days days + $hours hours $minutes minutes $seconds seconds $microseconds microseconds + "); } } diff --git a/tests/Support/SupportCarbonTest.php b/tests/Support/SupportCarbonTest.php index 6244192916fe..9500e2559469 100644 --- a/tests/Support/SupportCarbonTest.php +++ b/tests/Support/SupportCarbonTest.php @@ -141,4 +141,22 @@ public function testCreateFromUid() $uuidv7 = Carbon::createFromId('01880dfa-2825-72e4-acbb-b1e4981cf8af'); $this->assertEquals('2023-05-12 03:21:18.117000', $uuidv7->toDateTimeString('microsecond')); } + + public function testPlus(): void + { + $carbon = Carbon::parse('2026-01-31'); + $this->assertSame('2026-03-03', $carbon->plus(months: 1, overflow: true)->toDateString()); + + $carbon = Carbon::parse('2026-01-31'); + $this->assertSame('2026-02-28', $carbon->plus(months: 1, overflow: false)->toDateString()); + } + + public function testMinus(): void + { + $carbon = Carbon::parse('2026-05-31'); + $this->assertSame('2026-05-01', $carbon->minus(months: 1, overflow: true)->toDateString()); + + $carbon = Carbon::parse('2026-05-31'); + $this->assertSame('2026-04-30', $carbon->minus(months: 1, overflow: false)->toDateString()); + } } From 8df42a2c67c3b0a8ccaf5b7a84b7938712c5414a Mon Sep 17 00:00:00 2001 From: Timmy Lindholm <74464421+timmylindh@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:37:35 +0200 Subject: [PATCH 096/596] [13.x] Fix: respect null redirect in unauthenticated exception handler (#59505) * [13.x] Fix: respect null redirect in unauthenticated exception handler When `redirectGuestsTo` is configured to return null (common for pure API applications), the exception handler's `unauthenticated` method previously fell back to `route('login')` via the null coalescing operator. This caused a `RouteNotFoundException` in applications that don't define a login route. This change respects the null redirect by returning a 401 JSON response instead of falling back to `route('login')`. The default behavior for applications that do define a login route is unchanged, since `ApplicationBuilder` sets `redirectGuestsTo(fn () => route('login'))` by default, which resolves to a non-null URL. * Change response to noContent for unauthorized access --- src/Illuminate/Foundation/Exceptions/Handler.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index a98f78181edb..6853ad076f5c 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -750,9 +750,17 @@ protected function renderExceptionResponse($request, Throwable $e) */ protected function unauthenticated($request, AuthenticationException $exception) { - return $this->shouldReturnJson($request, $exception) - ? response()->json(['message' => $exception->getMessage()], 401) - : redirect()->guest($exception->redirectTo($request) ?? route('login')); + if ($this->shouldReturnJson($request, $exception)) { + return response()->json(['message' => $exception->getMessage()], 401); + } + + $redirectTo = $exception->redirectTo($request); + + if (! $redirectTo) { + return response()->noContent(401); + } + + return redirect()->guest($redirectTo); } /** From d6be8af77ce3bec95143e021cda2fff213f76e2c Mon Sep 17 00:00:00 2001 From: Timmy Lindholm Date: Sun, 5 Apr 2026 01:56:33 +0200 Subject: [PATCH 097/596] feat: redis cluster full support --- src/Illuminate/Queue/RedisQueue.php | 71 ++++- .../Redis/Connections/Connection.php | 33 +++ .../Connections/PhpRedisClusterConnection.php | 11 + .../Connections/PredisClusterConnection.php | 11 + .../Redis/Connectors/PhpRedisConnector.php | 45 ++- .../Redis/Limiters/ConcurrencyLimiter.php | 38 ++- tests/Queue/QueueRedisQueueTest.php | 272 +++++++++++++++++- tests/Redis/ConcurrencyLimiterTest.php | 227 +++++++++++++++ tests/Redis/PhpRedisConnectorTest.php | 111 +++++++ 9 files changed, 794 insertions(+), 25 deletions(-) create mode 100644 tests/Redis/ConcurrencyLimiterTest.php create mode 100644 tests/Redis/PhpRedisConnectorTest.php diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index eafe8fd1b84f..93eae812b69c 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Contracts\Redis\Factory as Redis; use Illuminate\Queue\Jobs\RedisJob; +use Illuminate\Redis\Connections\Connection; use Illuminate\Redis\Connections\PhpRedisClusterConnection; use Illuminate\Redis\Connections\PredisClusterConnection; use Illuminate\Support\Str; @@ -65,6 +66,13 @@ class RedisQueue extends Queue implements QueueContract, ClearableQueue */ protected $secondaryQueueHadJob = false; + /** + * Indicates if the connection is a Redis Cluster connection. + * + * @var bool|null + */ + protected $isCluster = null; + /** * Create a new Redis queue instance. * @@ -102,7 +110,7 @@ public function __construct( */ public function size($queue = null) { - $queue = $this->getQueue($queue); + $queue = $this->getRedisKey($queue); return $this->getConnection()->eval( LuaScripts::size(), 3, $queue, $queue.':delayed', $queue.':reserved' @@ -117,7 +125,7 @@ public function size($queue = null) */ public function pendingSize($queue = null) { - return $this->getConnection()->llen($this->getQueue($queue)); + return $this->getConnection()->llen($this->getRedisKey($queue)); } /** @@ -128,7 +136,7 @@ public function pendingSize($queue = null) */ public function delayedSize($queue = null) { - return $this->getConnection()->zcard($this->getQueue($queue).':delayed'); + return $this->getConnection()->zcard($this->getRedisKey($queue).':delayed'); } /** @@ -139,7 +147,7 @@ public function delayedSize($queue = null) */ public function reservedSize($queue = null) { - return $this->getConnection()->zcard($this->getQueue($queue).':reserved'); + return $this->getConnection()->zcard($this->getRedisKey($queue).':reserved'); } /** @@ -150,7 +158,7 @@ public function reservedSize($queue = null) */ public function creationTimeOfOldestPendingJob($queue = null) { - $payload = $this->getConnection()->lindex($this->getQueue($queue), 0); + $payload = $this->getConnection()->lindex($this->getRedisKey($queue), 0); if (! $payload) { return null; @@ -223,9 +231,11 @@ function ($payload, $queue) { */ public function pushRaw($payload, $queue = null, array $options = []) { + $queue = $this->getRedisKey($queue); + $this->getConnection()->eval( - LuaScripts::push(), 2, $this->getQueue($queue), - $this->getQueue($queue).':notify', $payload + LuaScripts::push(), 2, $queue, + $queue.':notify', $payload ); return json_decode($payload, true)['id'] ?? null; @@ -264,7 +274,7 @@ function ($payload, $queue, $delay) { protected function laterRaw($delay, $payload, $queue = null) { $this->getConnection()->eval( - LuaScripts::later(), 1, $this->getQueue($queue).':delayed', + LuaScripts::later(), 1, $this->getRedisKey($queue).':delayed', $this->availableAt($delay), $payload ); @@ -296,7 +306,7 @@ protected function createPayloadArray($job, $queue, $data = '') */ public function pop($queue = null, $index = 0) { - $this->migrate($prefixed = $this->getQueue($queue)); + $this->migrate($prefixed = $this->getRedisKey($queue)); $block = ! $this->secondaryQueueHadJob && $index == 0; @@ -384,7 +394,7 @@ protected function retrieveNextJob($queue, $block = true) */ public function deleteReserved($queue, $job) { - $this->getConnection()->zrem($this->getQueue($queue).':reserved', $job->getReservedJob()); + $this->getConnection()->zrem($this->getRedisKey($queue).':reserved', $job->getReservedJob()); } /** @@ -397,7 +407,7 @@ public function deleteReserved($queue, $job) */ public function deleteAndRelease($queue, $job, $delay) { - $queue = $this->getQueue($queue); + $queue = $this->getRedisKey($queue); $this->getConnection()->eval( LuaScripts::release(), 2, $queue.':delayed', $queue.':reserved', @@ -413,7 +423,7 @@ public function deleteAndRelease($queue, $job, $delay) */ public function clear($queue) { - $queue = $this->getQueue($queue); + $queue = $this->getRedisKey($queue); return $this->getConnection()->eval( LuaScripts::clear(), 4, $queue, $queue.':delayed', @@ -442,6 +452,43 @@ public function getQueue($queue) return 'queues:'.($queue ?: $this->default); } + /** + * Get the cluster-safe Redis key for the given queue. + * + * When connected to a Redis Cluster, queue names are wrapped in hash tags + * to ensure all related keys (queue, delayed, reserved, notify) hash to the + * same slot, which is required for multi-key Lua scripts. + * + * @param string|null $queue + * @return string + */ + protected function getRedisKey($queue = null) + { + $queue = $queue ?: $this->default; + + if ($this->isClusterConnection() && ! Connection::hasHashTag($queue)) { + return $this->getQueue('{'.$queue.'}'); + } + + return $this->getQueue($queue); + } + + /** + * Determine if the connection is a Redis Cluster connection. + * + * The result is cached for the lifetime of this queue instance. + * + * @return bool + */ + protected function isClusterConnection() + { + if (is_null($this->isCluster)) { + $this->isCluster = $this->getConnection()->isCluster(); + } + + return $this->isCluster; + } + /** * Get the connection for the queue. * diff --git a/src/Illuminate/Redis/Connections/Connection.php b/src/Illuminate/Redis/Connections/Connection.php index 5f98ba0d045b..5451a22c8d35 100644 --- a/src/Illuminate/Redis/Connections/Connection.php +++ b/src/Illuminate/Redis/Connections/Connection.php @@ -236,6 +236,39 @@ public function unsetEventDispatcher() $this->events = null; } + /** + * Determine if the connection is a cluster connection. + * + * @return bool + */ + public function isCluster() + { + return false; + } + + /** + * Determine if the given key contains a Redis Cluster hash tag. + * + * A hash tag is a substring enclosed in braces with at least one character + * between them (e.g., "{user}:sessions"). Empty braces ("{}") are not + * considered a valid hash tag. + * + * @param string $key + * @return bool + */ + public static function hasHashTag(string $key): bool + { + $open = strpos($key, '{'); + + if ($open === false) { + return false; + } + + $close = strpos($key, '}', $open + 1); + + return $close !== false && $close - $open > 1; + } + /** * Pass other method calls down to the underlying client. * diff --git a/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php b/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php index b49229ac8bc2..9a31dd25b367 100644 --- a/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php +++ b/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php @@ -20,6 +20,17 @@ class PhpRedisClusterConnection extends PhpRedisConnection */ protected $defaultNode; + /** + * Determine if the connection is a cluster connection. + * + * @return bool + */ + #[\Override] + public function isCluster() + { + return true; + } + /** * Scan all keys based on the given options. * diff --git a/src/Illuminate/Redis/Connections/PredisClusterConnection.php b/src/Illuminate/Redis/Connections/PredisClusterConnection.php index f0a9ad333b8d..8d5f66a85daa 100644 --- a/src/Illuminate/Redis/Connections/PredisClusterConnection.php +++ b/src/Illuminate/Redis/Connections/PredisClusterConnection.php @@ -7,6 +7,17 @@ class PredisClusterConnection extends PredisConnection { + /** + * Determine if the connection is a cluster connection. + * + * @return bool + */ + #[\Override] + public function isCluster() + { + return true; + } + /** * Get the keys that match the given pattern. * diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 7391e68ce540..4247e9d73ab0 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -202,12 +202,14 @@ protected function createRedisClusterInstance(array $servers, array $options) isset($options['persistent']) && $options['persistent'], ]; - if (version_compare(phpversion('redis'), '4.3.0', '>=')) { - $parameters[] = $options['password'] ?? null; - } + if (version_compare(phpversion('redis'), '5.3.2', '>=')) { + $parameters[] = $this->formatClusterPassword($options); - if (version_compare(phpversion('redis'), '5.3.2', '>=') && ! is_null($context = Arr::get($options, 'context'))) { - $parameters[] = $context; + if (! is_null($context = Arr::get($options, 'context'))) { + $parameters[] = $context; + } + } elseif (version_compare(phpversion('redis'), '4.3.0', '>=')) { + $parameters[] = $options['password'] ?? null; } return tap(new RedisCluster(...$parameters), function ($client) use ($options) { @@ -238,9 +240,42 @@ protected function createRedisClusterInstance(array $servers, array $options) if (! empty($options['tcp_keepalive'])) { $client->setOption(Redis::OPT_TCP_KEEPALIVE, $options['tcp_keepalive']); } + + if (array_key_exists('max_retries', $options)) { + $client->setOption(Redis::OPT_MAX_RETRIES, $options['max_retries']); + } + + if (array_key_exists('backoff_algorithm', $options)) { + $client->setOption(Redis::OPT_BACKOFF_ALGORITHM, $this->parseBackoffAlgorithm($options['backoff_algorithm'])); + } + + if (array_key_exists('backoff_base', $options)) { + $client->setOption(Redis::OPT_BACKOFF_BASE, $options['backoff_base']); + } + + if (array_key_exists('backoff_cap', $options)) { + $client->setOption(Redis::OPT_BACKOFF_CAP, $options['backoff_cap']); + } }); } + /** + * Format the password for a Redis cluster connection. + * + * @param array $options + * @return string|array|null + */ + protected function formatClusterPassword(array $options) + { + $password = $options['password'] ?? null; + + if (isset($options['username']) && $options['username'] !== '' && is_string($password)) { + return [$options['username'], $password]; + } + + return $password; + } + /** * Format the host using the scheme if available. * diff --git a/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php b/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php index 02c17870862a..19915e2cfdd7 100644 --- a/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php +++ b/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php @@ -3,6 +3,7 @@ namespace Illuminate\Redis\Limiters; use Illuminate\Contracts\Redis\LimiterTimeoutException; +use Illuminate\Redis\Connections\Connection; use Illuminate\Support\Sleep; use Illuminate\Support\Str; use Throwable; @@ -37,6 +38,13 @@ class ConcurrencyLimiter */ protected $releaseAfter; + /** + * The cluster-safe key prefix for lock slots. + * + * @var string|null + */ + protected $prefix; + /** * Create a new concurrency limiter instance. * @@ -101,16 +109,40 @@ public function block($timeout, $callback = null, $sleep = 250) */ protected function acquire($id) { - $slots = array_map(function ($i) { - return $this->name.$i; + $prefix = $this->getPrefix(); + + $slots = array_map(function ($i) use ($prefix) { + return $prefix.$i; }, range(1, $this->maxLocks)); + // The Lua lockScript returns ARGV[1]..index (i.e. prefix concatenated with + // the slot index). The release() method uses that return value as KEYS[1], + // so the two must stay in sync — any change to $prefix here must be + // reflected in the Lua script's return expression. return $this->redis->eval(...array_merge( [$this->lockScript(), count($slots)], - array_merge($slots, [$this->name, $this->releaseAfter, $id]) + array_merge($slots, [$prefix, $this->releaseAfter, $id]) )); } + /** + * Get the cluster-safe key prefix for lock slots. + * + * The result is cached for the lifetime of this limiter instance. + * + * @return string + */ + protected function getPrefix() + { + if (is_null($this->prefix)) { + $this->prefix = $this->redis->isCluster() && ! Connection::hasHashTag($this->name) + ? '{'.$this->name.'}' + : $this->name; + } + + return $this->prefix; + } + /** * Get the Lua script for acquiring a lock. * diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index e04e3e0be9f1..108a43b76e59 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -7,6 +7,8 @@ use Illuminate\Queue\LuaScripts; use Illuminate\Queue\Queue; use Illuminate\Queue\RedisQueue; +use Illuminate\Redis\Connections\PhpRedisClusterConnection; +use Illuminate\Redis\Connections\PredisClusterConnection; use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Mockery as m; @@ -14,6 +16,12 @@ class QueueRedisQueueTest extends TestCase { + protected function tearDown(): void + { + m::close(); + parent::tearDown(); + } + public function testPushProperlyPushesJobOntoRedis() { $uuid = Str::uuid(); @@ -28,7 +36,8 @@ public function testPushProperlyPushesJobOntoRedis() $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->setContainer($container = m::spy(Container::class)); - $redis->shouldReceive('connection')->once()->andReturn($redis); + $redis->shouldReceive('connection')->atLeast()->once()->andReturn($redis); + $redis->shouldReceive('isCluster')->andReturn(false); $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])); $id = $queue->push('foo', ['data']); @@ -53,7 +62,8 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook() $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->setContainer($container = m::spy(Container::class)); - $redis->shouldReceive('connection')->once()->andReturn($redis); + $redis->shouldReceive('connection')->atLeast()->once()->andReturn($redis); + $redis->shouldReceive('isCluster')->andReturn(false); $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'custom' => 'taylor', 'id' => 'foo', 'attempts' => 0, 'delay' => null])); Queue::createPayloadUsing(function ($connection, $queue, $payload) { @@ -84,7 +94,8 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook() $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->setContainer($container = m::spy(Container::class)); - $redis->shouldReceive('connection')->once()->andReturn($redis); + $redis->shouldReceive('connection')->atLeast()->once()->andReturn($redis); + $redis->shouldReceive('isCluster')->andReturn(false); $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'custom' => 'taylor', 'bar' => 'foo', 'id' => 'foo', 'attempts' => 0, 'delay' => null])); Queue::createPayloadUsing(function ($connection, $queue, $payload) { @@ -121,7 +132,8 @@ public function testDelayedPushProperlyPushesJobOntoRedis() $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->expects($this->once())->method('availableAt')->with(1)->willReturn(2); - $redis->shouldReceive('connection')->once()->andReturn($redis); + $redis->shouldReceive('connection')->atLeast()->once()->andReturn($redis); + $redis->shouldReceive('isCluster')->andReturn(false); $redis->shouldReceive('eval')->once()->with( LuaScripts::later(), 1, @@ -153,7 +165,8 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis() $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->expects($this->once())->method('availableAt')->with($date)->willReturn(5); - $redis->shouldReceive('connection')->once()->andReturn($redis); + $redis->shouldReceive('connection')->atLeast()->once()->andReturn($redis); + $redis->shouldReceive('isCluster')->andReturn(false); $redis->shouldReceive('eval')->once()->with( LuaScripts::later(), 1, @@ -168,4 +181,253 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis() Carbon::setTestNow(); Str::createUuidsNormally(); } + + public function testGetQueueRemainsUnchangedForNonCluster() + { + $queue = new RedisQueue($redis = m::mock(Factory::class), 'default'); + $this->assertSame('queues:default', $queue->getQueue(null)); + $this->assertSame('queues:emails', $queue->getQueue('emails')); + } + + public function testGetQueueRemainsUnchangedForCluster() + { + $queue = new RedisQueue($redis = m::mock(Factory::class), 'default'); + $redis->shouldReceive('connection')->andReturn(m::mock(PhpRedisClusterConnection::class)); + + // getQueue() should NOT add hash tags — it's unchanged + $this->assertSame('queues:default', $queue->getQueue(null)); + $this->assertSame('queues:emails', $queue->getQueue('emails')); + } + + public function testGetRedisKeyReturnsPlainKeyForNonCluster() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(\Illuminate\Redis\Connections\Connection::class); + $connection->shouldReceive('isCluster')->andReturn(false); + $redis->shouldReceive('connection')->andReturn($connection); + + $this->assertSame('queues:default', $queue->testGetRedisKey(null)); + $this->assertSame('queues:emails', $queue->testGetRedisKey('emails')); + } + + public function testGetRedisKeyWrapsWithHashTagsForPhpRedisCluster() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + $this->assertSame('queues:{default}', $queue->testGetRedisKey(null)); + $this->assertSame('queues:{emails}', $queue->testGetRedisKey('emails')); + } + + public function testGetRedisKeyWrapsWithHashTagsForPredisCluster() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PredisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + $this->assertSame('queues:{default}', $queue->testGetRedisKey(null)); + $this->assertSame('queues:{emails}', $queue->testGetRedisKey('emails')); + } + + public function testGetRedisKeyDoesNotDoubleWrapExistingHashTags() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), '{default}'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + $this->assertSame('queues:{default}', $queue->testGetRedisKey(null)); + $this->assertSame('queues:{custom}', $queue->testGetRedisKey('{custom}')); + } + + public function testGetRedisKeySkipsWrappingWhenQueueNameContainsBraces() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + // Queue name already contains hash tags — skip wrapping + $this->assertSame('queues:process-{batch}-results', $queue->testGetRedisKey('process-{batch}-results')); + } + + public function testGetRedisKeyWrapsEmptyHashTagOnCluster() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + // Empty braces '{}' are not a valid hash tag — should still get wrapped + $this->assertSame('queues:{my{}queue}', $queue->testGetRedisKey('my{}queue')); + } + + public function testGetRedisKeyWrapsUnmatchedOpeningBrace() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + // Unmatched '{' is not a valid hash tag — should still get wrapped + $this->assertSame('queues:{my{broken}', $queue->testGetRedisKey('my{broken')); + } + + public function testGetRedisKeyWrapsUnmatchedClosingBrace() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + // Unmatched '}' is not a valid hash tag — should still get wrapped + $this->assertSame('queues:{broken}queue}', $queue->testGetRedisKey('broken}queue')); + } + + public function testGetRedisKeyWrapsEmptyFirstHashTagFollowedByValidPair() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($connection); + + // Redis spec: the first '{}' is an empty hash tag, so the whole key is hashed + // even though '{bar}' looks valid. Must be wrapped to ensure slot affinity. + $this->assertSame('queues:{foo{}{bar}}', $queue->testGetRedisKey('foo{}{bar}')); + } + + public function testPushUsesGetRedisKeyForLuaScript() + { + $uuid = Str::uuid(); + + Str::createUuidsUsing(function () use ($uuid) { + return $uuid; + }); + + $time = Carbon::now(); + Carbon::setTestNow($time); + + $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); + $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); + $queue->setContainer($container = m::spy(Container::class)); + + $clusterConnection = m::mock(PhpRedisClusterConnection::class)->shouldIgnoreMissing(); + $clusterConnection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($clusterConnection); + + // command() is called by eval() — assert it receives hash-tagged keys + $clusterConnection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return $args[0] === LuaScripts::push() + && $args[2] === 2 + && $args[1][0] === 'queues:{default}' + && $args[1][1] === 'queues:{default}:notify'; + }))->andReturn(null); + + $queue->push('foo', ['data']); + + Carbon::setTestNow(); + Str::createUuidsNormally(); + } + + public function testPushPassesUnchangedQueueToCreatePayload() + { + $uuid = Str::uuid(); + + Str::createUuidsUsing(function () use ($uuid) { + return $uuid; + }); + + $time = Carbon::now(); + Carbon::setTestNow($time); + + $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); + $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); + $queue->setContainer($container = m::spy(Container::class)); + + $clusterConnection = m::mock(PhpRedisClusterConnection::class)->shouldIgnoreMissing(); + $clusterConnection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($clusterConnection); + + $receivedQueue = null; + Queue::createPayloadUsing(function ($connection, $queue) use (&$receivedQueue) { + $receivedQueue = $queue; + + return []; + }); + + $queue->push('foo', ['data']); + + // Payload hook should receive the unchanged getQueue() output (no hash tags) + $this->assertSame('queues:default', $receivedQueue); + + Queue::createPayloadUsing(null); + Carbon::setTestNow(); + Str::createUuidsNormally(); + } + + public function testSizeUsesGetRedisKeyOnCluster() + { + $queue = new RedisQueue($redis = m::mock(Factory::class), 'default'); + $clusterConnection = m::mock(PhpRedisClusterConnection::class)->shouldIgnoreMissing(); + $clusterConnection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($clusterConnection); + + $clusterConnection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return $args[0] === LuaScripts::size() + && $args[2] === 3 + && $args[1][0] === 'queues:{default}' + && $args[1][1] === 'queues:{default}:delayed' + && $args[1][2] === 'queues:{default}:reserved'; + }))->andReturn(5); + + $this->assertSame(5, $queue->size()); + } + + public function testClearUsesGetRedisKeyOnCluster() + { + $queue = new RedisQueue($redis = m::mock(Factory::class), 'default'); + $clusterConnection = m::mock(PhpRedisClusterConnection::class)->shouldIgnoreMissing(); + $clusterConnection->shouldReceive('isCluster')->andReturn(true); + $redis->shouldReceive('connection')->andReturn($clusterConnection); + + $clusterConnection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return $args[0] === LuaScripts::clear() + && $args[2] === 4 + && $args[1][0] === 'queues:{default}' + && $args[1][1] === 'queues:{default}:delayed' + && $args[1][2] === 'queues:{default}:reserved' + && $args[1][3] === 'queues:{default}:notify'; + }))->andReturn(3); + + $this->assertSame(3, $queue->clear('default')); + } + + public function testIsClusterConnectionCachesResult() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->once()->andReturn(true); + $redis->shouldReceive('connection')->once()->andReturn($connection); + + // Multiple calls should only trigger one connection() call + $this->assertTrue($queue->testIsClusterConnection()); + $this->assertTrue($queue->testIsClusterConnection()); + $this->assertTrue($queue->testIsClusterConnection()); + } +} + +class TestableRedisQueue extends RedisQueue +{ + public function testGetRedisKey($queue = null) + { + return $this->getRedisKey($queue); + } + + public function testIsClusterConnection() + { + return $this->isClusterConnection(); + } } diff --git a/tests/Redis/ConcurrencyLimiterTest.php b/tests/Redis/ConcurrencyLimiterTest.php new file mode 100644 index 000000000000..c656fb4b432e --- /dev/null +++ b/tests/Redis/ConcurrencyLimiterTest.php @@ -0,0 +1,227 @@ +shouldReceive('isCluster')->andReturn(true); + + // acquire() calls eval → command('eval', ...) with the lock script + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'mget') + && $args[2] === 3 + && $args[1][0] === '{test-limiter}1' + && $args[1][1] === '{test-limiter}2' + && $args[1][2] === '{test-limiter}3' + && $args[1][3] === '{test-limiter}'; // ARGV[1] = hash-tagged prefix + }))->andReturn('{test-limiter}1'); + + // release() also calls eval → command('eval', ...) with the release script + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'del') + && $args[1][0] === '{test-limiter}1'; // released key matches acquired key + }))->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'test-limiter', 3, 60); + $result = $limiter->block(0, function () { + return 'executed'; + }); + + $this->assertSame('executed', $result); + } + + public function testAcquireUsesPlainKeysOnNonClusterConnection() + { + $connection = m::mock(PhpRedisConnection::class); + $connection->shouldReceive('isCluster')->andReturn(false); + + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'mget') + && $args[2] === 2 + && $args[1][0] === 'mylock1' + && $args[1][1] === 'mylock2' + && $args[1][2] === 'mylock'; // ARGV[1] = plain name + }))->andReturn('mylock1'); + + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'del') + && $args[1][0] === 'mylock1'; + }))->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'mylock', 2, 60); + $result = $limiter->block(0, function () { + return 'done'; + }); + + $this->assertSame('done', $result); + } + + public function testAcquireUsesHashTagsOnPredisClusterConnection() + { + $connection = m::mock(PredisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + + $connection->shouldReceive('eval')->once()->with( + m::on(fn ($s) => str_contains($s, 'mget')), + 2, + '{limiter}1', '{limiter}2', + '{limiter}', m::any(), m::any() + )->andReturn('{limiter}1'); + + $connection->shouldReceive('eval')->once()->with( + m::on(fn ($s) => str_contains($s, 'del')), + 1, + '{limiter}1', m::any() + )->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'limiter', 2, 60); + $result = $limiter->block(0, function () { + return 'ok'; + }); + + $this->assertSame('ok', $result); + } + + public function testReleaseKeyMatchesAcquireKeyOnCluster() + { + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + + // Acquire returns the slot key + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'mget'); + }))->andReturn('{mykey}2'); + + // Release should be called with the exact same key + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'del') + && $args[1][0] === '{mykey}2'; + }))->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'mykey', 3, 60); + $limiter->block(0, function () { + // callback runs between acquire and release + }); + } + + public function testAcquireDoesNotDoubleWrapPreExistingHashTags() + { + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + + // Name already has hash tags — should NOT be double-wrapped + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'mget') + && $args[1][0] === '{mylock}1' + && $args[1][1] === '{mylock}2' + && $args[1][2] === '{mylock}'; // ARGV[1] = unchanged name with existing tags + }))->andReturn('{mylock}1'); + + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'del') + && $args[1][0] === '{mylock}1'; + }))->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, '{mylock}', 2, 60); + $result = $limiter->block(0, function () { + return 'ok'; + }); + + $this->assertSame('ok', $result); + } + + public function testAcquireWrapsUnmatchedBraceOnCluster() + { + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + + // Name has '{' but no '}' — not a valid hash tag, should be wrapped + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'mget') + && $args[1][0] === '{my{lock}1' + && $args[1][1] === '{my{lock}2' + && $args[1][2] === '{my{lock}'; // ARGV[1] = wrapped prefix + }))->andReturn('{my{lock}1'); + + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'del') + && $args[1][0] === '{my{lock}1'; + }))->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'my{lock', 2, 60); + $result = $limiter->block(0, function () { + return 'ok'; + }); + + $this->assertSame('ok', $result); + } + + public function testAcquireWrapsEmptyBracesOnCluster() + { + $connection = m::mock(PhpRedisClusterConnection::class); + $connection->shouldReceive('isCluster')->andReturn(true); + + // Name has '{}' but that's an empty hash tag — should be wrapped + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'mget') + && $args[1][0] === '{my{}lock}1' + && $args[1][1] === '{my{}lock}2' + && $args[1][2] === '{my{}lock}'; // ARGV[1] = wrapped prefix + }))->andReturn('{my{}lock}1'); + + $connection->shouldReceive('command')->once()->with('eval', m::on(function ($args) { + return str_contains($args[0], 'del') + && $args[1][0] === '{my{}lock}1'; + }))->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'my{}lock', 2, 60); + $result = $limiter->block(0, function () { + return 'ok'; + }); + + $this->assertSame('ok', $result); + } + + public function testAcquireUsesPlainKeysOnPredisNonClusterConnection() + { + $connection = m::mock(PredisConnection::class); + $connection->shouldReceive('isCluster')->andReturn(false); + + $connection->shouldReceive('eval')->once()->with( + m::on(fn ($s) => str_contains($s, 'mget')), + 2, + 'lock1', 'lock2', + 'lock', m::any(), m::any() + )->andReturn('lock1'); + + $connection->shouldReceive('eval')->once()->with( + m::on(fn ($s) => str_contains($s, 'del')), + 1, + 'lock1', m::any() + )->andReturn(1); + + $limiter = new ConcurrencyLimiter($connection, 'lock', 2, 60); + $result = $limiter->block(0, function () { + return 'success'; + }); + + $this->assertSame('success', $result); + } +} diff --git a/tests/Redis/PhpRedisConnectorTest.php b/tests/Redis/PhpRedisConnectorTest.php new file mode 100644 index 000000000000..daf253433efa --- /dev/null +++ b/tests/Redis/PhpRedisConnectorTest.php @@ -0,0 +1,111 @@ +testFormatClusterPassword([ + 'username' => 'myuser', + 'password' => 'mypass', + ]); + + $this->assertSame(['myuser', 'mypass'], $result); + } + + public function testFormatClusterPasswordReturnsPlainPasswordWithoutUsername() + { + $connector = new TestablePhpRedisConnector; + + $result = $connector->testFormatClusterPassword([ + 'password' => 'mypass', + ]); + + $this->assertSame('mypass', $result); + } + + public function testFormatClusterPasswordReturnsNullWhenNoPasswordProvided() + { + $connector = new TestablePhpRedisConnector; + + $result = $connector->testFormatClusterPassword([]); + + $this->assertNull($result); + } + + public function testFormatClusterPasswordReturnsPlainPasswordWhenUsernameIsEmpty() + { + $connector = new TestablePhpRedisConnector; + + $result = $connector->testFormatClusterPassword([ + 'username' => '', + 'password' => 'mypass', + ]); + + $this->assertSame('mypass', $result); + } + + public function testFormatClusterPasswordReturnsPlainPasswordWhenPasswordIsNotString() + { + $connector = new TestablePhpRedisConnector; + + $result = $connector->testFormatClusterPassword([ + 'username' => 'myuser', + 'password' => ['mypass'], + ]); + + $this->assertSame(['mypass'], $result); + } + + public function testParseBackoffAlgorithmReturnsIntegerAsIs() + { + $connector = new TestablePhpRedisConnector; + + $this->assertSame(42, $connector->testParseBackoffAlgorithm(42)); + } + + public function testParseBackoffAlgorithmParsesValidNames() + { + if (! extension_loaded('redis')) { + $this->markTestSkipped('Requires phpredis extension.'); + } + + $connector = new TestablePhpRedisConnector; + + $this->assertSame(\Redis::BACKOFF_ALGORITHM_DEFAULT, $connector->testParseBackoffAlgorithm('default')); + $this->assertSame(\Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER, $connector->testParseBackoffAlgorithm('decorrelated_jitter')); + $this->assertSame(\Redis::BACKOFF_ALGORITHM_EQUAL_JITTER, $connector->testParseBackoffAlgorithm('equal_jitter')); + $this->assertSame(\Redis::BACKOFF_ALGORITHM_EXPONENTIAL, $connector->testParseBackoffAlgorithm('exponential')); + $this->assertSame(\Redis::BACKOFF_ALGORITHM_UNIFORM, $connector->testParseBackoffAlgorithm('uniform')); + $this->assertSame(\Redis::BACKOFF_ALGORITHM_CONSTANT, $connector->testParseBackoffAlgorithm('constant')); + } + + public function testParseBackoffAlgorithmThrowsForInvalidName() + { + $connector = new TestablePhpRedisConnector; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Algorithm [bogus] is not a valid PhpRedis backoff algorithm.'); + + $connector->testParseBackoffAlgorithm('bogus'); + } +} + +class TestablePhpRedisConnector extends PhpRedisConnector +{ + public function testFormatClusterPassword(array $options) + { + return $this->formatClusterPassword($options); + } + + public function testParseBackoffAlgorithm(mixed $algorithm): int + { + return $this->parseBackoffAlgorithm($algorithm); + } +} From be799ce272d446709c4c7a737d0de9ce22c00c19 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:08:23 +0600 Subject: [PATCH 098/596] [13.x] Fix TypeError in starts_with/ends_with validation rules on non-string values (#59541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [13.x] Fix TypeError in starts_with/ends_with validation rules on non-string values The starts_with, ends_with, doesnt_start_with, and doesnt_end_with validation rules pass the value directly to Str::startsWith() / Str::endsWith() without checking if it's a string. When an array is provided, PHP throws a TypeError instead of returning a validation error. Fixes #59521 * Also fix lowercase and uppercase rules for non-string values Same TypeError occurs with lowercase and uppercase validation rules when an array is passed — Str::lower() and Str::upper() throw TypeError on non-string input. * Also fix ascii rule for non-string values Str::isAscii() generates a warning and returns true incorrectly when passed an array. * Also fix hex_color, max_digits, and min_digits for non-string values hex_color crashes with TypeError from preg_match on array input. max_digits and min_digits crash with TypeError from preg_match on array input. Added is_string/is_numeric guards matching the pattern used by alpha_dash, alpha_num, digits, and regex rules. --- .../Concerns/ValidatesAttributes.php | 24 +++++--- tests/Validation/ValidationValidatorTest.php | 56 +++++++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 9e1b63353dc4..6bf975678401 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -159,7 +159,7 @@ protected function getDnsRecords($hostname, $type) */ public function validateAscii($attribute, $value) { - return Str::isAscii($value); + return is_string($value) && Str::isAscii($value); } /** @@ -1453,7 +1453,7 @@ public function validateLte($attribute, $value, $parameters) */ public function validateLowercase($attribute, $value, $parameters) { - return Str::lower($value) === $value; + return is_string($value) && Str::lower($value) === $value; } /** @@ -1466,7 +1466,7 @@ public function validateLowercase($attribute, $value, $parameters) */ public function validateUppercase($attribute, $value, $parameters) { - return Str::upper($value) === $value; + return is_string($value) && Str::upper($value) === $value; } /** @@ -1478,7 +1478,7 @@ public function validateUppercase($attribute, $value, $parameters) */ public function validateHexColor($attribute, $value) { - return preg_match('/^#(?:(?:[0-9a-f]{3}){1,2}|(?:[0-9a-f]{4}){1,2})$/i', $value) === 1; + return is_string($value) && preg_match('/^#(?:(?:[0-9a-f]{3}){1,2}|(?:[0-9a-f]{4}){1,2})$/i', $value) === 1; } /** @@ -1693,6 +1693,10 @@ public function validateMaxDigits($attribute, $value, $parameters) { $this->requireParameterCount(1, $parameters, 'max_digits'); + if (! is_string($value) && ! is_numeric($value)) { + return false; + } + $length = strlen((string) $value); return ! preg_match('/[^0-9]/', $value) && $length <= $parameters[0]; @@ -1799,6 +1803,10 @@ public function validateMinDigits($attribute, $value, $parameters) { $this->requireParameterCount(1, $parameters, 'min_digits'); + if (! is_string($value) && ! is_numeric($value)) { + return false; + } + $length = strlen((string) $value); return ! preg_match('/[^0-9]/', $value) && $length >= $parameters[0]; @@ -2636,7 +2644,7 @@ public function validateSometimes() */ public function validateStartsWith($attribute, $value, $parameters) { - return Str::startsWith($value, $parameters); + return is_string($value) && Str::startsWith($value, $parameters); } /** @@ -2649,7 +2657,7 @@ public function validateStartsWith($attribute, $value, $parameters) */ public function validateDoesntStartWith($attribute, $value, $parameters) { - return ! Str::startsWith($value, $parameters); + return is_string($value) && ! Str::startsWith($value, $parameters); } /** @@ -2662,7 +2670,7 @@ public function validateDoesntStartWith($attribute, $value, $parameters) */ public function validateEndsWith($attribute, $value, $parameters) { - return Str::endsWith($value, $parameters); + return is_string($value) && Str::endsWith($value, $parameters); } /** @@ -2675,7 +2683,7 @@ public function validateEndsWith($attribute, $value, $parameters) */ public function validateDoesntEndWith($attribute, $value, $parameters) { - return ! Str::endsWith($value, $parameters); + return is_string($value) && ! Str::endsWith($value, $parameters); } /** diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 197c2b7e185e..d004acb5b883 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -3209,6 +3209,62 @@ public function testValidateStartsWith() $this->assertSame('The url must start with one of the following values http, https', $v->messages()->first('url')); } + public function testValidateStartsWithDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array', 'value']], ['x' => 'starts_with:arr']); + $this->assertFalse($v->passes()); + } + + public function testValidateEndsWithDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array', 'value']], ['x' => 'ends_with:ue']); + $this->assertFalse($v->passes()); + } + + public function testValidateLowercaseDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'lowercase']); + $this->assertFalse($v->passes()); + } + + public function testValidateUppercaseDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'uppercase']); + $this->assertFalse($v->passes()); + } + + public function testValidateAsciiDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'ascii']); + $this->assertFalse($v->passes()); + } + + public function testValidateHexColorDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'hex_color']); + $this->assertFalse($v->passes()); + } + + public function testValidateMaxDigitsDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'max_digits:5']); + $this->assertFalse($v->passes()); + } + + public function testValidateMinDigitsDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'min_digits:1']); + $this->assertFalse($v->passes()); + } + public function testValidateDoesntStartWith() { $trans = $this->getIlluminateArrayTranslator(); From 56f3f0de622dad228d264f1da43eb1b8650a7473 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sun, 5 Apr 2026 16:09:21 +0200 Subject: [PATCH 099/596] Document thrown exceptions in FilesystemAdapter (#59534) --- src/Illuminate/Filesystem/FilesystemAdapter.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index d96a0b095c6d..c30f338eb8bc 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -323,6 +323,8 @@ public function json($path, $flags = 0) * @param array $headers * @param string|null $disposition * @return \Symfony\Component\HttpFoundation\StreamedResponse + * + * @throws UnableToRetrieveMetadata */ public function response($path, $name = null, array $headers = [], $disposition = 'inline') { @@ -360,6 +362,8 @@ public function response($path, $name = null, array $headers = [], $disposition * @param string|null $name * @param array $headers * @return \Symfony\Component\HttpFoundation\StreamedResponse + * + * @throws UnableToRetrieveMetadata */ public function serve(Request $request, $path, $name = null, array $headers = []) { @@ -375,6 +379,8 @@ public function serve(Request $request, $path, $name = null, array $headers = [] * @param string|null $name * @param array $headers * @return \Symfony\Component\HttpFoundation\StreamedResponse + * + * @throws UnableToRetrieveMetadata */ public function download($path, $name = null, array $headers = []) { @@ -663,6 +669,8 @@ public function checksum(string $path, array $options = []) * * @param string $path * @return string|false + * + * @throws UnableToRetrieveMetadata */ public function mimeType($path) { From 51c7f3b9650c66a32f1986d280fadaefde522c18 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sun, 5 Apr 2026 16:10:16 +0200 Subject: [PATCH 100/596] Hint \Redis `@mixin` on Connection (#59532) --- src/Illuminate/Redis/Connections/Connection.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Redis/Connections/Connection.php b/src/Illuminate/Redis/Connections/Connection.php index 3d9758e89ffd..e45c6cd2aba5 100644 --- a/src/Illuminate/Redis/Connections/Connection.php +++ b/src/Illuminate/Redis/Connections/Connection.php @@ -11,6 +11,9 @@ use Illuminate\Support\Traits\Macroable; use Throwable; +/** + * @mixin \Redis + */ abstract class Connection { use Macroable { From 31212c1a003e3984d2545103e784fd5fb2039990 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:10:43 +0000 Subject: [PATCH 101/596] Update facade docblocks --- src/Illuminate/Support/Facades/Redis.php | 273 +++++++++++++++++++++++ 1 file changed, 273 insertions(+) diff --git a/src/Illuminate/Support/Facades/Redis.php b/src/Illuminate/Support/Facades/Redis.php index 1b467126fa8a..848d06153e70 100755 --- a/src/Illuminate/Support/Facades/Redis.php +++ b/src/Illuminate/Support/Facades/Redis.php @@ -30,6 +30,279 @@ * @method static bool hasMacro(string $name) * @method static void flushMacros() * @method static mixed macroCall(string $method, array $parameters) + * @method static string _compress(string $value) + * @method static string _uncompress(string $value) + * @method static string _prefix(string $key) + * @method static string _serialize(mixed $value) + * @method static mixed _unserialize(string $value) + * @method static string _pack(mixed $value) + * @method static mixed _unpack(string $value) + * @method static mixed acl(string $subcmd, string ...$args) + * @method static \Redis|int|false append(string $key, mixed $value) + * @method static \Redis|bool auth(mixed $credentials) + * @method static \Redis|bool bgSave() + * @method static \Redis|bool bgrewriteaof() + * @method static \Redis|array|false waitaof(int $numlocal, int $numreplicas, int $timeout) + * @method static \Redis|int|false bitcount(string $key, int $start = 0, int $end = -1, bool $bybit = false) + * @method static \Redis|int|false bitop(string $operation, string $deskey, string $srckey, string ...$other_keys) + * @method static \Redis|int|false bitpos(string $key, bool $bit, int $start = 0, int $end = -1, bool $bybit = false) + * @method static \Redis|array|false|null blPop(array|string $key_or_keys, string|int|float $timeout_or_key, mixed ...$extra_args) + * @method static \Redis|array|false|null brPop(array|string $key_or_keys, string|int|float $timeout_or_key, mixed ...$extra_args) + * @method static \Redis|string|false brpoplpush(string $src, string $dst, int|float $timeout) + * @method static \Redis|array|false bzPopMax(array|string $key, string|int $timeout_or_key, mixed ...$extra_args) + * @method static \Redis|array|false bzPopMin(array|string $key, string|int $timeout_or_key, mixed ...$extra_args) + * @method static \Redis|array|false|null bzmpop(float $timeout, array $keys, string $from, int $count = 1) + * @method static \Redis|array|false|null zmpop(array $keys, string $from, int $count = 1) + * @method static \Redis|array|false|null blmpop(float $timeout, array $keys, string $from, int $count = 1) + * @method static \Redis|array|false|null lmpop(array $keys, string $from, int $count = 1) + * @method static bool clearLastError() + * @method static bool close() + * @method static mixed config(string $operation, array|string|null $key_or_settings = null, string|null $value = null) + * @method static bool connect(string $host, int $port = 6379, float $timeout = 0, string|null $persistent_id = null, int $retry_interval = 0, float $read_timeout = 0, array|null $context = null) + * @method static \Redis|bool copy(string $src, string $dst, array|null $options = null) + * @method static \Redis|int|false dbSize() + * @method static \Redis|string debug(string $key) + * @method static \Redis|int|false decr(string $key, int $by = 1) + * @method static \Redis|int|false decrBy(string $key, int $value) + * @method static \Redis|int|false del(array|string $key, string ...$other_keys) + * @method static \Redis|int|false delifeq(string $key, mixed $value) + * @method static \Redis|bool discard() + * @method static \Redis|string|false dump(string $key) + * @method static \Redis|string|false echo(string $str) + * @method static mixed eval(string $script, array $args = [], int $num_keys = 0) + * @method static mixed eval_ro(string $script_sha, array $args = [], int $num_keys = 0) + * @method static mixed evalsha(string $sha1, array $args = [], int $num_keys = 0) + * @method static mixed evalsha_ro(string $sha1, array $args = [], int $num_keys = 0) + * @method static \Redis|array|false exec() + * @method static \Redis|int|bool exists(mixed $key, mixed ...$other_keys) + * @method static \Redis|bool expire(string $key, int $timeout, string|null $mode = null) + * @method static \Redis|bool expireAt(string $key, int $timestamp, string|null $mode = null) + * @method static \Redis|bool failover(array|null $to = null, bool $abort = false, int $timeout = 0) + * @method static \Redis|int|false expiretime(string $key) + * @method static \Redis|int|false pexpiretime(string $key) + * @method static mixed fcall(string $fn, array $keys = [], array $args = []) + * @method static mixed fcall_ro(string $fn, array $keys = [], array $args = []) + * @method static \Redis|bool flushAll(bool|null $sync = null) + * @method static \Redis|bool flushDB(bool|null $sync = null) + * @method static \Redis|array|string|bool function(string $operation, mixed ...$args) + * @method static \Redis|int|false geoadd(string $key, float $lng, float $lat, string $member, mixed ...$other_triples_and_options) + * @method static \Redis|float|false geodist(string $key, string $src, string $dst, string|null $unit = null) + * @method static \Redis|array|false geohash(string $key, string $member, string ...$other_members) + * @method static \Redis|array|false geopos(string $key, string $member, string ...$other_members) + * @method static mixed georadius(string $key, float $lng, float $lat, float $radius, string $unit, array $options = []) + * @method static mixed georadius_ro(string $key, float $lng, float $lat, float $radius, string $unit, array $options = []) + * @method static mixed georadiusbymember(string $key, string $member, float $radius, string $unit, array $options = []) + * @method static mixed georadiusbymember_ro(string $key, string $member, float $radius, string $unit, array $options = []) + * @method static array geosearch(string $key, array|string $position, array|int|float $shape, string $unit, array $options = []) + * @method static \Redis|array|int|false geosearchstore(string $dst, string $src, array|string $position, array|int|float $shape, string $unit, array $options = []) + * @method static mixed get(string $key) + * @method static \Redis|array|false getWithMeta(string $key) + * @method static mixed getAuth() + * @method static \Redis|int|false getBit(string $key, int $idx) + * @method static \Redis|string|bool getEx(string $key, array $options = []) + * @method static int getDBNum() + * @method static \Redis|string|bool getDel(string $key) + * @method static string getHost() + * @method static string|null getLastError() + * @method static int getMode() + * @method static mixed getOption(int $option) + * @method static string|null getPersistentID() + * @method static int getPort() + * @method static string|false serverName() + * @method static string|false serverVersion() + * @method static \Redis|string|false getRange(string $key, int $start, int $end) + * @method static \Redis|array|string|int|false lcs(string $key1, string $key2, array|null $options = null) + * @method static float getReadTimeout() + * @method static \Redis|string|false getset(string $key, mixed $value) + * @method static float|false getTimeout() + * @method static array getTransferredBytes() + * @method static void clearTransferredBytes() + * @method static \Redis|int|false hDel(string $key, string $field, string ...$other_fields) + * @method static \Redis|bool hExists(string $key, string $field) + * @method static mixed hGet(string $key, string $member) + * @method static \Redis|array|false hGetAll(string $key) + * @method static mixed hGetWithMeta(string $key, string $member) + * @method static \Redis|int|false hIncrBy(string $key, string $field, int $value) + * @method static \Redis|float|false hIncrByFloat(string $key, string $field, float $value) + * @method static \Redis|array|false hKeys(string $key) + * @method static \Redis|int|false hLen(string $key) + * @method static \Redis|array|false hMget(string $key, array $fields) + * @method static \Redis|array|false hgetex(string $key, array $fields, array|string|null $expiry = null) + * @method static \Redis|int|false hsetex(string $key, array $fields, array|null $expiry = null) + * @method static \Redis|array|false hgetdel(string $key, array $fields) + * @method static \Redis|bool hMset(string $key, array $fieldvals) + * @method static \Redis|array|string|false hRandField(string $key, array|null $options = null) + * @method static \Redis|int|false hSet(string $key, mixed ...$fields_and_vals) + * @method static \Redis|bool hSetNx(string $key, string $field, mixed $value) + * @method static \Redis|int|false hStrLen(string $key, string $field) + * @method static \Redis|array|false hVals(string $key) + * @method static \Redis|array|false hexpire(string $key, int $ttl, array $fields, string|null $mode = null) + * @method static \Redis|array|false hpexpire(string $key, int $ttl, array $fields, string|null $mode = null) + * @method static \Redis|array|false hexpireat(string $key, int $time, array $fields, string|null $mode = null) + * @method static \Redis|array|false hpexpireat(string $key, int $mstime, array $fields, string|null $mode = null) + * @method static \Redis|array|false httl(string $key, array $fields) + * @method static \Redis|array|false hpttl(string $key, array $fields) + * @method static \Redis|array|false hexpiretime(string $key, array $fields) + * @method static \Redis|array|false hpexpiretime(string $key, array $fields) + * @method static \Redis|array|false hpersist(string $key, array $fields) + * @method static \Redis|array|bool hscan(string $key, string|int|null $iterator, string|null $pattern = null, int $count = 0) + * @method static \Redis|int|false expiremember(string $key, string $field, int $ttl, string|null $unit = null) + * @method static \Redis|int|false expirememberat(string $key, string $field, int $timestamp) + * @method static \Redis|int|false incr(string $key, int $by = 1) + * @method static \Redis|int|false incrBy(string $key, int $value) + * @method static \Redis|float|false incrByFloat(string $key, float $value) + * @method static \Redis|array|false info(string ...$sections) + * @method static bool isConnected() + * @method static void keys(string $pattern) + * @method static void lInsert(string $key, string $pos, mixed $pivot, mixed $value) + * @method static \Redis|int|false lLen(string $key) + * @method static \Redis|string|false lMove(string $src, string $dst, string $wherefrom, string $whereto) + * @method static \Redis|string|false blmove(string $src, string $dst, string $wherefrom, string $whereto, float $timeout) + * @method static \Redis|array|string|bool lPop(string $key, int $count = 0) + * @method static \Redis|array|int|bool|null lPos(string $key, mixed $value, array|null $options = null) + * @method static \Redis|int|false lPush(string $key, mixed ...$elements) + * @method static \Redis|int|false rPush(string $key, mixed ...$elements) + * @method static \Redis|int|false lPushx(string $key, mixed $value) + * @method static \Redis|int|false rPushx(string $key, mixed $value) + * @method static \Redis|bool lSet(string $key, int $index, mixed $value) + * @method static int lastSave() + * @method static mixed lindex(string $key, int $index) + * @method static \Redis|array|false lrange(string $key, int $start, int $end) + * @method static \Redis|int|false lrem(string $key, mixed $value, int $count = 0) + * @method static \Redis|bool ltrim(string $key, int $start, int $end) + * @method static \Redis|array|false mget(array $keys) + * @method static \Redis|bool migrate(string $host, int $port, array|string $key, int $dstdb, int $timeout, bool $copy = false, bool $replace = false, mixed $credentials = null) + * @method static \Redis|bool move(string $key, int $index) + * @method static \Redis|bool mset(array $key_values) + * @method static \Redis|bool msetnx(array $key_values) + * @method static \Redis|bool multi(int $value = 1) + * @method static \Redis|string|int|false object(string $subcommand, string $key) + * @method static bool pconnect(string $host, int $port = 6379, float $timeout = 0, string|null $persistent_id = null, int $retry_interval = 0, float $read_timeout = 0, array|null $context = null) + * @method static \Redis|bool persist(string $key) + * @method static bool pexpire(string $key, int $timeout, string|null $mode = null) + * @method static \Redis|bool pexpireAt(string $key, int $timestamp, string|null $mode = null) + * @method static \Redis|int pfadd(string $key, array $elements) + * @method static \Redis|int|false pfcount(array|string $key_or_keys) + * @method static \Redis|bool pfmerge(string $dst, array $srckeys) + * @method static \Redis|string|bool ping(string|null $message = null) + * @method static \Redis|bool pipeline() + * @method static \Redis|bool psetex(string $key, int $expire, mixed $value) + * @method static \Redis|int|false pttl(string $key) + * @method static \Redis|int|false publish(string $channel, string $message) + * @method static mixed pubsub(string $command, mixed $arg = null) + * @method static \Redis|array|bool punsubscribe(array $patterns) + * @method static \Redis|array|string|bool rPop(string $key, int $count = 0) + * @method static \Redis|string|false randomKey() + * @method static mixed rawcommand(string $command, mixed ...$args) + * @method static \Redis|bool rename(string $old_name, string $new_name) + * @method static \Redis|bool renameNx(string $key_src, string $key_dst) + * @method static \Redis|bool reset() + * @method static \Redis|bool restore(string $key, int $ttl, string $value, array|null $options = null) + * @method static mixed role() + * @method static \Redis|string|false rpoplpush(string $srckey, string $dstkey) + * @method static \Redis|int|false sAdd(string $key, mixed $value, mixed ...$other_values) + * @method static int sAddArray(string $key, array $values) + * @method static \Redis|array|false sDiff(string $key, string ...$other_keys) + * @method static \Redis|int|false sDiffStore(string $dst, string $key, string ...$other_keys) + * @method static \Redis|array|false sInter(array|string $key, string ...$other_keys) + * @method static \Redis|int|false sintercard(array $keys, int $limit = -1) + * @method static \Redis|int|false sInterStore(array|string $key, string ...$other_keys) + * @method static \Redis|array|false sMembers(string $key) + * @method static \Redis|array|false sMisMember(string $key, string $member, string ...$other_members) + * @method static \Redis|bool sMove(string $src, string $dst, mixed $value) + * @method static \Redis|array|string|false sPop(string $key, int $count = 0) + * @method static mixed sRandMember(string $key, int $count = 0) + * @method static \Redis|array|false sUnion(string $key, string ...$other_keys) + * @method static \Redis|int|false sUnionStore(string $dst, string $key, string ...$other_keys) + * @method static \Redis|bool save() + * @method static array|false scan(string|int|null $iterator, string|null $pattern = null, int $count = 0, string|null $type = null) + * @method static \Redis|int|false scard(string $key) + * @method static mixed script(string $command, mixed ...$args) + * @method static \Redis|bool select(int $db) + * @method static \Redis|string|bool set(string $key, mixed $value, mixed $options = null) + * @method static \Redis|int|false setBit(string $key, int $idx, bool $value) + * @method static \Redis|int|false setRange(string $key, int $index, string $value) + * @method static bool setOption(int $option, mixed $value) + * @method static void setex(string $key, int $expire, mixed $value) + * @method static \Redis|bool setnx(string $key, mixed $value) + * @method static \Redis|bool sismember(string $key, mixed $value) + * @method static \Redis|bool replicaof(string|null $host = null, int $port = 6379) + * @method static \Redis|int|false touch(array|string $key_or_array, string ...$more_keys) + * @method static mixed slowlog(string $operation, int $length = 0) + * @method static mixed sort(string $key, array|null $options = null) + * @method static mixed sort_ro(string $key, array|null $options = null) + * @method static \Redis|int|false srem(string $key, mixed $value, mixed ...$other_values) + * @method static array|false sscan(string $key, string|int|null $iterator, string|null $pattern = null, int $count = 0) + * @method static bool ssubscribe(array $channels, callable $cb) + * @method static \Redis|int|false strlen(string $key) + * @method static \Redis|array|bool sunsubscribe(array $channels) + * @method static \Redis|bool swapdb(int $src, int $dst) + * @method static \Redis|array time() + * @method static \Redis|int|false ttl(string $key) + * @method static \Redis|int|false type(string $key) + * @method static \Redis|int|false unlink(array|string $key, string ...$other_keys) + * @method static \Redis|array|bool unsubscribe(array $channels) + * @method static \Redis|bool unwatch() + * @method static \Redis|bool watch(array|string $key, string ...$other_keys) + * @method static int|false wait(int $numreplicas, int $timeout) + * @method static int|false xack(string $key, string $group, array $ids) + * @method static \Redis|string|false xadd(string $key, string $id, array $values, int $maxlen = 0, bool $approx = false, bool $nomkstream = false) + * @method static \Redis|array|bool xautoclaim(string $key, string $group, string $consumer, int $min_idle, string $start, int $count = -1, bool $justid = false) + * @method static \Redis|array|bool xclaim(string $key, string $group, string $consumer, int $min_idle, array $ids, array $options) + * @method static \Redis|int|false xdel(string $key, array $ids) + * @method static mixed xgroup(string $operation, string|null $key = null, string|null $group = null, string|null $id_or_consumer = null, bool $mkstream = false, int $entries_read = -2) + * @method static mixed xinfo(string $operation, string|null $arg1 = null, string|null $arg2 = null, int $count = -1) + * @method static \Redis|int|false xlen(string $key) + * @method static \Redis|array|false xpending(string $key, string $group, string|null $start = null, string|null $end = null, int $count = -1, string|null $consumer = null) + * @method static \Redis|array|bool xrange(string $key, string $start, string $end, int $count = -1) + * @method static \Redis|array|bool xread(array $streams, int $count = -1, int $block = -1) + * @method static \Redis|array|bool xreadgroup(string $group, string $consumer, array $streams, int $count = 1, int $block = 1) + * @method static \Redis|array|bool xrevrange(string $key, string $end, string $start, int $count = -1) + * @method static \Redis|int|false vadd(string $key, array $values, mixed $element, array|null $options = null) + * @method static \Redis|array|false vsim(string $key, mixed $member, array|null $options = null) + * @method static \Redis|int|false vcard(string $key) + * @method static \Redis|int|false vdim(string $key) + * @method static \Redis|array|false vinfo(string $key) + * @method static \Redis|bool vismember(string $key, mixed $member) + * @method static \Redis|array|false vemb(string $key, mixed $member, bool $raw = false) + * @method static \Redis|array|string|false vrandmember(string $key, int $count = 0) + * @method static \Redis|array|false vrange(string $key, string $min, string $max, int $count = -1) + * @method static \Redis|int|false vrem(string $key, mixed $member) + * @method static \Redis|int|false vsetattr(string $key, mixed $member, array|string $attributes) + * @method static \Redis|array|string|false vgetattr(string $key, mixed $member, bool $decode = true) + * @method static \Redis|array|false vlinks(string $key, mixed $member, bool $withscores = false) + * @method static \Redis|int|false xtrim(string $key, string $threshold, bool $approx = false, bool $minid = false, int $limit = -1) + * @method static \Redis|int|float|false zAdd(string $key, array|float $score_or_options, mixed ...$more_scores_and_mems) + * @method static \Redis|int|false zCard(string $key) + * @method static \Redis|int|false zCount(string $key, string|int $start, string|int $end) + * @method static \Redis|float|false zIncrBy(string $key, float $value, mixed $member) + * @method static \Redis|int|false zLexCount(string $key, string $min, string $max) + * @method static \Redis|array|false zMscore(string $key, mixed $member, mixed ...$other_members) + * @method static \Redis|array|false zPopMax(string $key, int|null $count = null) + * @method static \Redis|array|false zPopMin(string $key, int|null $count = null) + * @method static \Redis|array|false zRange(string $key, string|int $start, string|int $end, array|bool|null $options = null) + * @method static \Redis|array|false zRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1) + * @method static \Redis|array|false zRangeByScore(string $key, string $start, string $end, array $options = []) + * @method static \Redis|int|false zrangestore(string $dstkey, string $srckey, string $start, string $end, array|bool|null $options = null) + * @method static \Redis|array|string zRandMember(string $key, array|null $options = null) + * @method static \Redis|int|false zRank(string $key, mixed $member) + * @method static \Redis|int|false zRem(mixed $key, mixed $member, mixed ...$other_members) + * @method static \Redis|int|false zRemRangeByLex(string $key, string $min, string $max) + * @method static \Redis|int|false zRemRangeByRank(string $key, int $start, int $end) + * @method static \Redis|int|false zRemRangeByScore(string $key, string $start, string $end) + * @method static \Redis|array|false zRevRange(string $key, int $start, int $end, mixed $scores = null) + * @method static \Redis|array|false zRevRangeByLex(string $key, string $max, string $min, int $offset = -1, int $count = -1) + * @method static \Redis|array|false zRevRangeByScore(string $key, string $max, string $min, array|bool $options = []) + * @method static \Redis|int|false zRevRank(string $key, mixed $member) + * @method static \Redis|float|false zScore(string $key, mixed $member) + * @method static \Redis|array|false zdiff(array $keys, array|null $options = null) + * @method static \Redis|int|false zdiffstore(string $dst, array $keys) + * @method static \Redis|array|false zinter(array $keys, array|null $weights = null, array|null $options = null) + * @method static \Redis|int|false zintercard(array $keys, int $limit = -1) + * @method static \Redis|int|false zinterstore(string $dst, array $keys, array|null $weights = null, string|null $aggregate = null) + * @method static \Redis|array|false zscan(string $key, string|int|null $iterator, string|null $pattern = null, int $count = 0) + * @method static \Redis|array|false zunion(array $keys, array|null $weights = null, array|null $options = null) + * @method static \Redis|int|false zunionstore(string $dst, array $keys, array|null $weights = null, string|null $aggregate = null) * * @see \Illuminate\Redis\RedisManager */ From a3d7986f4dd6a3c03a7413051a527738f381fc5c Mon Sep 17 00:00:00 2001 From: Timmy Lindholm <74464421+timmylindh@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:13:38 +0200 Subject: [PATCH 102/596] fix: redirectgueststo accept null param (#59526) --- src/Illuminate/Foundation/Configuration/Middleware.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index 9629eb4a57ae..a5d367aa79cf 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -533,10 +533,10 @@ public function getMiddlewareGroups() /** * Configure where guests are redirected by the "auth" middleware. * - * @param callable|string $redirect + * @param callable|string|null $redirect * @return $this */ - public function redirectGuestsTo(callable|string $redirect) + public function redirectGuestsTo(callable|string|null $redirect) { return $this->redirectTo(guests: $redirect); } @@ -561,7 +561,7 @@ public function redirectUsersTo(callable|string $redirect) */ public function redirectTo(callable|string|null $guests = null, callable|string|null $users = null) { - $guests = is_string($guests) ? fn () => $guests : $guests; + $guests = is_string($guests) || is_null($guests) ? fn () => $guests : $guests; $users = is_string($users) ? fn () => $users : $users; if ($guests) { From 30a3ce140c19d550e0013177869524ee3eea9719 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 5 Apr 2026 09:16:59 -0500 Subject: [PATCH 103/596] =?UTF-8?q?Revert=20"[13.x]=20Remove=20unnecessary?= =?UTF-8?q?=20clone=20in=20SessionManager=20to=20prevent=20duplicat?= =?UTF-8?q?=E2=80=A6"=20(#59542)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f920a23d35719af5b93810d65e4a1b1de630f858. --- src/Illuminate/Session/SessionManager.php | 14 +-- tests/Session/SessionManagerTest.php | 116 ---------------------- 2 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 tests/Session/SessionManagerTest.php diff --git a/src/Illuminate/Session/SessionManager.php b/src/Illuminate/Session/SessionManager.php index 2616ba112b85..0b176ef6d01a 100755 --- a/src/Illuminate/Session/SessionManager.php +++ b/src/Illuminate/Session/SessionManager.php @@ -137,15 +137,9 @@ protected function createRedisDriver() { $handler = $this->createCacheHandler('redis'); - $connection = $this->config->get('session.connection'); - - if ($connection) { - $handler->getCache()->setStore( - tap(clone $handler->getCache()->getStore(), function ($store) use ($connection) { - $store->setConnection($connection); - }) - ); - } + $handler->getCache()->getStore()->setConnection( + $this->config->get('session.connection') + ); return $this->buildSession($handler); } @@ -182,7 +176,7 @@ protected function createCacheHandler($driver) $store = $this->config->get('session.store') ?: $driver; return new CacheBasedSessionHandler( - $this->container->make('cache')->store($store), + clone $this->container->make('cache')->store($store), $this->config->get('session.lifetime') ); } diff --git a/tests/Session/SessionManagerTest.php b/tests/Session/SessionManagerTest.php deleted file mode 100644 index 13a35a9cc8b2..000000000000 --- a/tests/Session/SessionManagerTest.php +++ /dev/null @@ -1,116 +0,0 @@ -createApplication('memcached'); - - $manager = new SessionManager($app); - $session = $manager->driver('memcached'); - - $handler = $session->getHandler(); - - $this->assertInstanceOf(CacheBasedSessionHandler::class, $handler); - - // The handler should use the same Repository instance, not a clone - $this->assertSame( - $app->make('cache')->store('memcached'), - $handler->getCache() - ); - } - - public function testRedisSessionWithoutConnectionSharesCacheRepository() - { - $app = $this->createApplication('redis'); - - $manager = new SessionManager($app); - $session = $manager->driver('redis'); - - $handler = $session->getHandler(); - - $this->assertInstanceOf(CacheBasedSessionHandler::class, $handler); - - // Without session.connection, the handler should share the cache Repository - $this->assertSame( - $app->make('cache')->store('redis'), - $handler->getCache() - ); - } - - public function testRedisSessionWithConnectionDoesNotMutateSharedStore() - { - $app = $this->createApplication('redis', 'session'); - - $sharedStore = $app->make('cache')->store('redis')->getStore(); - $originalConnection = (new \ReflectionProperty($sharedStore, 'connection'))->getValue($sharedStore); - - $manager = new SessionManager($app); - $session = $manager->driver('redis'); - - $handler = $session->getHandler(); - - // The shared cache store's connection should not be mutated - $currentConnection = (new \ReflectionProperty($sharedStore, 'connection'))->getValue($sharedStore); - $this->assertSame($originalConnection, $currentConnection); - - // The session handler's store should have the session connection - $sessionStore = $handler->getCache()->getStore(); - $sessionConnection = (new \ReflectionProperty($sessionStore, 'connection'))->getValue($sessionStore); - $this->assertSame('session', $sessionConnection); - } - - protected function createApplication(string $driver, ?string $sessionConnection = null): Container - { - $app = new Container; - Container::setInstance($app); - - $config = new Repository([ - 'session' => [ - 'driver' => $driver, - 'lifetime' => 120, - 'connection' => $sessionConnection, - 'store' => null, - ], - 'cache' => [ - 'default' => $driver, - 'stores' => [ - 'memcached' => ['driver' => 'array'], - 'redis' => ['driver' => 'redis', 'connection' => 'default'], - ], - 'prefix' => 'test', - ], - ]); - - $app->instance('config', $config); - $app->singleton('cache', function ($app) { - return new \Illuminate\Cache\CacheManager($app); - }); - - $app->singleton('redis', function () { - $redis = m::mock(\Illuminate\Contracts\Redis\Factory::class); - $redis->shouldReceive('connection')->andReturn( - m::mock(\Illuminate\Redis\Connections\Connection::class) - ); - - return $redis; - }); - - return $app; - } -} From e15f891b1c21a9942c9336dc08b003a0ef25e2d8 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:51:55 +0600 Subject: [PATCH 104/596] [13.x] Fix deprecation warning in Contains and DoesntContain rules when values contain null (#59561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same issue as In and NotIn (#59560) — str_replace() receives null from enum_value(null), generating a deprecation warning that will become a TypeError in PHP 9.0. --- src/Illuminate/Validation/Rules/Contains.php | 2 +- src/Illuminate/Validation/Rules/DoesntContain.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Contains.php b/src/Illuminate/Validation/Rules/Contains.php index ca9877316518..3ff34ac58295 100644 --- a/src/Illuminate/Validation/Rules/Contains.php +++ b/src/Illuminate/Validation/Rules/Contains.php @@ -40,7 +40,7 @@ public function __toString() $values = array_map(function ($value) { $value = enum_value($value); - return '"'.str_replace('"', '""', $value).'"'; + return '"'.str_replace('"', '""', (string) $value).'"'; }, $this->values); return 'contains:'.implode(',', $values); diff --git a/src/Illuminate/Validation/Rules/DoesntContain.php b/src/Illuminate/Validation/Rules/DoesntContain.php index 4fafa10573a0..1cddd884405c 100644 --- a/src/Illuminate/Validation/Rules/DoesntContain.php +++ b/src/Illuminate/Validation/Rules/DoesntContain.php @@ -40,7 +40,7 @@ public function __toString() $values = array_map(function ($value) { $value = enum_value($value); - return '"'.str_replace('"', '""', $value).'"'; + return '"'.str_replace('"', '""', (string) $value).'"'; }, $this->values); return 'doesnt_contain:'.implode(',', $values); From a4a140bca23465d080ed6202b4ad190a70c058ec Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:53:35 +0600 Subject: [PATCH 105/596] [13.x] Fix Str::markdown() and Str::inlineMarkdown() crash on null input (#59554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [13.x] Fix Str::markdown() and Str::inlineMarkdown() crash on null input Both methods pass the input directly to the CommonMark converter which requires a string. Passing null throws a TypeError. Cast to string to handle null gracefully. * Also fix Str::transliterate() crash on null input Same issue — passes null directly to third-party ASCII library which requires a string. --- src/Illuminate/Support/Str.php | 6 +++++- tests/Support/SupportStrTest.php | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 8fd2869f292b..cc3874e9c976 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -140,7 +140,7 @@ public static function ascii($value, $language = 'en') */ public static function transliterate($string, $unknown = '?', $strict = false) { - return ASCII::to_transliterate($string, $unknown, $strict); + return ASCII::to_transliterate((string) $string, $unknown, $strict); } /** @@ -791,6 +791,8 @@ public static function words($value, $words = 100, $end = '...') */ public static function markdown($string, array $options = [], array $extensions = []) { + $string = (string) $string; + $converter = new GithubFlavoredMarkdownConverter($options); $environment = $converter->getEnvironment(); @@ -812,6 +814,8 @@ public static function markdown($string, array $options = [], array $extensions */ public static function inlineMarkdown($string, array $options = [], array $extensions = []) { + $string = (string) $string; + $environment = new Environment($options); $environment->addExtension(new GithubFlavoredMarkdownExtension()); diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index bdb2c9eb4692..6bfec03405af 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -1574,12 +1574,14 @@ public function testMarkdown() { $this->assertSame("

hello world

\n", Str::markdown('*hello world*')); $this->assertSame("

hello world

\n", Str::markdown('# hello world')); + $this->assertSame('', Str::markdown(null)); } public function testInlineMarkdown() { $this->assertSame("hello world\n", Str::inlineMarkdown('*hello world*')); $this->assertSame("Laravel\n", Str::inlineMarkdown('[**Laravel**](https://laravel.com)')); + $this->assertSame('', Str::inlineMarkdown(null)); } public function testRepeat() @@ -1620,6 +1622,7 @@ public function testTransliterateOverrideUnknown(): void { $this->assertSame('HHH', Str::transliterate('🎂🚧🏆', 'H')); $this->assertSame('Hello', Str::transliterate('🎂', 'Hello')); + $this->assertSame('', Str::transliterate(null)); } #[DataProvider('specialCharacterProvider')] From 5ae0f29676f52d7a39065fba2beed0d94d3ca7c1 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 6 Apr 2026 14:06:44 +0100 Subject: [PATCH 106/596] [13.x] Add queue methods to inspect jobs (#59511) * add queue methods to inspect jobs queue methods to inspect jobs squash it to save viewing pain doc Update InspectedJob.php csx2 cs wip tests and clean up * drop queue we pass it in.. so no need to see it * adjust order of params --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Queue/BeanstalkdQueue.php | 34 +++++++++ src/Illuminate/Queue/DatabaseQueue.php | 48 +++++++++++++ src/Illuminate/Queue/FailoverQueue.php | 34 +++++++++ src/Illuminate/Queue/Jobs/InspectedJob.php | 43 +++++++++++ src/Illuminate/Queue/NullQueue.php | 34 +++++++++ src/Illuminate/Queue/RedisQueue.php | 44 ++++++++++++ src/Illuminate/Queue/SqsQueue.php | 34 +++++++++ src/Illuminate/Queue/SyncQueue.php | 34 +++++++++ .../Support/Testing/Fakes/QueueFake.php | 44 ++++++++++++ tests/Integration/Queue/RedisQueueTest.php | 60 ++++++++++++++++ tests/Queue/QueueDatabaseQueueUnitTest.php | 72 +++++++++++++++++++ tests/Support/SupportTestingQueueFakeTest.php | 14 ++++ 12 files changed, 495 insertions(+) create mode 100644 src/Illuminate/Queue/Jobs/InspectedJob.php diff --git a/src/Illuminate/Queue/BeanstalkdQueue.php b/src/Illuminate/Queue/BeanstalkdQueue.php index 9c0c3e0e7988..be6562858ff9 100755 --- a/src/Illuminate/Queue/BeanstalkdQueue.php +++ b/src/Illuminate/Queue/BeanstalkdQueue.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Jobs\BeanstalkdJob; +use Illuminate\Support\Collection; use Pheanstalk\Contract\JobIdInterface; use Pheanstalk\Pheanstalk; use Pheanstalk\Values\Job; @@ -111,6 +112,39 @@ public function reservedSize($queue = null) return $this->pheanstalk->statsTube(new TubeName($this->getQueue($queue)))->currentJobsReserved; } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index 162072158232..efd2a5fa3427 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -7,6 +7,7 @@ use Illuminate\Database\Connection; use Illuminate\Queue\Jobs\DatabaseJob; use Illuminate\Queue\Jobs\DatabaseJobRecord; +use Illuminate\Queue\Jobs\InspectedJob; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -131,6 +132,53 @@ public function reservedSize($queue = null) ->count(); } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->whereNull('reserved_at') + ->where('available_at', '<=', $this->currentTime()) + ->get() + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->whereNull('reserved_at') + ->where('available_at', '>', $this->currentTime()) + ->get() + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->whereNotNull('reserved_at') + ->get() + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/FailoverQueue.php b/src/Illuminate/Queue/FailoverQueue.php index 73c024b60879..c8fe73cd232c 100644 --- a/src/Illuminate/Queue/FailoverQueue.php +++ b/src/Illuminate/Queue/FailoverQueue.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Events\QueueFailedOver; +use Illuminate\Support\Collection; use RuntimeException; use Throwable; @@ -71,6 +72,39 @@ public function reservedSize($queue = null) return $this->manager->connection($this->connections[0])->reservedSize($queue); } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + return $this->manager->connection($this->connections[0])->pendingJobs($queue); + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + return $this->manager->connection($this->connections[0])->delayedJobs($queue); + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + return $this->manager->connection($this->connections[0])->reservedJobs($queue); + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/Jobs/InspectedJob.php b/src/Illuminate/Queue/Jobs/InspectedJob.php new file mode 100644 index 000000000000..f4e98d62d8d0 --- /dev/null +++ b/src/Illuminate/Queue/Jobs/InspectedJob.php @@ -0,0 +1,43 @@ +getConnection()->zcard($this->getQueue($queue).':reserved'); } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + $queue = $this->getQueue($queue); + + return (new Collection($this->getConnection()->lrange($queue, 0, -1))) + ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + $queue = $this->getQueue($queue); + + return (new Collection($this->getConnection()->zrange($queue.':delayed', 0, -1))) + ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + $queue = $this->getQueue($queue); + + return (new Collection($this->getConnection()->zrange($queue.':reserved', 0, -1))) + ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index 3543d2dd4302..3601cf8d3702 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Queue\ClearableQueue; use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Jobs\SqsJob; +use Illuminate\Support\Collection; use Illuminate\Support\Str; class SqsQueue extends Queue implements QueueContract, ClearableQueue @@ -133,6 +134,39 @@ public function reservedSize($queue = null) return (int) $response['Attributes']['ApproximateNumberOfMessagesNotVisible'] ?? 0; } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/SyncQueue.php b/src/Illuminate/Queue/SyncQueue.php index aa3a90c290a4..900324d84c3c 100755 --- a/src/Illuminate/Queue/SyncQueue.php +++ b/src/Illuminate/Queue/SyncQueue.php @@ -12,6 +12,7 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\Jobs\SyncJob; +use Illuminate\Support\Collection; use Throwable; class SyncQueue extends Queue implements QueueContract @@ -70,6 +71,39 @@ public function reservedSize($queue = null) return 0; } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index 16d99166857d..41e39a41b377 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Events\CallQueuedListener; use Illuminate\Queue\CallQueuedClosure; +use Illuminate\Queue\Jobs\InspectedJob; use Illuminate\Queue\QueueManager; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -456,6 +457,49 @@ public function reservedSize($queue = null) return 0; } + /** + * Get the pending jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function pendingJobs($queue = null): Collection + { + return (new Collection($this->jobs)) + ->flatten(1) + ->filter(fn ($job) => $job['queue'] === $queue) + ->map(fn ($data) => new InspectedJob( + name: is_object($data['job']) + ? (method_exists($data['job'], 'displayName') ? $data['job']->displayName() : get_class($data['job'])) + : $data['job'], + attempts: 0, + uuid: null, + createdAt: null, + )); + } + + /** + * Get the delayed jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function delayedJobs($queue = null): Collection + { + return new Collection; + } + + /** + * Get the reserved jobs for the given queue. + * + * @param string|null $queue + * @return \Illuminate\Support\Collection + */ + public function reservedJobs($queue = null): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/tests/Integration/Queue/RedisQueueTest.php b/tests/Integration/Queue/RedisQueueTest.php index 7f1286109080..ae030b0324ac 100644 --- a/tests/Integration/Queue/RedisQueueTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -7,8 +7,10 @@ use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\JobQueueing; +use Illuminate\Queue\Jobs\InspectedJob; use Illuminate\Queue\Jobs\RedisJob; use Illuminate\Queue\RedisQueue; +use Illuminate\Support\Carbon; use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; use Mockery as m; @@ -593,6 +595,64 @@ public function testDelayedJobsWorkWithPhpRedisSerializationEnabled() $client->setOption($optSerializer, $originalSerializer); } } + + #[DataProvider('redisDriverProvider')] + public function testPendingJobs($driver) + { + $default = config('queue.connections.redis.queue', 'default'); + $this->setQueue($driver, $default); + + $job = new RedisQueueIntegrationTestJob(99); + $this->queue->push($job); + + $pending = $this->queue->pendingJobs(); + + $this->assertCount(1, $pending); + $this->assertInstanceOf(InspectedJob::class, $pending->first()); + $this->assertSame(RedisQueueIntegrationTestJob::class, $pending->first()->name); + $this->assertSame(0, $pending->first()->attempts); + $this->assertNotNull($pending->first()->uuid); + $this->assertInstanceOf(Carbon::class, $pending->first()->createdAt); + } + + #[DataProvider('redisDriverProvider')] + public function testDelayedJobs($driver) + { + $default = config('queue.connections.redis.queue', 'default'); + $this->setQueue($driver, $default); + + $job = new RedisQueueIntegrationTestJob(99); + $this->queue->later(60, $job); + + $delayed = $this->queue->delayedJobs(); + + $this->assertCount(1, $delayed); + $this->assertInstanceOf(InspectedJob::class, $delayed->first()); + $this->assertSame(RedisQueueIntegrationTestJob::class, $delayed->first()->name); + $this->assertSame(0, $delayed->first()->attempts); + $this->assertNotNull($delayed->first()->uuid); + $this->assertInstanceOf(Carbon::class, $delayed->first()->createdAt); + } + + #[DataProvider('redisDriverProvider')] + public function testReservedJobs($driver) + { + $default = config('queue.connections.redis.queue', 'default'); + $this->setQueue($driver, $default); + + $job = new RedisQueueIntegrationTestJob(99); + $this->queue->push($job); + $this->queue->pop(); // moves job to reserved sorted set + + $reserved = $this->queue->reservedJobs(); + + $this->assertCount(1, $reserved); + $this->assertInstanceOf(InspectedJob::class, $reserved->first()); + $this->assertSame(RedisQueueIntegrationTestJob::class, $reserved->first()->name); + $this->assertSame(1, $reserved->first()->attempts); + $this->assertNotNull($reserved->first()->uuid); + $this->assertInstanceOf(Carbon::class, $reserved->first()->createdAt); + } } class RedisQueueIntegrationTestJob diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 839e6db12344..9b0a791fd46f 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -6,6 +6,7 @@ use Illuminate\Container\Container; use Illuminate\Database\Connection; use Illuminate\Queue\DatabaseQueue; +use Illuminate\Queue\Jobs\InspectedJob; use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; use Illuminate\Support\Str; @@ -197,6 +198,77 @@ public function testBuildDatabaseRecordWithPayloadAtTheEnd() $this->assertArrayHasKey('payload', array_slice($record, -1, 1, true)); } + public function testPendingJobs() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer(m::spy(Container::class)); + + $payload = json_encode(['uuid' => 'test-uuid', 'displayName' => 'MyTestJob', 'job' => 'foo', 'data' => [], 'createdAt' => 1000000]); + + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->with('queue', 'default')->andReturnSelf(); + $query->shouldReceive('whereNull')->with('reserved_at')->andReturnSelf(); + $query->shouldReceive('where')->with('available_at', '<=', m::any())->andReturnSelf(); + $query->shouldReceive('get')->andReturn(collect([(object) ['id' => 1, 'queue' => 'default', 'payload' => $payload, 'attempts' => 0, 'reserved_at' => null]])); + + $jobs = $queue->pendingJobs(); + + $this->assertCount(1, $jobs); + $this->assertInstanceOf(InspectedJob::class, $jobs->first()); + $this->assertSame('MyTestJob', $jobs->first()->name); + $this->assertSame('test-uuid', $jobs->first()->uuid); + $this->assertSame(0, $jobs->first()->attempts); + $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); + $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); + } + + public function testDelayedJobs() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer(m::spy(Container::class)); + + $payload = json_encode(['uuid' => 'test-uuid', 'displayName' => 'MyDelayedJob', 'job' => 'foo', 'data' => [], 'createdAt' => 1000000]); + + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->with('queue', 'default')->andReturnSelf(); + $query->shouldReceive('whereNull')->with('reserved_at')->andReturnSelf(); + $query->shouldReceive('where')->with('available_at', '>', m::any())->andReturnSelf(); + $query->shouldReceive('get')->andReturn(collect([(object) ['id' => 2, 'queue' => 'default', 'payload' => $payload, 'attempts' => 0, 'reserved_at' => null]])); + + $jobs = $queue->delayedJobs(); + + $this->assertCount(1, $jobs); + $this->assertInstanceOf(InspectedJob::class, $jobs->first()); + $this->assertSame('MyDelayedJob', $jobs->first()->name); + $this->assertSame('test-uuid', $jobs->first()->uuid); + $this->assertSame(0, $jobs->first()->attempts); + $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); + $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); + } + + public function testReservedJobs() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer(m::spy(Container::class)); + + $payload = json_encode(['uuid' => 'test-uuid', 'displayName' => 'MyTestJob', 'job' => 'foo', 'data' => [], 'createdAt' => 1000000]); + + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('where')->with('queue', 'default')->andReturnSelf(); + $query->shouldReceive('whereNotNull')->with('reserved_at')->andReturnSelf(); + $query->shouldReceive('get')->andReturn(collect([(object) ['id' => 1, 'queue' => 'default', 'payload' => $payload, 'attempts' => 1, 'reserved_at' => now()->timestamp]])); + + $jobs = $queue->reservedJobs(); + + $this->assertCount(1, $jobs); + $this->assertInstanceOf(InspectedJob::class, $jobs->first()); + $this->assertSame('MyTestJob', $jobs->first()->name); + $this->assertSame('test-uuid', $jobs->first()->uuid); + $this->assertSame(1, $jobs->first()->attempts); + $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); + $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); + } + public function testGetLockForPoppingIsCached() { $database = m::mock(Connection::class); diff --git a/tests/Support/SupportTestingQueueFakeTest.php b/tests/Support/SupportTestingQueueFakeTest.php index 77607e73a790..82d48c38ae97 100644 --- a/tests/Support/SupportTestingQueueFakeTest.php +++ b/tests/Support/SupportTestingQueueFakeTest.php @@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Foundation\Application; use Illuminate\Queue\CallQueuedClosure; +use Illuminate\Queue\Jobs\InspectedJob; use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; use Mockery as m; @@ -481,6 +482,19 @@ public function testAssertChainErrorHandling() } } + public function testPendingJobs() + { + $this->fake->push($this->job, '', 'foo'); + $this->fake->push(new JobToFakeStub, '', 'bar'); + + $pending = $this->fake->pendingJobs('foo'); + + $this->assertCount(1, $pending); + $this->assertInstanceOf(InspectedJob::class, $pending->first()); + $this->assertSame(JobStub::class, $pending->first()->name); + $this->assertSame(0, $pending->first()->attempts); + } + public function testGetRawPushes() { $this->fake->pushRaw('some-payload', null, ['options' => 'yeah']); From 0124a5438c879819931e993937a1c67b8899c5a0 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:07:16 +0000 Subject: [PATCH 107/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index e590560be189..6adba39e69e6 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -67,6 +67,9 @@ * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) * @method static bool hasPushed(string $job) + * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) + * @method static \Illuminate\Support\Collection delayedJobs(string|null $queue = null) + * @method static \Illuminate\Support\Collection reservedJobs(string|null $queue = null) * @method static bool shouldFakeJob(object $job) * @method static array pushedJobs() * @method static array rawPushes() From 9f92a159e9b1c7bf9b7adb6bb08a5dfead2d0be7 Mon Sep 17 00:00:00 2001 From: NurullahDemirel <64475698+NurullahDemirel@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:37:34 +0300 Subject: [PATCH 108/596] Feature/form request strict mode (#59430) * strict mode for validation * delete validation.php file * formated * formatting * formatting * more tests --------- Co-authored-by: Taylor Otwell --- .../Http/Attributes/FailOnUnknownFields.php | 13 + .../Foundation/Http/FormRequest.php | 87 +++++ .../Foundation/FoundationFormRequestTest.php | 346 ++++++++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php diff --git a/src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php b/src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php new file mode 100644 index 000000000000..2f85d0660b05 --- /dev/null +++ b/src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php @@ -0,0 +1,13 @@ +shouldFailOnUnknownFields()) { + $validator->after(function (Validator $validator) { + $this->validateNoUnknownFields($validator); + }); + } + $this->setValidator($validator); return $this->validator; @@ -144,6 +159,7 @@ protected function configureFromAttributes() if (count($errorBag) > 0) { $this->errorBag = $errorBag[0]->newInstance()->name; } + } /** @@ -192,6 +208,66 @@ protected function validationRules() return method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; } + /** + * Determine if fields not present in rules should fail validation. + * + * @return bool + */ + protected function shouldFailOnUnknownFields(): bool + { + $failOnUnknownFields = (new ReflectionClass($this))->getAttributes(FailOnUnknownFields::class); + + return $failOnUnknownFields !== [] + ? $failOnUnknownFields[0]->newInstance()->value + : static::$globalFailOnUnknownFields; + } + + /** + * Validate that no unknown fields were sent as input. + * + * @param \Illuminate\Contracts\Validation\Validator $validator + * @return void + */ + protected function validateNoUnknownFields(Validator $validator): void + { + $allowedKeys = array_keys($this->validationRules()); + + foreach (array_keys(Arr::dot($this->all())) as $inputKey) { + if (! $this->isKnownField($inputKey, $allowedKeys)) { + $validator->errors()->add($inputKey, trans('validation.prohibited', [ + 'attribute' => str_replace('_', ' ', $inputKey), + ])); + } + } + } + + /** + * Determine if the given input key is an allowed key based on the validation rules. + * + * @param string $inputKey + * @param array $allowedKeys + * @return bool + */ + protected function isKnownField(string $inputKey, array $allowedKeys): bool + { + foreach ($allowedKeys as $ruleKey) { + if ($ruleKey === $inputKey) { + return true; + } + + if (str_contains($ruleKey, '*')) { + $pattern = '/^'.str_replace('\*', '[^.]+', preg_quote($ruleKey, '/')).'$/'; + + if (preg_match($pattern, $inputKey)) { + return true; + } + } + + } + + return false; + } + /** * Handle a failed validation attempt. * @@ -304,6 +380,17 @@ public function attributes() return []; } + /** + * Enable or disable unknown-field rejection globally for all form requests. + * + * @param bool $value + * @return void + */ + public static function failOnUnknownFields(bool $value = true): void + { + static::$globalFailOnUnknownFields = $value; + } + /** * Set the Validator instance. * diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index 21985e2fb7a1..43d4251ce866 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -10,10 +10,13 @@ use Illuminate\Contracts\Validation\Factory as ValidationFactoryContract; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\Attributes\ErrorBag; +use Illuminate\Foundation\Http\Attributes\FailOnUnknownFields; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; use Illuminate\Routing\UrlGenerator; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator as TranslatorConcrete; use Illuminate\Validation\Factory as ValidationFactory; use Illuminate\Validation\ValidationException; use Mockery as m; @@ -25,6 +28,10 @@ class FoundationFormRequestTest extends TestCase protected function tearDown(): void { + FormRequest::failOnUnknownFields(false); + + Container::setInstance(null); + $this->mocks = []; parent::tearDown(); @@ -241,6 +248,213 @@ public function testRequestWithGetRules() $request->validateResolved(); } + public function testFailOnUnknownFieldsRejectsExtraInputWhenEnabledOnRequest() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'unexpected' => 'value'], + FoundationTestFormRequestFailOnUnknownFieldsStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsAllowsExtraInputWhenExplicitlyDisabledOnRequest() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'with' => 'extras'], + FoundationTestFormRequestSkipUnknownFieldsFailureStub::class + ); + + $request->validateResolved(); + + $this->assertEquals(['name' => 'Taylor'], $request->validated()); + } + + public function testFailOnUnknownFieldsEnabledViaFailOnUnknownFieldsStaticMethod() + { + FormRequest::failOnUnknownFields(); + + $request = $this->createRequest( + ['name' => 'Taylor', 'unexpected' => 'value'], + FoundationTestFormRequestStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsWorksWhenRequestDoesNotDefineRulesMethod() + { + FormRequest::failOnUnknownFields(); + + $request = $this->createRequest( + ['unexpected' => 'value'], + FoundationTestFormRequestWithoutRulesMethod::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsAttributeOverridesGlobalStatic() + { + FormRequest::failOnUnknownFields(); + + $request = $this->createRequest( + ['name' => 'Taylor', 'with' => 'extras'], + FoundationTestFormRequestSkipUnknownFieldsFailureStub::class + ); + + $request->validateResolved(); + + $this->assertEquals(['name' => 'Taylor'], $request->validated()); + } + + public function testFailOnUnknownFieldsAllowsKeysMatchingWildcardRules() + { + $request = $this->createRequest( + [ + 'items' => [ + ['id' => 1, 'name' => 'a'], + ['id' => 2, 'name' => 'b'], + ], + ], + FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('items.0.name')); + } + + public function testFailOnUnknownFieldsPassesForInputMatchingWildcardRulesOnly() + { + $request = $this->createRequest( + [ + 'items' => [ + ['id' => 1], + ['id' => 2], + ], + ], + FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class + ); + + $request->validateResolved(); + + $this->assertSame( + [ + 'items' => [ + ['id' => 1], + ['id' => 2], + ], + ], + $request->validated() + ); + } + + public function testFailOnUnknownFieldsWildcardMatchesSingleSegmentOnly() + { + $request = $this->createRequest( + [ + 'items' => [ + ['name' => 'a'], + ], + ], + FoundationTestFormRequestFailOnUnknownFieldsSingleSegmentWildcardStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('items.0.name')); + } + + public function testFailOnUnknownFieldsRejectsMultipleUnknownKeys() + { + $request = $this->createRequest( + [ + 'name' => 'Taylor', + 'role' => 'admin', + 'profile' => ['is_admin' => true], + ], + FoundationTestFormRequestFailOnUnknownFieldsStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('role')); + $this->assertTrue($exception->validator->errors()->has('profile.is_admin')); + } + + public function testFailOnUnknownFieldsRejectsUnknownNestedSibling() + { + $request = $this->createRequest( + ['user' => ['name' => 'Taylor', 'role' => 'admin']], + FoundationTestFormRequestFailOnUnknownFieldsNestedStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('user.role')); + } + + public function testFailOnUnknownFieldsUsesPreparedInput() + { + $request = $this->createRequest( + ['full_name' => 'Taylor'], + FoundationTestFormRequestFailOnUnknownFieldsPrepareForValidationStub::class + ); + + $request->validateResolved(); + + $this->assertSame(['name' => 'Taylor'], $request->validated()); + } + + public function testFailOnUnknownFieldsChecksRequestPayloadWhenValidationDataIsOverridden() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'unexpected' => 'value'], + FoundationTestFormRequestFailOnUnknownFieldsValidationDataOverrideStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsStillRunsWithStopOnFirstFailureAttribute() + { + $request = $this->createRequest( + ['unexpected' => 'value'], + FoundationTestFormRequestFailOnUnknownFieldsStopOnFirstFailureStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + /** * Catch the given exception thrown from the executor, and return it. * @@ -279,8 +493,16 @@ protected function createRequest($payload = [], $class = FoundationTestFormReque ValidationFactoryContract::class, $this->createValidationFactory($container) ); + + $container->instance('translator', new TranslatorConcrete(new ArrayLoader([ + 'validation' => [ + 'prohibited' => 'The :attribute field is prohibited.', + ], + ]), 'en')); }); + Container::setInstance($container); + $request = $class::create('/', 'GET', $payload); return $request->setRedirector($this->createMockRedirector($request)) @@ -296,6 +518,7 @@ protected function createRequest($payload = [], $class = FoundationTestFormReque protected function createValidationFactory($container) { $translator = m::mock(Translator::class)->shouldReceive('get') + ->zeroOrMoreTimes()->andReturn('error')->shouldReceive('choice') ->zeroOrMoreTimes()->andReturn('error')->getMock(); return new ValidationFactory($translator, $container); @@ -542,3 +765,126 @@ protected function validationRules(): array } } } + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields(false)] +class FoundationTestFormRequestSkipUnknownFieldsFailureStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub extends FormRequest +{ + public function rules() + { + return ['items.*.id' => 'required']; + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsSingleSegmentWildcardStub extends FormRequest +{ + public function rules() + { + return ['items.*' => 'array']; + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsNestedStub extends FormRequest +{ + public function rules() + { + return ['user.name' => 'required']; + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsPrepareForValidationStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function prepareForValidation() + { + $this->replace(['name' => $this->input('full_name')]); + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsValidationDataOverrideStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function validationData() + { + return ['name' => $this->input('name')]; + } + + public function authorize() + { + return true; + } +} + +#[StopOnFirstFailure] +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsStopOnFirstFailureStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function authorize() + { + return true; + } +} From 127197fe3ba2593c0aaff7b78517f49b5adb53b3 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 6 Apr 2026 13:38:01 +0000 Subject: [PATCH 109/596] Apply fixes from StyleCI --- src/Illuminate/Foundation/Http/FormRequest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index b7a6e7ff7a64..8ef12c152821 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -159,7 +159,6 @@ protected function configureFromAttributes() if (count($errorBag) > 0) { $this->errorBag = $errorBag[0]->newInstance()->name; } - } /** @@ -262,7 +261,6 @@ protected function isKnownField(string $inputKey, array $allowedKeys): bool return true; } } - } return false; From 8563ff12bc9e3ffe50ebe9aad49e2f29f13f5f16 Mon Sep 17 00:00:00 2001 From: Timmy Lindholm Date: Mon, 6 Apr 2026 19:26:28 +0200 Subject: [PATCH 110/596] fix: fix --- src/Illuminate/Queue/RedisQueue.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index e5103a3ad13b..ca468ae11b2c 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -160,7 +160,7 @@ public function reservedSize($queue = null) */ public function pendingJobs($queue = null): Collection { - $queue = $this->getQueue($queue); + $queue = $this->getRedisKey($queue); return (new Collection($this->getConnection()->lrange($queue, 0, -1))) ->map(fn ($payload) => InspectedJob::fromPayload($payload)); @@ -174,7 +174,7 @@ public function pendingJobs($queue = null): Collection */ public function delayedJobs($queue = null): Collection { - $queue = $this->getQueue($queue); + $queue = $this->getRedisKey($queue); return (new Collection($this->getConnection()->zrange($queue.':delayed', 0, -1))) ->map(fn ($payload) => InspectedJob::fromPayload($payload)); @@ -188,7 +188,7 @@ public function delayedJobs($queue = null): Collection */ public function reservedJobs($queue = null): Collection { - $queue = $this->getQueue($queue); + $queue = $this->getRedisKey($queue); return (new Collection($this->getConnection()->zrange($queue.':reserved', 0, -1))) ->map(fn ($payload) => InspectedJob::fromPayload($payload)); From 9ac55a50bcaa50ebbd4e428cf52ae1725609db8f Mon Sep 17 00:00:00 2001 From: Timmy Lindholm Date: Mon, 6 Apr 2026 21:43:21 +0200 Subject: [PATCH 111/596] fix: redis workflow test --- .github/workflows/redis.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/redis.yml b/.github/workflows/redis.yml index ed795345a992..b7b9400cf4f3 100644 --- a/.github/workflows/redis.yml +++ b/.github/workflows/redis.yml @@ -4,7 +4,7 @@ on: push: branches: - master - - '*.x' + - "*.x" pull_request: jobs: @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: true matrix: - client: ['phpredis', 'predis'] + client: ["phpredis", "predis"] name: Redis (${{ matrix.client}}) Driver @@ -67,7 +67,7 @@ jobs: strategy: fail-fast: true matrix: - client: ['phpredis', 'predis'] + client: ["phpredis", "predis"] name: Redis Cluster (${{ matrix.client}}) Driver @@ -129,6 +129,5 @@ jobs: env: REDIS_CLIENT: ${{ matrix.client }} REDIS_CLUSTER_HOSTS_AND_PORTS: 127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002 - REDIS_QUEUE: '{default}' + REDIS_QUEUE: "default" QUEUE_CONNECTION: redis - From ccc9e1c6213f6bee79f50d364e8be161410cdece Mon Sep 17 00:00:00 2001 From: Timmy Lindholm Date: Mon, 6 Apr 2026 21:49:35 +0200 Subject: [PATCH 112/596] ci: retrigger workflow From 06c0ee5ac99a3e4188f69c905baf7dd17585a4dd Mon Sep 17 00:00:00 2001 From: Timmy Lindholm Date: Mon, 6 Apr 2026 21:58:36 +0200 Subject: [PATCH 113/596] fix: tests --- tests/Integration/Queue/RedisQueueTest.php | 57 ++++++++++++++-------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/tests/Integration/Queue/RedisQueueTest.php b/tests/Integration/Queue/RedisQueueTest.php index ae030b0324ac..13d8ae955146 100644 --- a/tests/Integration/Queue/RedisQueueTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -62,6 +62,11 @@ private function setQueue($driver, $default = 'default', $connection = null, $re $this->queue->setContainer($this->container); } + private function getRedisKey($queue = null) + { + return (new \ReflectionMethod($this->queue, 'getRedisKey'))->invoke($this->queue, $queue); + } + /** * @param string $driver */ @@ -88,8 +93,9 @@ public function testExpiredJobsArePopped($driver) $this->assertEquals($jobs[3], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertNull($this->queue->pop()); - $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("queues:$default:delayed")); - $this->assertEquals(3, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); + $this->assertEquals(3, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); } /** @@ -162,8 +168,9 @@ public function testPopProperlyPopsJobOffOfRedis($driver) $this->assertEquals($redisJob->getJobId(), json_decode($redisJob->getReservedJob())->id); // Check reserved queue - $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); - $result = $this->redis[$driver]->connection()->zrangebyscore("queues:$default:reserved", -INF, INF, ['withscores' => true]); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); + $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; $score = (int) $result[$reservedJob]; $this->assertLessThanOrEqual($score, $before + 60); @@ -189,8 +196,9 @@ public function testPopProperlyPopsDelayedJobOffOfRedis($driver) $after = $this->currentTime(); // Check reserved queue - $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); - $result = $this->redis[$driver]->connection()->zrangebyscore("queues:$default:reserved", -INF, INF, ['withscores' => true]); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); + $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; $score = (int) $result[$reservedJob]; $this->assertLessThanOrEqual($score, $before + 60); @@ -219,8 +227,9 @@ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) $after = $this->currentTime(); // Check reserved queue - $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); - $result = $this->redis[$driver]->connection()->zrangebyscore("queues:$default:reserved", -INF, INF, ['withscores' => true]); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); + $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; $score = (int) $result[$reservedJob]; $this->assertLessThanOrEqual($score, $before); @@ -273,9 +282,10 @@ public function testBlockingPopProperlyPopsExpiredJobs($driver) $this->assertEquals($jobs[0], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertEquals($jobs[1], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); - $this->assertEquals(0, $this->redis[$driver]->connection()->llen('queues:default:notify')); - $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("queues:$default:delayed")); - $this->assertEquals(2, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(0, $this->redis[$driver]->connection()->llen("$redisKey:notify")); + $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); + $this->assertEquals(2, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); Str::createUuidsNormally(); } @@ -309,8 +319,9 @@ public function testNotExpireJobsWhenExpireNull($driver) $after = $this->currentTime(); // Check reserved queue - $this->assertEquals(2, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); - $result = $this->redis[$driver]->connection()->zrangebyscore("queues:$default:reserved", -INF, INF, ['withscores' => true]); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(2, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); + $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); foreach ($result as $payload => $score) { $command = unserialize(json_decode($payload)->data->command); @@ -349,8 +360,9 @@ public function testExpireJobsWhenExpireSet($driver) $after = $this->currentTime(); // Check reserved queue - $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); - $result = $this->redis[$driver]->connection()->zrangebyscore("queues:$default:reserved", -INF, INF, ['withscores' => true]); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); + $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; $score = (int) $result[$reservedJob]; $this->assertLessThanOrEqual($score, $before + 30); @@ -379,9 +391,10 @@ public function testRelease($driver) $after = $this->currentTime(); // check the content of delayed queue - $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("queues:$default:delayed")); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); - $results = $this->redis[$driver]->connection()->zrangebyscore("queues:$default:delayed", -INF, INF, ['withscores' => true]); + $results = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:delayed", -INF, INF, ['withscores' => true]); $payload = array_keys($results)[0]; @@ -434,9 +447,10 @@ public function testDelete($driver) $redisJob->delete(); - $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("queues:$default:delayed")); - $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("queues:$default:reserved")); - $this->assertEquals(0, $this->redis[$driver]->connection()->llen("queues:$default")); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); + $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); + $this->assertEquals(0, $this->redis[$driver]->connection()->llen("$redisKey")); $this->assertNull($this->queue->pop()); } @@ -458,7 +472,8 @@ public function testClear($driver) $this->assertEquals(2, $this->queue->clear(null)); $this->assertEquals(0, $this->queue->size()); - $this->assertEquals(0, $this->redis[$driver]->connection()->llen('queues:default:notify')); + $redisKey = $this->getRedisKey($default); + $this->assertEquals(0, $this->redis[$driver]->connection()->llen("$redisKey:notify")); } /** From 4d1bf71b5f886fa5a964f1cd6df5f0f3b8150f48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:04:14 -0500 Subject: [PATCH 114/596] Bump vite in /src/Illuminate/Foundation/resources/exceptions/renderer (#59571) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../exceptions/renderer/package-lock.json | 68 +++++++++++++++++-- .../exceptions/renderer/package.json | 2 +- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json index 7905819e57e8..f307f8765944 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.1.12", - "vite": "^7.1.11" + "vite": "^7.3.2" }, "engines": { "node": ">=22.19.0" @@ -1113,6 +1113,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", @@ -1798,7 +1858,6 @@ "version": "4.0.3", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2092,10 +2151,11 @@ } }, "node_modules/vite": { - "version": "7.3.1", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/package.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package.json index 98860cd412b9..1cb280f6141f 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package.json @@ -18,6 +18,6 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.1.12", - "vite": "^7.1.11" + "vite": "^7.3.2" } } From a3a626b7ab87723df08fef4d99b92c0b0e91e14d Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:05:04 +0600 Subject: [PATCH 115/596] [13.x] Fix deprecation warning in In and NotIn rules when values contain null (#59576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fix as #59561 (Contains/DoesntContain) — str_replace() receives null from enum_value(null), generating a deprecation warning. The In and NotIn rules have the identical pattern that was missed. --- src/Illuminate/Validation/Rules/In.php | 2 +- src/Illuminate/Validation/Rules/NotIn.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/In.php b/src/Illuminate/Validation/Rules/In.php index 7e2d83a29673..c001c05b1b1c 100644 --- a/src/Illuminate/Validation/Rules/In.php +++ b/src/Illuminate/Validation/Rules/In.php @@ -49,7 +49,7 @@ public function __toString() $values = array_map(function ($value) { $value = enum_value($value); - return '"'.str_replace('"', '""', $value).'"'; + return '"'.str_replace('"', '""', (string) $value).'"'; }, $this->values); return $this->rule.':'.implode(',', $values); diff --git a/src/Illuminate/Validation/Rules/NotIn.php b/src/Illuminate/Validation/Rules/NotIn.php index 290b84e941fe..441c90d03cba 100644 --- a/src/Illuminate/Validation/Rules/NotIn.php +++ b/src/Illuminate/Validation/Rules/NotIn.php @@ -47,7 +47,7 @@ public function __toString() $values = array_map(function ($value) { $value = enum_value($value); - return '"'.str_replace('"', '""', $value).'"'; + return '"'.str_replace('"', '""', (string) $value).'"'; }, $this->values); return $this->rule.':'.implode(',', $values); From 9f4c5cf80d024792596812e0479c50f585ed6b72 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:30:07 +0600 Subject: [PATCH 116/596] [13.x] Add flushState to FormRequest to reset global strict mode between tests (#59574) * [13.x] Add flushState to FormRequest to reset global strict mode between tests FormRequest::failOnUnknownFields() sets a static flag that persists across tests. Without a flushState() method and a call in the test lifecycle teardown, enabling strict mode in one test leaks into all subsequent tests. * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Http/FormRequest.php | 10 ++++++++++ .../Concerns/InteractsWithTestCaseLifecycle.php | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 8ef12c152821..415ba1db8ed7 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -427,4 +427,14 @@ public function setContainer(Container $container) return $this; } + + /** + * Flush the global state of the form request. + * + * @return void + */ + public static function flushState(): void + { + static::$globalFailOnUnknownFields = false; + } } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index ae592732602a..54e264a27964 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -11,6 +11,7 @@ use Illuminate\Foundation\Bootstrap\HandleExceptions; use Illuminate\Foundation\Bootstrap\RegisterProviders; use Illuminate\Foundation\Console\AboutCommand; +use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; @@ -183,6 +184,7 @@ protected function tearDownTheTestEnvironment(): void Component::forgetFactory(); ConvertEmptyStringsToNull::flushState(); Factory::flushState(); + FormRequest::flushState(); EncodedHtmlString::flushState(); EncryptCookies::flushState(); HandleCors::flushState(); From 1becc6c850b74abd3cf9d1969f73929edfdfaa16 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 7 Apr 2026 09:31:17 -0400 Subject: [PATCH 117/596] [13.x] Fix `#[WithoutRelations]` queue attribute not being inherited by child classes (#59568) * Lookup WithoutRelations attribute recursively * Add test --- src/Illuminate/Queue/SerializesModels.php | 3 ++- .../Queue/ModelSerializationTest.php | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/SerializesModels.php b/src/Illuminate/Queue/SerializesModels.php index 388c1d745561..f1f6348c792f 100644 --- a/src/Illuminate/Queue/SerializesModels.php +++ b/src/Illuminate/Queue/SerializesModels.php @@ -3,6 +3,7 @@ namespace Illuminate\Queue; use Illuminate\Queue\Attributes\WithoutRelations; +use Illuminate\Support\Reflector; use ReflectionClass; use ReflectionProperty; @@ -24,7 +25,7 @@ public function __serialize() [$class, $properties, $classLevelWithoutRelations] = [ get_class($this), $reflectionClass->getProperties(), - ! empty($reflectionClass->getAttributes(WithoutRelations::class)), + ! is_null(Reflector::getClassAttribute($this, WithoutRelations::class, ascend: true)), ]; foreach ($properties as $property) { diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php index f083d21e04cd..1fa68472821c 100644 --- a/tests/Integration/Queue/ModelSerializationTest.php +++ b/tests/Integration/Queue/ModelSerializationTest.php @@ -472,6 +472,22 @@ public function test_it_respects_without_relations_attribute_applied_to_class() $this->assertEquals('hello', $unserialized->value->value); } + #[WithConfig('database.default', 'testing')] + public function test_it_respects_without_relations_attribute_applied_to_parent_class() + { + $user = User::create([ + 'email' => 'taylor@laravel.com', + ])->load(['roles']); + + $serialized = serialize(new ModelSerializationAttributeTargetsParentClassTestClass($user, new DataValueObject('hello'))); + + /** @var ModelSerializationAttributeTargetsParentClassTestClass $unserialized */ + $unserialized = unserialize($serialized); + + $this->assertFalse($unserialized->user->relationLoaded('roles')); + $this->assertEquals('hello', $unserialized->value->value); + } + public function test_serialization_types_empty_custom_eloquent_collection() { $class = new ModelSerializationTypedCustomCollectionTestClass( @@ -788,6 +804,11 @@ public function __construct(public User $user, public DataValueObject $value) } } +class ModelSerializationAttributeTargetsParentClassTestClass extends ModelSerializationAttributeTargetsClassTestClass +{ + // +} + class ModelRelationSerializationTestClass { use SerializesModels; From 56de979e7a655e8daa187f38c7a5d34e3e8dcfc3 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:34:38 +0000 Subject: [PATCH 118/596] Update facade docblocks --- src/Illuminate/Support/Facades/App.php | 16 +++++++------- src/Illuminate/Support/Facades/Bus.php | 2 +- src/Illuminate/Support/Facades/Cache.php | 6 ++--- src/Illuminate/Support/Facades/Config.php | 6 ++--- src/Illuminate/Support/Facades/Context.php | 22 +++++++++---------- src/Illuminate/Support/Facades/DB.php | 4 ++-- src/Illuminate/Support/Facades/Exceptions.php | 8 +++---- src/Illuminate/Support/Facades/Hash.php | 2 +- src/Illuminate/Support/Facades/Http.php | 18 +++++++-------- .../Support/Facades/MaintenanceMode.php | 2 +- .../Support/Facades/Notification.php | 2 +- src/Illuminate/Support/Facades/Process.php | 6 ++--- src/Illuminate/Support/Facades/Queue.php | 8 +++---- src/Illuminate/Support/Facades/Request.php | 8 +++---- src/Illuminate/Support/Facades/Schedule.php | 14 ++++++------ src/Illuminate/Support/Facades/Schema.php | 22 +++++++++---------- src/Illuminate/Support/Facades/Session.php | 2 +- src/Illuminate/Support/Facades/Storage.php | 10 ++++----- 18 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 5bad0f492df4..541792f5f5e5 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -53,7 +53,7 @@ * @method static void loadDeferredProviders() * @method static void loadDeferredProvider(string $service) * @method static void registerDeferredProvider(string $provider, string|null $service = null) - * @method static object|mixed make(string $abstract, array $parameters = []) + * @method static object|mixed make(string|string $abstract, array $parameters = []) * @method static bool bound(string $abstract) * @method static bool isBooted() * @method static void boot() @@ -79,7 +79,7 @@ * @method static never abort(int $code, string $message = '', array $headers = []) * @method static \Illuminate\Foundation\Application terminating(callable|string $callback) * @method static void terminate() - * @method static array getLoadedProviders() + * @method static array getLoadedProviders() * @method static bool providerIsLoaded(string $provider) * @method static array getDeferredServices() * @method static void setDeferredServices(array $services) @@ -119,11 +119,11 @@ * @method static mixed rebinding(string $abstract, \Closure $callback) * @method static mixed refresh(string $abstract, mixed $target, string $method) * @method static \Closure wrap(\Closure $callback, array $parameters = []) - * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) - * @method static \Closure|\Closure factory(string $abstract) - * @method static object|mixed makeWith(string|callable $abstract, array $parameters = []) - * @method static object|mixed get(string $id) - * @method static object build(\Closure|string $concrete) + * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) + * @method static \Closure|\Closure factory(string|string $abstract) + * @method static object|mixed makeWith(string|string|callable $abstract, array $parameters = []) + * @method static object|mixed get(string|string $id) + * @method static object build(\Closure|string $concrete) * @method static mixed resolveFromAttribute(\ReflectionAttribute $attribute) * @method static void beforeResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void resolving(\Closure|string $abstract, \Closure|null $callback = null) @@ -138,7 +138,7 @@ * @method static void forgetInstances() * @method static void forgetScopedInstances() * @method static void resolveEnvironmentUsing(callable|string|null $callback) - * @method static bool currentEnvironmentIs(array|string $environments) + * @method static bool currentEnvironmentIs(array|string $environments) * @method static \Illuminate\Foundation\Application getInstance() * @method static \Illuminate\Contracts\Container\Container|\Illuminate\Foundation\Application setInstance(\Illuminate\Contracts\Container\Container|null $container = null) * @method static void macro(string $name, object|callable $macro) diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index 9894bc405900..f0220accd31d 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -47,7 +47,7 @@ * @method static \Illuminate\Support\Collection dispatched(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedSync(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedAfterResponse(string $command, callable|null $callback = null) - * @method static \Illuminate\Support\Collection batched(callable $callback) + * @method static \Illuminate\Support\Collection batched(callable $callback) * @method static bool hasDispatched(string $command) * @method static bool hasDispatchedSync(string $command) * @method static bool hasDispatchedAfterResponse(string $command) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 9ec1e5600110..8144545ebb41 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -22,13 +22,13 @@ * @method static bool missing(\UnitEnum|string $key) * @method static mixed get(\UnitEnum|array|string $key, mixed $default = null) * @method static array many(array $keys) - * @method static iterable getMultiple(iterable $keys, mixed $default = null) + * @method static iterable getMultiple(iterable $keys, mixed $default = null) * @method static mixed pull(\UnitEnum|array|string $key, mixed $default = null) * @method static string string(\UnitEnum|string $key, \Closure|string|null $default = null) * @method static int integer(\UnitEnum|string $key, \Closure|int|null $default = null) * @method static float float(\UnitEnum|string $key, \Closure|float|null $default = null) * @method static bool boolean(\UnitEnum|string $key, \Closure|bool|null $default = null) - * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) + * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) * @method static bool put(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool set(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) @@ -46,7 +46,7 @@ * @method static \Illuminate\Cache\Limiters\ConcurrencyLimiterBuilder funnel(\UnitEnum|string $name) * @method static bool forget(\UnitEnum|array|string $key) * @method static bool delete(\UnitEnum|array|string $key) - * @method static bool deleteMultiple(iterable $keys) + * @method static bool deleteMultiple(iterable $keys) * @method static bool clear() * @method static bool flushLocks() * @method static \Illuminate\Cache\TaggedCache tags(mixed $names) diff --git a/src/Illuminate/Support/Facades/Config.php b/src/Illuminate/Support/Facades/Config.php index 09228769a306..990e34739f68 100755 --- a/src/Illuminate/Support/Facades/Config.php +++ b/src/Illuminate/Support/Facades/Config.php @@ -5,13 +5,13 @@ /** * @method static bool has(string $key) * @method static mixed get(array|string $key, mixed $default = null) - * @method static array getMany(array $keys) + * @method static array getMany(array $keys) * @method static string string(string $key, \Closure|string|null $default = null) * @method static int integer(string $key, \Closure|int|null $default = null) * @method static float float(string $key, \Closure|float|null $default = null) * @method static bool boolean(string $key, \Closure|bool|null $default = null) - * @method static array array(string $key, \Closure|array|null $default = null) - * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) + * @method static array array(string $key, \Closure|array|null $default = null) + * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) * @method static void set(array|string $key, mixed $value = null) * @method static void prepend(string $key, mixed $value) * @method static void push(string $key, mixed $value) diff --git a/src/Illuminate/Support/Facades/Context.php b/src/Illuminate/Support/Facades/Context.php index be57f00fa6d0..714ec2b6ddd8 100644 --- a/src/Illuminate/Support/Facades/Context.php +++ b/src/Illuminate/Support/Facades/Context.php @@ -7,22 +7,22 @@ * @method static bool missing(string $key) * @method static bool hasHidden(string $key) * @method static bool missingHidden(string $key) - * @method static array all() - * @method static array allHidden() + * @method static array all() + * @method static array allHidden() * @method static mixed get(string $key, mixed $default = null) * @method static mixed getHidden(string $key, mixed $default = null) * @method static mixed pull(string $key, mixed $default = null) * @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 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 mixed remember(string $key, mixed $value) * @method static mixed rememberHidden(string $key, mixed $value) - * @method static \Illuminate\Log\Context\Repository forget(string|array $key) - * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) + * @method static \Illuminate\Log\Context\Repository forget(string|array $key) + * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) * @method static \Illuminate\Log\Context\Repository addIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository addHiddenIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository push(string $key, mixed ...$values) @@ -33,7 +33,7 @@ * @method static \Illuminate\Log\Context\Repository decrement(string $key, int $amount = 1) * @method static bool stackContains(string $key, mixed $value, bool $strict = false) * @method static bool hiddenStackContains(string $key, mixed $value, bool $strict = false) - * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) + * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) * @method static bool isEmpty() * @method static \Illuminate\Log\Context\Repository dehydrating(callable $callback) * @method static \Illuminate\Log\Context\Repository hydrated(callable $callback) diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 3da739b441ba..425b8a83c153 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -23,7 +23,7 @@ * @method static string[] availableDrivers() * @method static void extend(string $name, callable $resolver) * @method static void forgetExtension(string $name) - * @method static array getConnections() + * @method static array getConnections() * @method static void setReconnector(callable $reconnector) * @method static \Illuminate\Database\DatabaseManager setApplication(\Illuminate\Contracts\Foundation\Application $app) * @method static void macro(string $name, object|callable $macro) @@ -42,7 +42,7 @@ * @method static array selectFromWriteConnection(string $query, array $bindings = []) * @method static array select(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) * @method static array selectResultSets(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) - * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) + * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) * @method static bool insert(string $query, array $bindings = []) * @method static int update(string $query, array $bindings = []) * @method static int delete(string $query, array $bindings = []) diff --git a/src/Illuminate/Support/Facades/Exceptions.php b/src/Illuminate/Support/Facades/Exceptions.php index 263b95bd0418..59b4b07ef2d7 100644 --- a/src/Illuminate/Support/Facades/Exceptions.php +++ b/src/Illuminate/Support/Facades/Exceptions.php @@ -15,7 +15,7 @@ * @method static \Illuminate\Foundation\Exceptions\Handler dontReportWhen(callable $dontReportWhen) * @method static \Illuminate\Foundation\Exceptions\Handler ignore(array|string $exceptions) * @method static \Illuminate\Foundation\Exceptions\Handler dontFlash(array|string $attributes) - * @method static \Illuminate\Foundation\Exceptions\Handler level(string $type, string $level) + * @method static \Illuminate\Foundation\Exceptions\Handler level(string<\Throwable> $type, string $level) * @method static void report(\Throwable $e) * @method static bool shouldReport(\Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler throttleUsing(callable $throttleUsing) @@ -26,14 +26,14 @@ * @method static \Illuminate\Foundation\Exceptions\Handler shouldRenderJsonWhen(callable $callback) * @method static \Illuminate\Foundation\Exceptions\Handler dontReportDuplicates() * @method static \Illuminate\Contracts\Debug\ExceptionHandler handler() - * @method static void assertReported(\Closure|string $exception) + * @method static void assertReported(\Closure|string<\Throwable> $exception) * @method static void assertReportedCount(int $count) - * @method static void assertNotReported(\Closure|string $exception) + * @method static void assertNotReported(\Closure|string<\Throwable> $exception) * @method static void assertNothingReported() * @method static void renderForConsole(\Symfony\Component\Console\Output\OutputInterface $output, \Throwable $e) * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwOnReport() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwFirstReported() - * @method static array reported() + * @method static array<\Throwable> reported() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake setHandler(\Illuminate\Contracts\Debug\ExceptionHandler $handler) * * @see \Illuminate\Foundation\Exceptions\Handler diff --git a/src/Illuminate/Support/Facades/Hash.php b/src/Illuminate/Support/Facades/Hash.php index 280585d6e374..450705ef0215 100755 --- a/src/Illuminate/Support/Facades/Hash.php +++ b/src/Illuminate/Support/Facades/Hash.php @@ -14,7 +14,7 @@ * @method static string getDefaultDriver() * @method static mixed driver(string|null $driver = null) * @method static \Illuminate\Hashing\HashManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Hashing\HashManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Hashing\HashManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 4967b314d7a7..a35310045307 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -10,21 +10,21 @@ * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalOptions(\Closure|array $options) * @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\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 \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(array|null $only = null) + * @method static \Illuminate\Http\Client\Factory allowStrayRequests(array|null $only = null) * @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|\Closure $callback) - * @method static void assertSentInOrder(array $callbacks) + * @method static void assertSentInOrder(array $callbacks) * @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(\Closure|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() @@ -65,7 +65,7 @@ * @method static \Illuminate\Http\Client\PendingRequest withMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withResponseMiddleware(callable $middleware) - * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) + * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) * @method static \Illuminate\Http\Client\PendingRequest beforeSending(callable $callback) * @method static \Illuminate\Http\Client\PendingRequest afterResponse(callable|null $callback) * @method static \Illuminate\Http\Client\PendingRequest throw(callable|null $callback = null) @@ -79,7 +79,7 @@ * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface patch(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface put(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface delete(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) - * @method static array pool(callable $callback, int|null $concurrency = 0) + * @method static array pool(callable $callback, int|null $concurrency = 0) * @method static \Illuminate\Http\Client\Batch batch(callable $callback) * @method static \Illuminate\Http\Client\Response|\Illuminate\Http\Client\Promises\LazyPromise send(string $method, string $url, array $options = []) * @method static \GuzzleHttp\Client buildClient() @@ -93,9 +93,9 @@ * @method static array mergeOptions(array ...$options) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) * @method static bool isAllowedRequestUrl(string $url) - * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) + * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) * @method static \GuzzleHttp\Promise\PromiseInterface|null getPromise() - * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) + * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) * @method static \Illuminate\Http\Client\PendingRequest dontTruncateExceptions() * @method static \Illuminate\Http\Client\PendingRequest setClient(\GuzzleHttp\Client $client) * @method static \Illuminate\Http\Client\PendingRequest setHandler(callable $handler) diff --git a/src/Illuminate/Support/Facades/MaintenanceMode.php b/src/Illuminate/Support/Facades/MaintenanceMode.php index b63d48191554..78c46e70926e 100644 --- a/src/Illuminate/Support/Facades/MaintenanceMode.php +++ b/src/Illuminate/Support/Facades/MaintenanceMode.php @@ -8,7 +8,7 @@ * @method static string getDefaultDriver() * @method static mixed driver(string|null $driver = null) * @method static \Illuminate\Foundation\MaintenanceModeManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Foundation\MaintenanceModeManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Foundation\MaintenanceModeManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Notification.php b/src/Illuminate/Support/Facades/Notification.php index d9086fc60411..9b6eb0b34bc9 100644 --- a/src/Illuminate/Support/Facades/Notification.php +++ b/src/Illuminate/Support/Facades/Notification.php @@ -16,7 +16,7 @@ * @method static \Illuminate\Notifications\ChannelManager locale(string $locale) * @method static mixed driver(string|null $driver = null) * @method static \Illuminate\Notifications\ChannelManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Notifications\ChannelManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Notifications\ChannelManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index f15e70bcd292..afb4a71b3d78 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -6,7 +6,7 @@ use Illuminate\Process\Factory; /** - * @method static \Illuminate\Process\PendingProcess command(array|string $command) + * @method static \Illuminate\Process\PendingProcess command(array|string $command) * @method static \Illuminate\Process\PendingProcess path(string $path) * @method static \Illuminate\Process\PendingProcess timeout(\Carbon\CarbonInterval|int $timeout) * @method static \Illuminate\Process\PendingProcess idleTimeout(\Carbon\CarbonInterval|int $timeout) @@ -16,8 +16,8 @@ * @method static \Illuminate\Process\PendingProcess quietly() * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) - * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) - * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) * @method static bool supportsTty() * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) * @method static \Illuminate\Process\PendingProcess|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 6adba39e69e6..09449af944bf 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -64,15 +64,15 @@ * @method static void assertCount(int $expectedCount) * @method static void assertNothingPushed() * @method static \Illuminate\Support\Collection pushed(string $job, callable|null $callback = null) - * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) - * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) + * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) + * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) * @method static bool hasPushed(string $job) - * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) + * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) * @method static \Illuminate\Support\Collection delayedJobs(string|null $queue = null) * @method static \Illuminate\Support\Collection reservedJobs(string|null $queue = null) * @method static bool shouldFakeJob(object $job) * @method static array pushedJobs() - * @method static array rawPushes() + * @method static array rawPushes() * @method static \Illuminate\Support\Testing\Fakes\QueueFake serializeAndRestore(bool $serializeAndRestore = true) * @method static void releaseUniqueJobLocks() * diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 2865715dcb98..0ff5c7aac60c 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -152,9 +152,9 @@ * @method static string|array|null post(string|null $key = null, string|array|null $default = null) * @method static bool hasCookie(string $key) * @method static string|array|null cookie(string|null $key = null, string|array|null $default = null) - * @method static array allFiles() + * @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) @@ -175,8 +175,8 @@ * @method static float|int clamp(string $key, int|float $min, int|float $max, int|float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, \UnitEnum|string|null $tz = null) * @method static \Carbon\CarbonInterval|null interval(string $key, \Carbon\Unit|string|null $unit = null) - * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string $enumClass, \BackedEnum|null $default = null) - * @method static \BackedEnum[] enums(string $key, string $enumClass) + * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string<\BackedEnum> $enumClass, \BackedEnum|null $default = null) + * @method static \BackedEnum[] enums(string $key, string<\BackedEnum> $enumClass) * @method static array array(array|string|null $key = null) * @method static \Illuminate\Support\Collection collect(array|string|null $key = null) * @method static array only(mixed $keys) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index 86a2c02e6933..7da6c9d1fb60 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -51,7 +51,7 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyFifteenMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThirtyMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyOddHour(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyTwoHours(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThreeHours(array|string|int $offset = 0) @@ -60,8 +60,8 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daily() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes at(string $time) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes dailyAt(string $time) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekdays() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekends() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes mondays() @@ -74,14 +74,14 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weeklyOn(mixed $dayOfWeek, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes lastDayOfMonth(string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array|int ...$days) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array>|int ...$days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterlyOn(int $dayOfQuarter = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes days(mixed $days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes timezone(\UnitEnum|\DateTimeZone|string $timezone) * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 5c617687bb3a..523813f228ed 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -10,31 +10,31 @@ * @method static void morphUsingUlids() * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) - * @method static array getSchemas() + * @method static array getSchemas() * @method static bool hasTable(string $table) * @method static bool hasView(string $view) - * @method static array getTables(string|string[]|null $schema = null) - * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) - * @method static array getViews(string|string[]|null $schema = null) - * @method static array getTypes(string|string[]|null $schema = null) + * @method static array getTables(string|string[]|null $schema = null) + * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) + * @method static array getViews(string|string[]|null $schema = null) + * @method static array getTypes(string|string[]|null $schema = null) * @method static bool hasColumn(string $table, string $column) - * @method static bool hasColumns(string $table, array $columns) + * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) * @method static void whenTableDoesntHaveColumn(string $table, string $column, \Closure $callback) * @method static void whenTableHasIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static void whenTableDoesntHaveIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static string getColumnType(string $table, string $column, bool $fullDefinition = false) - * @method static array getColumnListing(string $table) - * @method static array getColumns(string $table) - * @method static array getIndexes(string $table) - * @method static array getIndexListing(string $table) + * @method static array getColumnListing(string $table) + * @method static array getColumns(string $table) + * @method static array getIndexes(string $table) + * @method static array getIndexListing(string $table) * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) * @method static void dropIfExists(string $table) - * @method static void dropColumns(string $table, string|array $columns) + * @method static void dropColumns(string $table, string|array $columns) * @method static void dropAllTables() * @method static void dropAllViews() * @method static void dropAllTypes() diff --git a/src/Illuminate/Support/Facades/Session.php b/src/Illuminate/Support/Facades/Session.php index 269a98513a45..2999e3814da3 100755 --- a/src/Illuminate/Support/Facades/Session.php +++ b/src/Illuminate/Support/Facades/Session.php @@ -12,7 +12,7 @@ * @method static void setDefaultDriver(string $name) * @method static mixed driver(string|null $driver = null) * @method static \Illuminate\Session\SessionManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Session\SessionManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Session\SessionManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Storage.php b/src/Illuminate/Support/Facades/Storage.php index 9f37e575eca4..941d5fe69d25 100644 --- a/src/Illuminate/Support/Facades/Storage.php +++ b/src/Illuminate/Support/Facades/Storage.php @@ -40,10 +40,10 @@ * @method static bool move(string $from, string $to) * @method static int size(string $path) * @method static int lastModified(string $path) - * @method static array files(string|null $directory = null, bool $recursive = false) - * @method static array allFiles(string|null $directory = null) - * @method static array directories(string|null $directory = null, bool $recursive = false) - * @method static array allDirectories(string|null $directory = null) + * @method static array files(string|null $directory = null, bool $recursive = false) + * @method static array allFiles(string|null $directory = null) + * @method static array directories(string|null $directory = null, bool $recursive = false) + * @method static array allDirectories(string|null $directory = null) * @method static bool makeDirectory(string $path) * @method static bool deleteDirectory(string $directory) * @method static \Illuminate\Filesystem\FilesystemAdapter assertExists(string|array $path, string|null $content = null) @@ -81,7 +81,7 @@ * @method static mixed macroCall(string $method, array $parameters) * @method static bool has(string $location) * @method static string read(string $location) - * @method static \League\Flysystem\DirectoryListing listContents(string $location, bool $deep = false) + * @method static \League\Flysystem\DirectoryListing<\League\Flysystem\StorageAttributes> listContents(string $location, bool $deep = false) * @method static int fileSize(string $path) * @method static string visibility(string $path) * @method static void write(string $location, string $contents, array $config = []) From 912de244f88a69742b76e8a2807f6765947776da Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:38:26 +0000 Subject: [PATCH 119/596] Update version to v13.4.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 0c58bcda00a1..fdc3cdebfbd1 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 = '13.3.0'; + const VERSION = '13.4.0'; /** * The base path for the Laravel installation. From e98efa0eea0791805a6e086c395b26bda788ab25 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:40:04 +0000 Subject: [PATCH 120/596] Update CHANGELOG --- CHANGELOG.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc1c39517f8..aa99e7b31e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.3.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.4.0...13.x) + +## [v13.4.0](https://github.com/laravel/framework/compare/v13.3.0...v13.4.0) - 2026-04-07 + +* [13.x] Fix missing `Illuminate\Queue\Attributes\Delay` attribute by [@fadez](https://github.com/fadez) in https://github.com/laravel/framework/pull/59504 +* [13.x] Fix `$request->interval()` failing with very small float values by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/59502 +* [13.x] Add pint.json to export-ignore by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/59497 +* [13.x] Add --ignore-scripts to yarn in BroadcastingInstallCommand by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59494 +* [13.x] Fix static closure binding in remaining manager classes by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59493 +* [13.x] Fix CollectedBy attribute not resolving through abstract parent classes by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59488 +* [13.x] Fix: Allow runtime property overrides (onQueue) to take precedence over class attributes by [@niduranga](https://github.com/niduranga) in https://github.com/laravel/framework/pull/59468 +* [13.x] Use #[Delay] attribute in Bus Dispatcher by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59514 +* [13.x] Use #[Delay] attribute in NotificationSender by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59513 +* [13.x] Add `overflow` option to Carbon plus and minus by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59509 +* [13.x] Fix: respect null redirect in unauthenticated exception handler by [@timmylindh](https://github.com/timmylindh) in https://github.com/laravel/framework/pull/59505 +* [13.x] Fix TypeError in starts_with/ends_with validation rules on non-string values by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59541 +* [13.x] allow null to be passed directly to redirectGuestsTo() by [@timmylindh](https://github.com/timmylindh) in https://github.com/laravel/framework/pull/59526 +* Revert "[13.x] Remove unnecessary clone in SessionManager to prevent duplicate Redis connections" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/59542 +* [13.x] Fix deprecation warning in Contains and DoesntContain rules when values contain null by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59561 +* [13.x] Fix Str::markdown() and Str::inlineMarkdown() crash on null input by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59554 +* [13.x] Add queue methods to inspect jobs by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59511 +* Feature/form request strict mode by [@NurullahDemirel](https://github.com/NurullahDemirel) in https://github.com/laravel/framework/pull/59430 +* Bump vite from 7.3.1 to 7.3.2 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/59571 +* [13.x] Fix deprecation warning in In and NotIn rules when values contain null by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59576 +* [13.x] Add flushState to FormRequest to reset global strict mode between tests by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59574 +* [13.x] Fix `#[WithoutRelations]` queue attribute not being inherited by child classes by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/59568 ## [v13.3.0](https://github.com/laravel/framework/compare/v13.2.0...v13.3.0) - 2026-04-01 From 96613ecca2532a02ede96e034a007468b6f2971f Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:23:07 +0600 Subject: [PATCH 121/596] [13.x] Support #[Delay] attribute on queued mailables (#59580) The #[Delay] attribute is supported for queued jobs, event listeners, and notifications, but queued mailables only check the $delay property and ignore the attribute entirely. This adds the ReadsClassAttributes support trait to Mailable and uses getAttributeValue() in the queue() method, consistent with how Bus\Dispatcher, Events\Dispatcher, and NotificationSender handle the #[Delay] attribute. --- src/Illuminate/Mail/Mailable.php | 10 ++++-- tests/Mail/MailableQueuedTest.php | 52 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 92c55e17198e..d3c5048ddd77 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -12,6 +12,7 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\Translation\HasLocalePreference; +use Illuminate\Queue\Attributes\Delay; use Illuminate\Support\Collection; use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\HtmlString; @@ -20,6 +21,7 @@ use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\Localizable; use Illuminate\Support\Traits\Macroable; +use Illuminate\Support\Traits\ReadsClassAttributes; use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\Constraints\SeeInOrder; use PHPUnit\Framework\Assert as PHPUnit; @@ -31,7 +33,7 @@ class Mailable implements MailableContract, Renderable { - use Conditionable, ForwardsCalls, Localizable, Tappable, Macroable { + use Conditionable, ForwardsCalls, Localizable, ReadsClassAttributes, Tappable, Macroable { __call as macroCall; } @@ -224,8 +226,10 @@ public function send($mailer) */ public function queue(Queue $queue) { - if (isset($this->delay)) { - return $this->later($this->delay, $queue); + $delay = $this->getAttributeValue($this, Delay::class, 'delay'); + + if (isset($delay)) { + return $this->later($delay, $queue); } $connection = property_exists($this, 'connection') ? $this->connection : null; diff --git a/tests/Mail/MailableQueuedTest.php b/tests/Mail/MailableQueuedTest.php index 2cd87bc46795..da5b621b9baa 100644 --- a/tests/Mail/MailableQueuedTest.php +++ b/tests/Mail/MailableQueuedTest.php @@ -12,6 +12,7 @@ use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailer; use Illuminate\Mail\SendQueuedMailable; +use Illuminate\Queue\Attributes\Delay; use Illuminate\Support\Testing\Fakes\QueueFake; use Laravel\SerializableClosure\SerializableClosure; use Mockery as m; @@ -147,6 +148,41 @@ public function testQueuedMailableForwardsDeduplicatorToQueueJob(): void $this->assertEquals($mockedDeduplicator, $pushedJob->deduplicator->getClosure()); } + public function testQueuedMailableRespectsDelayAttribute(): void + { + $queueFake = new QueueFake(new Application); + $mailer = $this->getMockBuilder(Mailer::class) + ->setConstructorArgs($this->getMocks()) + ->onlyMethods(['createMessage', 'to']) + ->getMock(); + $mailer->setQueue($queueFake); + $mailable = new MailableQueueableStubWithDelayAttribute; + $queueFake->assertNothingPushed(); + $mailer->send($mailable); + $queueFake->assertPushedOn(null, SendQueuedMailable::class); + + $pushedJob = $queueFake->pushed(SendQueuedMailable::class)->first(); + $this->assertEquals(30, $pushedJob->delay); + } + + public function testQueuedMailableDelayPropertyOverridesAttribute(): void + { + $queueFake = new QueueFake(new Application); + $mailer = $this->getMockBuilder(Mailer::class) + ->setConstructorArgs($this->getMocks()) + ->onlyMethods(['createMessage', 'to']) + ->getMock(); + $mailer->setQueue($queueFake); + $mailable = new MailableQueueableStubWithDelayAttribute; + $mailable->delay = 60; + $queueFake->assertNothingPushed(); + $mailer->send($mailable); + $queueFake->assertPushedOn(null, SendQueuedMailable::class); + + $pushedJob = $queueFake->pushed(SendQueuedMailable::class)->first(); + $this->assertEquals(60, $pushedJob->delay); + } + public function testQueuedMailableForwardsDeduplicationIdMethodToQueueJob(): void { $queueFake = new QueueFake(new Application); @@ -206,6 +242,22 @@ public function messageGroup(): string } } +#[Delay(30)] +class MailableQueueableStubWithDelayAttribute extends Mailable implements ShouldQueue +{ + use Queueable; + + public function build(): self + { + $this + ->subject('lorem ipsum') + ->html('foo bar baz') + ->to('foo@example.tld'); + + return $this; + } +} + class MailableQueueableStubWithDeduplication extends Mailable implements ShouldQueue { use Queueable; From 451fd57169e1ebbec1fd9a95e2e128b211fa0497 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 7 Apr 2026 13:36:44 -0500 Subject: [PATCH 122/596] wip --- src/Illuminate/Foundation/Configuration/Middleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index a5d367aa79cf..312c746985cb 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -561,7 +561,7 @@ public function redirectUsersTo(callable|string $redirect) */ public function redirectTo(callable|string|null $guests = null, callable|string|null $users = null) { - $guests = is_string($guests) || is_null($guests) ? fn () => $guests : $guests; + $guests = is_string($guests) ? fn () => $guests : $guests; $users = is_string($users) ? fn () => $users : $users; if ($guests) { From edffb2f30aa27a1f943f9e13829fb90f915deec8 Mon Sep 17 00:00:00 2001 From: Niduranga Jayarathna Date: Wed, 8 Apr 2026 19:18:31 +0530 Subject: [PATCH 123/596] [13.x] Added inheritance support for Controller Middleware attributes. (#59597) * Added inheritance support for Controller Middleware attributes * Update Route.php * Update Route.php * add test --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Routing/Route.php | 24 +++++-- .../RoutingControllerAttributeTest.php | 63 +++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/Routing/RoutingControllerAttributeTest.php diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index 447362d0e459..fdf48d27f972 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -1179,16 +1179,30 @@ protected function attributeProvidedControllerMiddleware(string $class, string $ { try { $reflectionClass = new ReflectionClass($class); - $reflectionMethod = $reflectionClass->getMethod($method); } catch (ReflectionException) { return []; } - return (new Collection(array_merge( - $reflectionClass->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF), - $reflectionMethod->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF), - )))->map(function (ReflectionAttribute $attribute) use ($method) { + $attributes = new Collection; + + $current = $reflectionClass; + + while ($current) { + $classAttributes = array_reverse($current->getAttributes( + MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF + )); + + foreach ($classAttributes as $attribute) { + $attributes->prepend($attribute); + } + + $current = $current->getParentClass(); + } + + return $attributes->merge( + $reflectionMethod->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF) + )->map(function (ReflectionAttribute $attribute) use ($method) { $instance = $attribute->newInstance(); return static::methodExcludedByOptions( diff --git a/tests/Routing/RoutingControllerAttributeTest.php b/tests/Routing/RoutingControllerAttributeTest.php new file mode 100644 index 000000000000..ba77ae2820f8 --- /dev/null +++ b/tests/Routing/RoutingControllerAttributeTest.php @@ -0,0 +1,63 @@ + InheritMiddlewareController::class.'@index']); + $route->setContainer(new Container); + + $this->assertEquals(['auth', 'log'], $route->gatherMiddleware()); + } + + public function testControllerMiddlewareAttributesAreInheritedInDeclarationOrder() + { + $route = new Route('GET', 'foo', ['uses' => InheritMiddlewareDeclarationOrderController::class.'@index']); + $route->setContainer(new Container); + + $this->assertEquals(['middleware1', 'middleware2', 'middleware3'], $route->gatherMiddleware()); + } +} + +abstract class Controller +{ + // +} + +#[Middleware('auth')] +abstract class BaseMiddlewareController extends Controller +{ + // +} + +#[Middleware('log')] +class InheritMiddlewareController extends BaseMiddlewareController +{ + public function index() + { + // + } +} + +#[Middleware('middleware1')] +#[Middleware('middleware2')] +abstract class BaseMiddlewareDeclarationOrderController extends Controller +{ + // +} + +#[Middleware('middleware3')] +class InheritMiddlewareDeclarationOrderController extends BaseMiddlewareDeclarationOrderController +{ + public function index() + { + // + } +} From 26013caaf61036704c25c3caf3e46e39a83d31e1 Mon Sep 17 00:00:00 2001 From: Timmy Lindholm <74464421+timmylindh@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:54:12 +0200 Subject: [PATCH 124/596] [13.x] Normalize phpredis SSL context for single and cluster connections (#59569) * fix: phpredis config normalization * formatting --------- Co-authored-by: Taylor Otwell --- .../Redis/Connectors/PhpRedisConnector.php | 46 +++++- tests/Redis/PhpRedisConnectorTest.php | 148 ++++++++++++++++++ 2 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 tests/Redis/PhpRedisConnectorTest.php diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 7391e68ce540..c1dffb08fb6c 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -179,7 +179,7 @@ protected function establishConnection($client, array $config) } if (version_compare(phpversion('redis'), '5.3.0', '>=') && ! is_null($context = Arr::get($config, 'context'))) { - $parameters[] = $context; + $parameters[] = $this->normalizeContext($context); } $client->{$persistent ? 'pconnect' : 'connect'}(...$parameters); @@ -207,7 +207,7 @@ protected function createRedisClusterInstance(array $servers, array $options) } if (version_compare(phpversion('redis'), '5.3.2', '>=') && ! is_null($context = Arr::get($options, 'context'))) { - $parameters[] = $context; + $parameters[] = $this->normalizeClusterContext($context); } return tap(new RedisCluster(...$parameters), function ($client) use ($options) { @@ -256,6 +256,48 @@ protected function formatHost(array $options) return $options['host']; } + /** + * Normalize the SSL context for a single Redis connection. + * + * Redis::connect() expects the context as ['stream' => ['verify_peer' => false, ...]]. + * + * @param array $context + * @return array + */ + protected function normalizeContext(array $context) + { + if (isset($context['stream'])) { + return $context; + } + + if (isset($context['ssl']) && is_array($context['ssl'])) { + return ['stream' => $context['ssl']]; + } + + return ['stream' => $context]; + } + + /** + * Normalize the SSL context for a RedisCluster connection. + * + * RedisCluster::__construct() expects a flat context ['verify_peer' => false, ...]. + * + * @param array $context + * @return array + */ + protected function normalizeClusterContext(array $context) + { + if (isset($context['ssl']) && is_array($context['ssl'])) { + return $context['ssl']; + } + + if (isset($context['stream']) && is_array($context['stream'])) { + return $context['stream']; + } + + return $context; + } + /** * Parse a "friendly" backoff algorithm name into an integer. * diff --git a/tests/Redis/PhpRedisConnectorTest.php b/tests/Redis/PhpRedisConnectorTest.php new file mode 100644 index 000000000000..37ce51a66982 --- /dev/null +++ b/tests/Redis/PhpRedisConnectorTest.php @@ -0,0 +1,148 @@ +connector = new PhpRedisConnector; + } + + public function testNormalizeContextWrapsFlatArrayInStream() + { + $result = $this->callNormalizeContext([ + 'verify_peer' => false, + 'verify_peer_name' => false, + ]); + + $this->assertSame([ + 'stream' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ], $result); + } + + public function testNormalizeContextConvertsSslKeyToStream() + { + $result = $this->callNormalizeContext([ + 'ssl' => [ + 'verify_peer' => false, + 'cafile' => '/path/to/ca.pem', + ], + ]); + + $this->assertSame([ + 'stream' => [ + 'verify_peer' => false, + 'cafile' => '/path/to/ca.pem', + ], + ], $result); + } + + public function testNormalizeContextPassesThroughStreamKey() + { + $context = [ + 'stream' => [ + 'verify_peer' => false, + ], + ]; + + $result = $this->callNormalizeContext($context); + + $this->assertSame($context, $result); + } + + public function testNormalizeClusterContextUnwrapsSslKey() + { + $result = $this->callNormalizeClusterContext([ + 'ssl' => [ + 'verify_peer' => false, + 'peer_name' => 'example.com', + ], + ]); + + $this->assertSame([ + 'verify_peer' => false, + 'peer_name' => 'example.com', + ], $result); + } + + public function testNormalizeClusterContextUnwrapsStreamKey() + { + $result = $this->callNormalizeClusterContext([ + 'stream' => [ + 'verify_peer' => false, + ], + ]); + + $this->assertSame([ + 'verify_peer' => false, + ], $result); + } + + public function testNormalizeClusterContextPassesThroughFlatArray() + { + $context = [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ]; + + $result = $this->callNormalizeClusterContext($context); + + $this->assertSame($context, $result); + } + + public function testNormalizeContextSslKeyTakesPrecedenceOverFlatKeys() + { + $result = $this->callNormalizeContext([ + 'verify_peer' => true, + 'ssl' => [ + 'verify_peer' => false, + ], + ]); + + $this->assertSame([ + 'stream' => [ + 'verify_peer' => false, + ], + ], $result); + } + + public function testNormalizeClusterContextSslKeyTakesPrecedenceOverFlatKeys() + { + $result = $this->callNormalizeClusterContext([ + 'verify_peer' => true, + 'ssl' => [ + 'verify_peer' => false, + ], + ]); + + $this->assertSame([ + 'verify_peer' => false, + ], $result); + } + + protected function callNormalizeContext(array $context): array + { + $method = new ReflectionMethod(PhpRedisConnector::class, 'normalizeContext'); + + return $method->invoke($this->connector, $context); + } + + protected function callNormalizeClusterContext(array $context): array + { + $method = new ReflectionMethod(PhpRedisConnector::class, 'normalizeClusterContext'); + + return $method->invoke($this->connector, $context); + } +} From 38a0031095afd3cc96ce3645b8e54f0fca23dc44 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:20:15 -0400 Subject: [PATCH 125/596] memoize result (#59610) --- src/Illuminate/Foundation/Testing/TestCase.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index e4d2f531bae2..7ab44a6e88d0 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -30,6 +30,13 @@ abstract class TestCase extends BaseTestCase */ protected array $traitsUsedByTest; + /** + * Memoized result of the withoutBootingFramework check. + * + * @var bool|null + */ + protected ?bool $withoutBootingFramework = null; + /** * Creates the application. * @@ -105,10 +112,14 @@ protected function tearDown(): void */ protected function withoutBootingFramework(): bool { + if ($this->withoutBootingFramework !== null) { + return $this->withoutBootingFramework; + } + try { - return (new ReflectionMethod(static::class, $this->name()))->getAttributes(UnitTest::class) !== []; + return $this->withoutBootingFramework = (new ReflectionMethod(static::class, $this->name()))->getAttributes(UnitTest::class) !== []; } catch (Throwable) { - return false; + return $this->withoutBootingFramework = false; } } From 31309d8cb63a4b7e3983158190ae0af1a52d56a0 Mon Sep 17 00:00:00 2001 From: Sebastian Cabarcas Berrio <42840369+scabarcas17@users.noreply.github.com> Date: Thu, 9 Apr 2026 08:38:07 -0500 Subject: [PATCH 126/596] [13.x] Add missing @throws and docblocks for concurrency and model info methods (#59602) Co-authored-by: sebastian cabarcas --- src/Illuminate/Concurrency/ProcessDriver.php | 2 ++ src/Illuminate/Database/Eloquent/ModelInfo.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index 47439a605723..823a6ba8584b 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -26,6 +26,8 @@ public function __construct(protected ProcessFactory $processFactory) /** * Run the given tasks concurrently and return an array containing the results. + * + * @throws \Throwable */ public function run(Closure|array $tasks): array { diff --git a/src/Illuminate/Database/Eloquent/ModelInfo.php b/src/Illuminate/Database/Eloquent/ModelInfo.php index f80d82727628..0c476a54a879 100644 --- a/src/Illuminate/Database/Eloquent/ModelInfo.php +++ b/src/Illuminate/Database/Eloquent/ModelInfo.php @@ -77,21 +77,39 @@ public function toArray() ]; } + /** + * Determine if the given offset exists. + */ public function offsetExists(mixed $offset): bool { return property_exists($this, $offset); } + /** + * Get the value for a given offset. + * + * @throws \InvalidArgumentException + */ public function offsetGet(mixed $offset): mixed { return property_exists($this, $offset) ? $this->{$offset} : throw new InvalidArgumentException("Property {$offset} does not exist."); } + /** + * Set the value at the given offset. + * + * @throws \LogicException + */ public function offsetSet(mixed $offset, mixed $value): void { throw new LogicException(self::class.' may not be mutated using array access.'); } + /** + * Unset the value at the given offset. + * + * @throws \LogicException + */ public function offsetUnset(mixed $offset): void { throw new LogicException(self::class.' may not be mutated using array access.'); From 1741307c50b52a8293db001f6d2d66504ea10ef0 Mon Sep 17 00:00:00 2001 From: Dominik Kohler <18621527+kohlerdominik@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:52:51 +0200 Subject: [PATCH 127/596] Fix ShouldBeUniqueUntilProcessing retries releasing locks they don't own (#59567) --- src/Illuminate/Queue/CallQueuedHandler.php | 4 +- tests/Events/QueuedEventsTest.php | 1 + tests/Integration/Queue/UniqueJobTest.php | 43 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/CallQueuedHandler.php b/src/Illuminate/Queue/CallQueuedHandler.php index c545afc25e82..7b1b4b5d52e2 100644 --- a/src/Illuminate/Queue/CallQueuedHandler.php +++ b/src/Illuminate/Queue/CallQueuedHandler.php @@ -120,12 +120,12 @@ protected function dispatchThroughMiddleware(Job $job, $command) return (new Pipeline($this->container))->send($command) ->through(array_merge(method_exists($command, 'middleware') ? $command->middleware() : [], $command->middleware ?? [])) ->finally(function ($command) use (&$lockReleased) { - if (! $lockReleased && $this->commandShouldBeUniqueUntilProcessing($command) && ! $command->job->isReleased()) { + if (! $lockReleased && $this->commandShouldBeUniqueUntilProcessing($command) && ! $command->job->isReleased() && $command->job->attempts() <= 1) { $this->ensureUniqueJobLockIsReleased($command); } }) ->then(function ($command) use ($job, &$lockReleased) { - if ($this->commandShouldBeUniqueUntilProcessing($command)) { + if ($this->commandShouldBeUniqueUntilProcessing($command) && $job->attempts() <= 1) { $this->ensureUniqueJobLockIsReleased($command); $lockReleased = true; diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index 4d39d8e7d3ba..c350b0f6a096 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -587,6 +587,7 @@ public function testUniqueUntilProcessingLockIsReleasedBeforeHandling() $job->shouldReceive('isDeleted')->andReturn(false); $job->shouldReceive('isReleased')->andReturn(false); $job->shouldReceive('isDeletedOrReleased')->andReturn(false); + $job->shouldReceive('attempts')->andReturn(1); $job->shouldReceive('delete')->once(); $handler = new CallQueuedHandler(new BusDispatcher($container), $container); diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index 9b0f8519e48f..978826fad225 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -145,6 +145,31 @@ public function testLockCanBeReleasedBeforeProcessing() $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); } + public function testRetryOfUniqueUntilProcessingJobDoesNotForceReleaseSubsequentLock() + { + $this->markTestSkippedWhenUsingSyncQueueDriver(); + + dispatch($job = new UniqueUntilProcessingRetryJob); + + // Lock acquired at dispatch time. + $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); + + $this->runQueueWorkerCommand(['--once' => true]); // attempt 1: releases lock, then fails + + $this->assertTrue($job::$handled); + + // Lock was correctly released before attempt 1 ran. Simulate a subsequent external dispatch + // acquiring it (asserts it was free and holds it for the rest of the test). + $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 60)->get()); + + // Attempt 2 (the retry) must not force-release the lock it did not acquire. + UniqueUntilProcessingRetryJob::$handled = false; + $this->runQueueWorkerCommand(['--once' => true]); // attempt 2 + + $this->assertTrue($job::$handled); + $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); + } + public function testLockIsReleasedOnModelNotFoundException() { UniqueTestSerializesModelsJob::$handled = false; @@ -302,6 +327,24 @@ class UniqueUntilStartTestJob extends UniqueTestJob implements ShouldBeUniqueUnt public $tries = 2; } +class UniqueUntilProcessingRetryJob implements ShouldQueue, ShouldBeUniqueUntilProcessing +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public $tries = 2; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + + if ($this->attempts() === 1) { + throw new Exception('First attempt failure.'); + } + } +} + class UniqueTestSerializesModelsJob extends UniqueTestJob { use SerializesModels; From 64a0ff275c688b081942f9f416058f1124f51579 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 9 Apr 2026 09:38:15 -0500 Subject: [PATCH 128/596] formatting --- src/Illuminate/Queue/RedisQueue.php | 64 ++++++++----------- .../Redis/Connections/Connection.php | 24 +++---- .../Connections/PhpRedisClusterConnection.php | 22 +++---- .../Connections/PredisClusterConnection.php | 22 +++---- .../Redis/Connectors/PhpRedisConnector.php | 28 ++++---- .../Redis/Limiters/ConcurrencyLimiter.php | 40 ++++++------ 6 files changed, 90 insertions(+), 110 deletions(-) diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index ca468ae11b2c..3d7423030fb6 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -112,7 +112,7 @@ public function __construct( */ public function size($queue = null) { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); return $this->getConnection()->eval( LuaScripts::size(), 3, $queue, $queue.':delayed', $queue.':reserved' @@ -127,7 +127,7 @@ public function size($queue = null) */ public function pendingSize($queue = null) { - return $this->getConnection()->llen($this->getRedisKey($queue)); + return $this->getConnection()->llen($this->getQueueRedisKey($queue)); } /** @@ -138,7 +138,7 @@ public function pendingSize($queue = null) */ public function delayedSize($queue = null) { - return $this->getConnection()->zcard($this->getRedisKey($queue).':delayed'); + return $this->getConnection()->zcard($this->getQueueRedisKey($queue).':delayed'); } /** @@ -149,7 +149,7 @@ public function delayedSize($queue = null) */ public function reservedSize($queue = null) { - return $this->getConnection()->zcard($this->getRedisKey($queue).':reserved'); + return $this->getConnection()->zcard($this->getQueueRedisKey($queue).':reserved'); } /** @@ -160,7 +160,7 @@ public function reservedSize($queue = null) */ public function pendingJobs($queue = null): Collection { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); return (new Collection($this->getConnection()->lrange($queue, 0, -1))) ->map(fn ($payload) => InspectedJob::fromPayload($payload)); @@ -174,7 +174,7 @@ public function pendingJobs($queue = null): Collection */ public function delayedJobs($queue = null): Collection { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); return (new Collection($this->getConnection()->zrange($queue.':delayed', 0, -1))) ->map(fn ($payload) => InspectedJob::fromPayload($payload)); @@ -188,7 +188,7 @@ public function delayedJobs($queue = null): Collection */ public function reservedJobs($queue = null): Collection { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); return (new Collection($this->getConnection()->zrange($queue.':reserved', 0, -1))) ->map(fn ($payload) => InspectedJob::fromPayload($payload)); @@ -202,7 +202,7 @@ public function reservedJobs($queue = null): Collection */ public function creationTimeOfOldestPendingJob($queue = null) { - $payload = $this->getConnection()->lindex($this->getRedisKey($queue), 0); + $payload = $this->getConnection()->lindex($this->getQueueRedisKey($queue), 0); if (! $payload) { return null; @@ -275,7 +275,7 @@ function ($payload, $queue) { */ public function pushRaw($payload, $queue = null, array $options = []) { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); $this->getConnection()->eval( LuaScripts::push(), 2, $queue, @@ -318,7 +318,7 @@ function ($payload, $queue, $delay) { protected function laterRaw($delay, $payload, $queue = null) { $this->getConnection()->eval( - LuaScripts::later(), 1, $this->getRedisKey($queue).':delayed', + LuaScripts::later(), 1, $this->getQueueRedisKey($queue).':delayed', $this->availableAt($delay), $payload ); @@ -350,7 +350,7 @@ protected function createPayloadArray($job, $queue, $data = '') */ public function pop($queue = null, $index = 0) { - $this->migrate($prefixed = $this->getRedisKey($queue)); + $this->migrate($prefixed = $this->getQueueRedisKey($queue)); $block = ! $this->secondaryQueueHadJob && $index == 0; @@ -438,7 +438,7 @@ protected function retrieveNextJob($queue, $block = true) */ public function deleteReserved($queue, $job) { - $this->getConnection()->zrem($this->getRedisKey($queue).':reserved', $job->getReservedJob()); + $this->getConnection()->zrem($this->getQueueRedisKey($queue).':reserved', $job->getReservedJob()); } /** @@ -451,7 +451,7 @@ public function deleteReserved($queue, $job) */ public function deleteAndRelease($queue, $job, $delay) { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); $this->getConnection()->eval( LuaScripts::release(), 2, $queue.':delayed', $queue.':reserved', @@ -467,7 +467,7 @@ public function deleteAndRelease($queue, $job, $delay) */ public function clear($queue) { - $queue = $this->getRedisKey($queue); + $queue = $this->getQueueRedisKey($queue); return $this->getConnection()->eval( LuaScripts::clear(), 4, $queue, $queue.':delayed', @@ -499,48 +499,36 @@ public function getQueue($queue) /** * Get the cluster-safe Redis key for the given queue. * - * When connected to a Redis Cluster, queue names are wrapped in hash tags - * to ensure all related keys (queue, delayed, reserved, notify) hash to the - * same slot, which is required for multi-key Lua scripts. - * * @param string|null $queue * @return string */ - protected function getRedisKey($queue = null) + protected function getQueueRedisKey($queue = null) { $queue = $queue ?: $this->default; - if ($this->isClusterConnection() && ! Connection::hasHashTag($queue)) { - return $this->getQueue('{'.$queue.'}'); - } - - return $this->getQueue($queue); + return $this->isClusterConnection() && ! Connection::hasHashTag($queue) + ? $this->getQueue('{'.$queue.'}') + : $this->getQueue($queue); } /** - * Determine if the connection is a Redis Cluster connection. - * - * The result is cached for the lifetime of this queue instance. + * Get the connection for the queue. * - * @return bool + * @return \Illuminate\Redis\Connections\Connection */ - protected function isClusterConnection() + public function getConnection() { - if (is_null($this->isCluster)) { - $this->isCluster = $this->getConnection()->isCluster(); - } - - return $this->isCluster; + return $this->redis->connection($this->connection); } /** - * Get the connection for the queue. + * Determine if the connection is a Redis Cluster connection. * - * @return \Illuminate\Redis\Connections\Connection + * @return bool */ - public function getConnection() + protected function isClusterConnection() { - return $this->redis->connection($this->connection); + return $this->isCluster ??= $this->getConnection()->isCluster(); } /** diff --git a/src/Illuminate/Redis/Connections/Connection.php b/src/Illuminate/Redis/Connections/Connection.php index 5451a22c8d35..21922d4d01fe 100644 --- a/src/Illuminate/Redis/Connections/Connection.php +++ b/src/Illuminate/Redis/Connections/Connection.php @@ -182,6 +182,16 @@ public function listenForFailures(Closure $callback) $this->events?->listen(CommandFailed::class, $callback); } + /** + * Determine if the connection is a cluster connection. + * + * @return bool + */ + public function isCluster() + { + return false; + } + /** * Get the connection name. * @@ -236,23 +246,9 @@ public function unsetEventDispatcher() $this->events = null; } - /** - * Determine if the connection is a cluster connection. - * - * @return bool - */ - public function isCluster() - { - return false; - } - /** * Determine if the given key contains a Redis Cluster hash tag. * - * A hash tag is a substring enclosed in braces with at least one character - * between them (e.g., "{user}:sessions"). Empty braces ("{}") are not - * considered a valid hash tag. - * * @param string $key * @return bool */ diff --git a/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php b/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php index 9a31dd25b367..b21a65c1eb36 100644 --- a/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php +++ b/src/Illuminate/Redis/Connections/PhpRedisClusterConnection.php @@ -20,17 +20,6 @@ class PhpRedisClusterConnection extends PhpRedisConnection */ protected $defaultNode; - /** - * Determine if the connection is a cluster connection. - * - * @return bool - */ - #[\Override] - public function isCluster() - { - return true; - } - /** * Scan all keys based on the given options. * @@ -89,4 +78,15 @@ private function defaultNode() return $this->defaultNode; } + + /** + * Determine if the connection is a cluster connection. + * + * @return bool + */ + #[\Override] + public function isCluster() + { + return true; + } } diff --git a/src/Illuminate/Redis/Connections/PredisClusterConnection.php b/src/Illuminate/Redis/Connections/PredisClusterConnection.php index 8d5f66a85daa..a42b40d7ad21 100644 --- a/src/Illuminate/Redis/Connections/PredisClusterConnection.php +++ b/src/Illuminate/Redis/Connections/PredisClusterConnection.php @@ -7,17 +7,6 @@ class PredisClusterConnection extends PredisConnection { - /** - * Determine if the connection is a cluster connection. - * - * @return bool - */ - #[\Override] - public function isCluster() - { - return true; - } - /** * Get the keys that match the given pattern. * @@ -50,4 +39,15 @@ public function flushdb() $node->executeCommand(tap(new $command)->setArguments(func_get_args())); } } + + /** + * Determine if the connection is a cluster connection. + * + * @return bool + */ + #[\Override] + public function isCluster() + { + return true; + } } diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 4247e9d73ab0..f80e0dc28f4d 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -260,35 +260,35 @@ protected function createRedisClusterInstance(array $servers, array $options) } /** - * Format the password for a Redis cluster connection. + * Format the host using the scheme if available. * * @param array $options - * @return string|array|null + * @return string */ - protected function formatClusterPassword(array $options) + protected function formatHost(array $options) { - $password = $options['password'] ?? null; - - if (isset($options['username']) && $options['username'] !== '' && is_string($password)) { - return [$options['username'], $password]; + if (isset($options['scheme'])) { + return Str::start($options['host'], "{$options['scheme']}://"); } - return $password; + return $options['host']; } /** - * Format the host using the scheme if available. + * Format the password for a Redis cluster connection. * * @param array $options - * @return string + * @return string|array|null */ - protected function formatHost(array $options) + protected function formatClusterPassword(array $options) { - if (isset($options['scheme'])) { - return Str::start($options['host'], "{$options['scheme']}://"); + $password = $options['password'] ?? null; + + if (isset($options['username']) && $options['username'] !== '' && is_string($password)) { + return [$options['username'], $password]; } - return $options['host']; + return $password; } /** diff --git a/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php b/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php index 19915e2cfdd7..8f156ee40a88 100644 --- a/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php +++ b/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php @@ -115,34 +115,12 @@ protected function acquire($id) return $prefix.$i; }, range(1, $this->maxLocks)); - // The Lua lockScript returns ARGV[1]..index (i.e. prefix concatenated with - // the slot index). The release() method uses that return value as KEYS[1], - // so the two must stay in sync — any change to $prefix here must be - // reflected in the Lua script's return expression. return $this->redis->eval(...array_merge( [$this->lockScript(), count($slots)], array_merge($slots, [$prefix, $this->releaseAfter, $id]) )); } - /** - * Get the cluster-safe key prefix for lock slots. - * - * The result is cached for the lifetime of this limiter instance. - * - * @return string - */ - protected function getPrefix() - { - if (is_null($this->prefix)) { - $this->prefix = $this->redis->isCluster() && ! Connection::hasHashTag($this->name) - ? '{'.$this->name.'}' - : $this->name; - } - - return $this->prefix; - } - /** * Get the Lua script for acquiring a lock. * @@ -196,4 +174,22 @@ protected function releaseScript() end LUA; } + + /** + * Get the cluster-safe key prefix for lock slots. + * + * The result is cached for the lifetime of this limiter instance. + * + * @return string + */ + protected function getPrefix() + { + if (is_null($this->prefix)) { + $this->prefix = $this->redis->isCluster() && ! Connection::hasHashTag($this->name) + ? '{'.$this->name.'}' + : $this->name; + } + + return $this->prefix; + } } From 0f00b99e6d81af049f14602625a14e95de1e6e94 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:55:59 +0000 Subject: [PATCH 129/596] Update facade docblocks --- src/Illuminate/Support/Facades/Redis.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Support/Facades/Redis.php b/src/Illuminate/Support/Facades/Redis.php index 1b467126fa8a..044447e1b04e 100755 --- a/src/Illuminate/Support/Facades/Redis.php +++ b/src/Illuminate/Support/Facades/Redis.php @@ -20,11 +20,13 @@ * @method static mixed command(string $method, array $parameters = []) * @method static void listen(\Closure $callback) * @method static void listenForFailures(\Closure $callback) + * @method static bool isCluster() * @method static string|null getName() * @method static \Illuminate\Redis\Connections\Connection setName(string $name) * @method static \Illuminate\Contracts\Events\Dispatcher|null getEventDispatcher() * @method static void setEventDispatcher(\Illuminate\Contracts\Events\Dispatcher $events) * @method static void unsetEventDispatcher() + * @method static bool hasHashTag(string $key) * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) * @method static bool hasMacro(string $name) From 6a4b14cf52ed50293388f9f99fd8d9aa3ec8416e Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 9 Apr 2026 22:56:41 +0800 Subject: [PATCH 130/596] [13.x] chore: Update PHP version from 8.2 to 8.3 in `bin/test.sh` script (#59605) --- bin/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test.sh b/bin/test.sh index 42b08961acee..aaded71acd15 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash down=false -php="8.2" +php="8.3" while true; do case "$1" in From 0dcc8d2ba7f41bc8376a08e9ccd5d7b83e6a6d90 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 9 Apr 2026 16:20:28 +0100 Subject: [PATCH 131/596] [13.x] Fix RedisQueueTest (#59613) * Update RedisQueueTest.php * andd here * Update QueueRedisQueueTest.php --- tests/Integration/Queue/RedisQueueTest.php | 24 ++++++++--------- tests/Queue/QueueRedisQueueTest.php | 30 +++++++++++----------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/Integration/Queue/RedisQueueTest.php b/tests/Integration/Queue/RedisQueueTest.php index 13d8ae955146..ca6ed36028f0 100644 --- a/tests/Integration/Queue/RedisQueueTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -62,9 +62,9 @@ private function setQueue($driver, $default = 'default', $connection = null, $re $this->queue->setContainer($this->container); } - private function getRedisKey($queue = null) + private function getQueueRedisKey($queue = null) { - return (new \ReflectionMethod($this->queue, 'getRedisKey'))->invoke($this->queue, $queue); + return (new \ReflectionMethod($this->queue, 'getQueueRedisKey'))->invoke($this->queue, $queue); } /** @@ -93,7 +93,7 @@ public function testExpiredJobsArePopped($driver) $this->assertEquals($jobs[3], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertNull($this->queue->pop()); - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); $this->assertEquals(3, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); } @@ -168,7 +168,7 @@ public function testPopProperlyPopsJobOffOfRedis($driver) $this->assertEquals($redisJob->getJobId(), json_decode($redisJob->getReservedJob())->id); // Check reserved queue - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; @@ -196,7 +196,7 @@ public function testPopProperlyPopsDelayedJobOffOfRedis($driver) $after = $this->currentTime(); // Check reserved queue - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; @@ -227,7 +227,7 @@ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) $after = $this->currentTime(); // Check reserved queue - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; @@ -282,7 +282,7 @@ public function testBlockingPopProperlyPopsExpiredJobs($driver) $this->assertEquals($jobs[0], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertEquals($jobs[1], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(0, $this->redis[$driver]->connection()->llen("$redisKey:notify")); $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); $this->assertEquals(2, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); @@ -319,7 +319,7 @@ public function testNotExpireJobsWhenExpireNull($driver) $after = $this->currentTime(); // Check reserved queue - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(2, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); @@ -360,7 +360,7 @@ public function testExpireJobsWhenExpireSet($driver) $after = $this->currentTime(); // Check reserved queue - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); $result = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:reserved", -INF, INF, ['withscores' => true]); $reservedJob = array_keys($result)[0]; @@ -391,7 +391,7 @@ public function testRelease($driver) $after = $this->currentTime(); // check the content of delayed queue - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(1, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); $results = $this->redis[$driver]->connection()->zrangebyscore("$redisKey:delayed", -INF, INF, ['withscores' => true]); @@ -447,7 +447,7 @@ public function testDelete($driver) $redisJob->delete(); - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("$redisKey:delayed")); $this->assertEquals(0, $this->redis[$driver]->connection()->zcard("$redisKey:reserved")); $this->assertEquals(0, $this->redis[$driver]->connection()->llen("$redisKey")); @@ -472,7 +472,7 @@ public function testClear($driver) $this->assertEquals(2, $this->queue->clear(null)); $this->assertEquals(0, $this->queue->size()); - $redisKey = $this->getRedisKey($default); + $redisKey = $this->getQueueRedisKey($default); $this->assertEquals(0, $this->redis[$driver]->connection()->llen("$redisKey:notify")); } diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 108a43b76e59..4d2eb94239af 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -206,8 +206,8 @@ public function testGetRedisKeyReturnsPlainKeyForNonCluster() $connection->shouldReceive('isCluster')->andReturn(false); $redis->shouldReceive('connection')->andReturn($connection); - $this->assertSame('queues:default', $queue->testGetRedisKey(null)); - $this->assertSame('queues:emails', $queue->testGetRedisKey('emails')); + $this->assertSame('queues:default', $queue->testGetQueueRedisKey(null)); + $this->assertSame('queues:emails', $queue->testGetQueueRedisKey('emails')); } public function testGetRedisKeyWrapsWithHashTagsForPhpRedisCluster() @@ -217,8 +217,8 @@ public function testGetRedisKeyWrapsWithHashTagsForPhpRedisCluster() $connection->shouldReceive('isCluster')->andReturn(true); $redis->shouldReceive('connection')->andReturn($connection); - $this->assertSame('queues:{default}', $queue->testGetRedisKey(null)); - $this->assertSame('queues:{emails}', $queue->testGetRedisKey('emails')); + $this->assertSame('queues:{default}', $queue->testGetQueueRedisKey(null)); + $this->assertSame('queues:{emails}', $queue->testGetQueueRedisKey('emails')); } public function testGetRedisKeyWrapsWithHashTagsForPredisCluster() @@ -228,8 +228,8 @@ public function testGetRedisKeyWrapsWithHashTagsForPredisCluster() $connection->shouldReceive('isCluster')->andReturn(true); $redis->shouldReceive('connection')->andReturn($connection); - $this->assertSame('queues:{default}', $queue->testGetRedisKey(null)); - $this->assertSame('queues:{emails}', $queue->testGetRedisKey('emails')); + $this->assertSame('queues:{default}', $queue->testGetQueueRedisKey(null)); + $this->assertSame('queues:{emails}', $queue->testGetQueueRedisKey('emails')); } public function testGetRedisKeyDoesNotDoubleWrapExistingHashTags() @@ -239,8 +239,8 @@ public function testGetRedisKeyDoesNotDoubleWrapExistingHashTags() $connection->shouldReceive('isCluster')->andReturn(true); $redis->shouldReceive('connection')->andReturn($connection); - $this->assertSame('queues:{default}', $queue->testGetRedisKey(null)); - $this->assertSame('queues:{custom}', $queue->testGetRedisKey('{custom}')); + $this->assertSame('queues:{default}', $queue->testGetQueueRedisKey(null)); + $this->assertSame('queues:{custom}', $queue->testGetQueueRedisKey('{custom}')); } public function testGetRedisKeySkipsWrappingWhenQueueNameContainsBraces() @@ -251,7 +251,7 @@ public function testGetRedisKeySkipsWrappingWhenQueueNameContainsBraces() $redis->shouldReceive('connection')->andReturn($connection); // Queue name already contains hash tags — skip wrapping - $this->assertSame('queues:process-{batch}-results', $queue->testGetRedisKey('process-{batch}-results')); + $this->assertSame('queues:process-{batch}-results', $queue->testGetQueueRedisKey('process-{batch}-results')); } public function testGetRedisKeyWrapsEmptyHashTagOnCluster() @@ -262,7 +262,7 @@ public function testGetRedisKeyWrapsEmptyHashTagOnCluster() $redis->shouldReceive('connection')->andReturn($connection); // Empty braces '{}' are not a valid hash tag — should still get wrapped - $this->assertSame('queues:{my{}queue}', $queue->testGetRedisKey('my{}queue')); + $this->assertSame('queues:{my{}queue}', $queue->testGetQueueRedisKey('my{}queue')); } public function testGetRedisKeyWrapsUnmatchedOpeningBrace() @@ -273,7 +273,7 @@ public function testGetRedisKeyWrapsUnmatchedOpeningBrace() $redis->shouldReceive('connection')->andReturn($connection); // Unmatched '{' is not a valid hash tag — should still get wrapped - $this->assertSame('queues:{my{broken}', $queue->testGetRedisKey('my{broken')); + $this->assertSame('queues:{my{broken}', $queue->testGetQueueRedisKey('my{broken')); } public function testGetRedisKeyWrapsUnmatchedClosingBrace() @@ -284,7 +284,7 @@ public function testGetRedisKeyWrapsUnmatchedClosingBrace() $redis->shouldReceive('connection')->andReturn($connection); // Unmatched '}' is not a valid hash tag — should still get wrapped - $this->assertSame('queues:{broken}queue}', $queue->testGetRedisKey('broken}queue')); + $this->assertSame('queues:{broken}queue}', $queue->testGetQueueRedisKey('broken}queue')); } public function testGetRedisKeyWrapsEmptyFirstHashTagFollowedByValidPair() @@ -296,7 +296,7 @@ public function testGetRedisKeyWrapsEmptyFirstHashTagFollowedByValidPair() // Redis spec: the first '{}' is an empty hash tag, so the whole key is hashed // even though '{bar}' looks valid. Must be wrapped to ensure slot affinity. - $this->assertSame('queues:{foo{}{bar}}', $queue->testGetRedisKey('foo{}{bar}')); + $this->assertSame('queues:{foo{}{bar}}', $queue->testGetQueueRedisKey('foo{}{bar}')); } public function testPushUsesGetRedisKeyForLuaScript() @@ -421,9 +421,9 @@ public function testIsClusterConnectionCachesResult() class TestableRedisQueue extends RedisQueue { - public function testGetRedisKey($queue = null) + public function testGetQueueRedisKey($queue = null) { - return $this->getRedisKey($queue); + return $this->getQueueRedisKey($queue); } public function testIsClusterConnection() From d4cbb3fe951ec9d51fc39cc321befab50de795d9 Mon Sep 17 00:00:00 2001 From: yousef kadah Date: Fri, 10 Apr 2026 18:23:31 +0300 Subject: [PATCH 132/596] [13.x] Add enum support to CacheManager store and driver methods (#59637) Allows passing a BackedEnum (or any UnitEnum) to CacheManager::store(), driver(), memo(), forgetDriver(), purge(), and setDefaultDriver(), mirroring the enum support already added to QueueManager (#59389), LogManager (#59391), DatabaseManager, FilesystemManager, and RedisManager. --- src/Illuminate/Cache/CacheManager.php | 24 +++-- src/Illuminate/Contracts/Cache/Factory.php | 2 +- src/Illuminate/Support/Facades/Cache.php | 12 +-- tests/Cache/CacheManagerTest.php | 107 +++++++++++++++++++++ 4 files changed, 128 insertions(+), 17 deletions(-) diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index bd7a0c0f7ce1..d5967d076226 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -14,6 +14,8 @@ use RuntimeException; use Throwable; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Cache\Repository * @mixin \Illuminate\Contracts\Cache\LockProvider @@ -54,12 +56,12 @@ public function __construct($app) /** * Get a cache store instance by name, wrapped in a repository. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Cache\Repository */ public function store($name = null) { - $name = $name ?? $this->getDefaultDriver(); + $name = enum_value($name) ?? $this->getDefaultDriver(); return $this->stores[$name] ??= $this->resolve($name); } @@ -67,7 +69,7 @@ public function store($name = null) /** * Get a cache driver instance. * - * @param string|null $driver + * @param \UnitEnum|string|null $driver * @return \Illuminate\Contracts\Cache\Repository */ public function driver($driver = null) @@ -78,12 +80,12 @@ public function driver($driver = null) /** * Get a memoized cache driver instance. * - * @param string|null $driver + * @param \UnitEnum|string|null $driver * @return \Illuminate\Contracts\Cache\Repository */ public function memo($driver = null) { - $driver = $driver ?? $this->getDefaultDriver(); + $driver = enum_value($driver) ?? $this->getDefaultDriver(); $bindingKey = "cache.__memoized:{$driver}"; @@ -478,18 +480,18 @@ public function getDefaultDriver() /** * Set the default cache driver name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->app['config']['cache.default'] = $name; + $this->app['config']['cache.default'] = enum_value($name); } /** * Unset the given driver instances. * - * @param array|string|null $name + * @param array|\UnitEnum|string|null $name * @return $this */ public function forgetDriver($name = null) @@ -497,6 +499,8 @@ public function forgetDriver($name = null) $name ??= $this->getDefaultDriver(); foreach ((array) $name as $cacheName) { + $cacheName = enum_value($cacheName); + if (isset($this->stores[$cacheName])) { unset($this->stores[$cacheName]); } @@ -508,12 +512,12 @@ public function forgetDriver($name = null) /** * Disconnect the given driver and remove from local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return void */ public function purge($name = null) { - $name ??= $this->getDefaultDriver(); + $name = enum_value($name) ?? $this->getDefaultDriver(); unset($this->stores[$name]); } diff --git a/src/Illuminate/Contracts/Cache/Factory.php b/src/Illuminate/Contracts/Cache/Factory.php index 3924662d6cbc..327e1cfa6c3c 100644 --- a/src/Illuminate/Contracts/Cache/Factory.php +++ b/src/Illuminate/Contracts/Cache/Factory.php @@ -7,7 +7,7 @@ interface Factory /** * Get a cache store instance by name. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Cache\Repository */ public function store($name = null); diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 8144545ebb41..99b31fa3e5db 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -5,17 +5,17 @@ use Mockery; /** - * @method static \Illuminate\Contracts\Cache\Repository store(string|null $name = null) - * @method static \Illuminate\Contracts\Cache\Repository driver(string|null $driver = null) - * @method static \Illuminate\Contracts\Cache\Repository memo(string|null $driver = null) + * @method static \Illuminate\Contracts\Cache\Repository store(\UnitEnum|string|null $name = null) + * @method static \Illuminate\Contracts\Cache\Repository driver(\UnitEnum|string|null $driver = null) + * @method static \Illuminate\Contracts\Cache\Repository memo(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Contracts\Cache\Repository resolve(string $name) * @method static \Illuminate\Cache\Repository build(array $config) * @method static \Illuminate\Cache\Repository repository(\Illuminate\Contracts\Cache\Store $store, array $config = []) * @method static void refreshEventDispatcher() * @method static string getDefaultDriver() - * @method static void setDefaultDriver(string $name) - * @method static \Illuminate\Cache\CacheManager forgetDriver(array|string|null $name = null) - * @method static void purge(string|null $name = null) + * @method static void setDefaultDriver(\UnitEnum|string $name) + * @method static \Illuminate\Cache\CacheManager forgetDriver(array|\UnitEnum|string|null $name = null) + * @method static void purge(\UnitEnum|string|null $name = null) * @method static \Illuminate\Cache\CacheManager extend(string $driver, \Closure $callback) * @method static \Illuminate\Cache\CacheManager setApplication(\Illuminate\Contracts\Foundation\Application $app) * @method static bool has(\UnitEnum|array|string $key) diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index b48b8903c85c..5efceadcf0bc 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -332,6 +332,108 @@ public function testMakesRepositoryWithoutDispatcherWhenEventsDisabled() $this->assertNull($repoWithoutEvents->getEventDispatcher()); } + public function testEnumStoreCanBeResolved() + { + $userConfig = [ + 'cache' => [ + 'stores' => [ + 'array' => [ + 'driver' => 'array', + ], + ], + ], + ]; + + $app = $this->getApp($userConfig); + $cacheManager = new CacheManager($app); + + $store = $cacheManager->store(CacheStoreName::ArrayStore); + + $this->assertInstanceOf(ArrayStore::class, $store->getStore()); + $this->assertSame($store, $cacheManager->store(CacheStoreName::ArrayStore)); + } + + public function testEnumDriverCanBeResolved() + { + $userConfig = [ + 'cache' => [ + 'stores' => [ + 'array' => [ + 'driver' => 'array', + ], + ], + ], + ]; + + $app = $this->getApp($userConfig); + $cacheManager = new CacheManager($app); + + $store = $cacheManager->driver(CacheStoreName::ArrayStore); + + $this->assertInstanceOf(ArrayStore::class, $store->getStore()); + } + + public function testForgetDriverAcceptsEnum() + { + $userConfig = [ + 'cache' => [ + 'stores' => [ + 'array' => [ + 'driver' => 'array', + ], + ], + ], + ]; + + $app = $this->getApp($userConfig); + $cacheManager = new CacheManager($app); + + $repo1 = $cacheManager->store(CacheStoreName::ArrayStore); + $cacheManager->forgetDriver(CacheStoreName::ArrayStore); + $repo2 = $cacheManager->store(CacheStoreName::ArrayStore); + + $this->assertNotSame($repo1, $repo2); + } + + public function testPurgeAcceptsEnum() + { + $userConfig = [ + 'cache' => [ + 'stores' => [ + 'array' => [ + 'driver' => 'array', + ], + ], + ], + ]; + + $app = $this->getApp($userConfig); + $cacheManager = new CacheManager($app); + + $repo1 = $cacheManager->store(CacheStoreName::ArrayStore); + $cacheManager->purge(CacheStoreName::ArrayStore); + $repo2 = $cacheManager->store(CacheStoreName::ArrayStore); + + $this->assertNotSame($repo1, $repo2); + } + + public function testSetDefaultDriverAcceptsEnum() + { + $userConfig = [ + 'cache' => [ + 'default' => 'old', + 'stores' => [], + ], + ]; + + $app = $this->getApp($userConfig); + $cacheManager = new CacheManager($app); + + $cacheManager->setDefaultDriver(CacheStoreName::ArrayStore); + + $this->assertSame('array', $app->get('config')->get('cache.default')); + } + protected function getApp(array $userConfig) { $app = new Container; @@ -340,3 +442,8 @@ protected function getApp(array $userConfig) return $app; } } + +enum CacheStoreName: string +{ + case ArrayStore = 'array'; +} From 5f484cc42915667a442d57b4ba6d0c8a50b115e9 Mon Sep 17 00:00:00 2001 From: Timmy Lindholm <74464421+timmylindh@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:06:57 +0200 Subject: [PATCH 133/596] fix: fix (#59633) --- .../Foundation/Configuration/Middleware.php | 4 ++ .../Configuration/MiddlewareTest.php | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index 312c746985cb..c4d90bb98850 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -538,6 +538,10 @@ public function getMiddlewareGroups() */ public function redirectGuestsTo(callable|string|null $redirect) { + if (is_null($redirect)) { + $redirect = fn () => null; + } + return $this->redirectTo(guests: $redirect); } diff --git a/tests/Foundation/Configuration/MiddlewareTest.php b/tests/Foundation/Configuration/MiddlewareTest.php index eeea7f68b349..240c52dc1f54 100644 --- a/tests/Foundation/Configuration/MiddlewareTest.php +++ b/tests/Foundation/Configuration/MiddlewareTest.php @@ -2,6 +2,9 @@ namespace Illuminate\Tests\Foundation\Configuration; +use Illuminate\Auth\AuthenticationException; +use Illuminate\Auth\Middleware\Authenticate; +use Illuminate\Auth\Middleware\RedirectIfAuthenticated; use Illuminate\Container\Container; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Foundation\Application; @@ -15,6 +18,7 @@ use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; use Illuminate\Http\Request; +use Illuminate\Session\Middleware\AuthenticateSession; use Mockery as m; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -32,6 +36,10 @@ protected function tearDown(): void TrimStrings::flushState(); TrustProxies::flushState(); + foreach ([Authenticate::class, AuthenticateSession::class, AuthenticationException::class, RedirectIfAuthenticated::class] as $class) { + (new ReflectionClass($class))->getProperty('redirectToCallback')->setValue(null, null); + } + parent::tearDown(); } @@ -303,4 +311,39 @@ public function testPreventRequestForgery() $this->assertTrue($reflection->getStaticPropertyValue('originOnly')); $this->assertTrue($reflection->getStaticPropertyValue('allowSameSite')); } + + public function testRedirectUsersToDoesNotOverwriteRedirectGuestsTo() + { + $middleware = new Middleware; + + $middleware->redirectGuestsTo(fn () => '/login'); + $middleware->redirectUsersTo('/dashboard'); + + $authenticateCallback = (new ReflectionClass(Authenticate::class)) + ->getProperty('redirectToCallback')->getValue(); + $sessionCallback = (new ReflectionClass(AuthenticateSession::class)) + ->getProperty('redirectToCallback')->getValue(); + $exceptionCallback = (new ReflectionClass(AuthenticationException::class)) + ->getProperty('redirectToCallback')->getValue(); + $usersCallback = (new ReflectionClass(RedirectIfAuthenticated::class)) + ->getProperty('redirectToCallback')->getValue(); + + $this->assertSame('/login', $authenticateCallback(null)); + $this->assertSame('/login', $sessionCallback(null)); + $this->assertSame('/login', $exceptionCallback(null)); + $this->assertSame('/dashboard', $usersCallback(null)); + } + + public function testRedirectGuestsToNullRegistersNullCallback() + { + $middleware = new Middleware; + + $middleware->redirectGuestsTo(null); + + $callback = (new ReflectionClass(Authenticate::class)) + ->getProperty('redirectToCallback')->getValue(); + + $this->assertNotNull($callback); + $this->assertNull($callback(null)); + } } From 5db7c4a5cea1adcad984853ee9cf9507fc7acc2c Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 10 Apr 2026 18:09:32 +0100 Subject: [PATCH 134/596] [13.x] Add ability to detect unserializable values returned from cache (#59630) * 13.x add handleUnserializableClassUsing * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Cache/CacheManager.php | 11 +++++++ src/Illuminate/Cache/Repository.php | 44 +++++++++++++++++++++++++++ tests/Cache/CacheRepositoryTest.php | 42 +++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index d5967d076226..011d33e89ac4 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -558,6 +558,17 @@ public function setApplication($app) return $this; } + /** + * Register a callback to be invoked when an unserializable class is encountered. + * + * @param callable|null $callback + * @return void + */ + public function handleUnserializableClassUsing(?callable $callback): void + { + Repository::handleUnserializableClassUsing($callback); + } + /** * Dynamically call the default driver instance. * diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 558a6cd92de5..229caa6517fa 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -75,6 +75,13 @@ class Repository implements ArrayAccess, CacheContract */ protected $config = []; + /** + * The callback to invoke when an unserializable class is encountered. + * + * @var callable|null + */ + protected static $unserializableClassHandler; + /** * Create a new cache repository instance. */ @@ -131,6 +138,8 @@ public function get($key, $default = null): mixed $value = value($default); } else { + $value = $this->handleIncompleteClass($key, $value); + $this->event(new CacheHit($this->getName(), $key, $value)); } @@ -195,6 +204,8 @@ protected function handleManyResult($keys, $key, $value) // If we found a valid value we will fire the "hit" event and return the value // back from this function. The "hit" event gives developers an opportunity // to listen for every possible cache "hit" throughout this applications. + $value = $this->handleIncompleteClass($key, $value); + $this->event(new CacheHit($this->getName(), $key, $value)); return $value; @@ -822,6 +833,28 @@ protected function itemKey($key) return $key; } + /** + * Handle a cache value that contains an incomplete class. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function handleIncompleteClass(string $key, mixed $value): mixed + { + if (! ($value instanceof \__PHP_Incomplete_Class)) { + return $value; + } + + $class = ((array) $value)['__PHP_Incomplete_Class_Name'] ?? null; + + if (isset(static::$unserializableClassHandler)) { + (static::$unserializableClassHandler)($key, $class); + } + + return $value; + } + /** * Calculate the number of seconds for the given TTL. * @@ -946,6 +979,17 @@ public function setEventDispatcher(Dispatcher $events) $this->events = $events; } + /** + * Register a callback to be invoked when an unserializable class is encountered. + * + * @param callable|null $callback + * @return void + */ + public static function handleUnserializableClassUsing(?callable $callback): void + { + static::$unserializableClassHandler = $callback; + } + /** * Determine if a cached value exists. * diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 00a5d8f7adce..716a42792459 100755 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -26,6 +26,7 @@ use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use stdClass; class CacheRepositoryTest extends TestCase { @@ -39,6 +40,7 @@ protected function setUp(): void protected function tearDown(): void { Carbon::setTestNow(null); + Repository::handleUnserializableClassUsing(null); parent::tearDown(); } @@ -581,6 +583,46 @@ public function testTaggedCacheWorksWithEnumKey() $this->assertSame(5, $cache->decrement(TestCacheKey::FOO)); } + public function testGetReturnsIncompleteClassWhenNoHandlerRegistered() + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('foo')->andReturn(unserialize(serialize(new stdClass), ['allowed_classes' => false])); + + $this->assertInstanceOf(\__PHP_Incomplete_Class::class, $repo->get('foo')); + } + + public function testGetCallsHandlerWithKeyAndClassForIncompleteClass() + { + $class = null; + $key = null; + + Repository::handleUnserializableClassUsing(function ($k, $c) use (&$class, &$key) { + $key = $k; + $class = $c; + }); + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('foo')->andReturn(unserialize(serialize(new stdClass), ['allowed_classes' => false])); + $repo->get('foo'); + + $this->assertSame('foo', $key); + $this->assertSame('stdClass', $class); + } + + public function testManyCallsHandlerForEachIncompleteClass() + { + $handled = []; + Repository::handleUnserializableClassUsing(function ($key, $class) use (&$handled) { + $handled[] = $key; + }); + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('many')->once()->with(['foo', 'bar'])->andReturn(['foo' => unserialize(serialize(new stdClass), ['allowed_classes' => false]), 'bar' => 'baz']); + $repo->many(['foo', 'bar']); + + $this->assertSame(['foo'], $handled); + } + protected function getRepository() { $dispatcher = new Dispatcher(m::mock(Container::class)); From 839f0fbf3e1cf1913326b0272e06e3a228824368 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:10:10 +0000 Subject: [PATCH 135/596] Update facade docblocks --- src/Illuminate/Support/Facades/Cache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 99b31fa3e5db..4fc659602694 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -18,6 +18,7 @@ * @method static void purge(\UnitEnum|string|null $name = null) * @method static \Illuminate\Cache\CacheManager extend(string $driver, \Closure $callback) * @method static \Illuminate\Cache\CacheManager setApplication(\Illuminate\Contracts\Foundation\Application $app) + * @method static void handleUnserializableClassUsing(callable|null $callback) * @method static bool has(\UnitEnum|array|string $key) * @method static bool missing(\UnitEnum|string $key) * @method static mixed get(\UnitEnum|array|string $key, mixed $default = null) From 635129b24d0d6c5664e4b77f41b1297a2f74c91a Mon Sep 17 00:00:00 2001 From: Sebastian Cabarcas Berrio <42840369+scabarcas17@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:51:29 -0500 Subject: [PATCH 136/596] [13.x] Fix loose comparison false positive in NotPwnedVerifier with magic hash passwords (#59644) --- .../Validation/NotPwnedVerifier.php | 2 +- .../ValidationNotPwnedVerifierTest.php | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index a6dbaa3c7de0..20088f7e6473 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -55,7 +55,7 @@ public function verify($data) ->contains(function ($line) use ($hash, $hashPrefix, $threshold) { [$hashSuffix, $count] = explode(':', $line); - return $hashPrefix.$hashSuffix == $hash && $count > $threshold; + return $hashPrefix.$hashSuffix === $hash && $count > $threshold; }); } diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php index a4ae15d64d82..d330f8e34cec 100644 --- a/tests/Validation/ValidationNotPwnedVerifierTest.php +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -105,6 +105,53 @@ public function testApiGoesDown() ])); } + public function testMagicHashDoesNotCauseFalsePositive() + { + // "aaroZmOk" produces a SHA-1 hash that is all digits prefixed with "0E", + // which PHP treats as scientific notation (zero) during loose comparison, + // causing any other all-digit "0E" hash to falsely match. + $password = 'aaroZmOk'; + $hash = strtoupper(sha1($password)); + $hashPrefix = substr($hash, 0, 5); + + $differentSuffix = '00000000000000000000000000000000000'; + + $httpFactory = m::mock(HttpFactory::class); + $response = m::mock(Response::class); + + $httpFactory + ->shouldReceive('withHeaders') + ->once() + ->with(['Add-Padding' => true]) + ->andReturn($httpFactory); + + $httpFactory + ->shouldReceive('timeout') + ->once() + ->with(30) + ->andReturn($httpFactory); + + $httpFactory->shouldReceive('get') + ->once() + ->with('https://api.pwnedpasswords.com/range/'.$hashPrefix) + ->andReturn($response); + + $response->shouldReceive('successful') + ->once() + ->andReturn(true); + + $response->shouldReceive('body') + ->once() + ->andReturn($differentSuffix.':5'); + + $verifier = new NotPwnedVerifier($httpFactory); + + $this->assertTrue($verifier->verify([ + 'value' => $password, + 'threshold' => 0, + ])); + } + public function testDnsDown() { $container = Container::getInstance(); From 929818739b056ab60cd5d4eb511c378c798a4cf0 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:42:02 -0400 Subject: [PATCH 137/596] [13.x] Refactor `Skip` middleware (#59651) * Refactor Skip middleware to use static instance * Change return type to static in Skip middleware * Fix parameter documentation in Skip class constructor --- src/Illuminate/Queue/Middleware/Skip.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Queue/Middleware/Skip.php b/src/Illuminate/Queue/Middleware/Skip.php index 37fc91d5949f..6cb79a0a35b4 100644 --- a/src/Illuminate/Queue/Middleware/Skip.php +++ b/src/Illuminate/Queue/Middleware/Skip.php @@ -6,6 +6,9 @@ class Skip { + /** + * @param bool $param Whether the job should be skipped. + */ public function __construct(protected bool $skip = false) { } @@ -13,21 +16,21 @@ public function __construct(protected bool $skip = false) /** * Apply the middleware if the given condition is truthy. * - * @param bool|Closure(): bool $condition + * @param bool|(\Closure(): bool) $condition */ - public static function when(Closure|bool $condition): self + public static function when(Closure|bool $condition): static { - return new self(value($condition)); + return new static(value($condition)); } /** * Apply the middleware unless the given condition is truthy. * - * @param bool|Closure(): bool $condition + * @param bool|(\Closure(): bool) $condition */ - public static function unless(Closure|bool $condition): self + public static function unless(Closure|bool $condition): static { - return new self(! value($condition)); + return new static(! value($condition)); } /** From a7e2ef137fd94bb5d284f31401cb110f88a0faac Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 12 Apr 2026 18:43:18 +0100 Subject: [PATCH 138/596] [13.x] Resolve stan errors on MySqlSchemaState (#59652) * Update MySqlSchemaState.php * wip * Revert "wip" This reverts commit 31c17359c9064fe3eb0fbc39ed9cbd724a89d228. --- src/Illuminate/Database/Schema/MySqlSchemaState.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index dbfa3ccaef78..74096054ceb8 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -117,22 +117,18 @@ protected function connectionString(array $versionInfo) ? ' --socket="${:LARAVEL_LOAD_SOCKET}"' : ' --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}"'; - /** @phpstan-ignore class.notFound */ if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA])) { $value .= ' --ssl-ca="${:LARAVEL_LOAD_SSL_CA}"'; } - /** @phpstan-ignore class.notFound */ if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT])) { $value .= ' --ssl-cert="${:LARAVEL_LOAD_SSL_CERT}"'; } - /** @phpstan-ignore class.notFound */ if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY])) { $value .= ' --ssl-key="${:LARAVEL_LOAD_SSL_KEY}"'; } - /** @phpstan-ignore class.notFound */ $verifyCertOption = PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT : \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT; if (isset($config['options'][$verifyCertOption]) && $config['options'][$verifyCertOption] === false) { @@ -163,9 +159,9 @@ protected function baseVariables(array $config) 'LARAVEL_LOAD_USER' => $config['username'], 'LARAVEL_LOAD_PASSWORD' => $config['password'] ?? '', 'LARAVEL_LOAD_DATABASE' => $config['database'], - 'LARAVEL_LOAD_SSL_CA' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA] ?? '', // @phpstan-ignore class.notFound - 'LARAVEL_LOAD_SSL_CERT' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT] ?? '', // @phpstan-ignore class.notFound - 'LARAVEL_LOAD_SSL_KEY' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY] ?? '', // @phpstan-ignore class.notFound + 'LARAVEL_LOAD_SSL_CA' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA] ?? '', + 'LARAVEL_LOAD_SSL_CERT' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT] ?? '', + 'LARAVEL_LOAD_SSL_KEY' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY] ?? '', ]; } From 10539d466ced6e032c5aeccede9e1a6a38d09377 Mon Sep 17 00:00:00 2001 From: yousef kadah Date: Sun, 12 Apr 2026 20:44:25 +0300 Subject: [PATCH 139/596] [13.x] Allow closure values in updateOrCreate and firstOrNew (#59647) Extends the lazy-evaluation pattern from #58639 to the two remaining *OrCreate / *OrNew methods that were not updated: - Eloquent\Builder::firstOrNew() - Eloquent\Builder::updateOrCreate() - Relations\HasOneOrMany::firstOrNew() - Relations\HasOneOrMany::updateOrCreate() - Relations\BelongsToMany::firstOrNew() - Relations\BelongsToMany::updateOrCreate() Today, firstOrCreate and createOrFirst already accept Closure|array $values. updateOrCreate calls firstOrCreate internally, so it appears to be a passthrough, but it then also calls $instance->fill($values) on the update path - if $values is a Closure that's a runtime error, because the type signature lies about the runtime contract. The fix resolves the closure exactly once per call: - Path A (record does not exist): firstOrCreate -> createOrFirst resolves $values via value() once. The tap callback sees wasRecentlyCreated=true and skips fill(). - Path B (record exists): firstOrCreate short-circuits before resolving. The tap callback then resolves $values via value() exactly once for fill(). firstOrNew gets the same treatment for consistency with the rest of the *OrCreate family. Adds 13 new tests covering the create path, the update path, the "closure must run exactly once when creating", "closure must run exactly once when updating", and "closure must NOT run at all when firstOrNew finds an existing record" guarantees across Builder, HasMany, and BelongsToMany. --- src/Illuminate/Database/Eloquent/Builder.php | 12 +- .../Eloquent/Relations/BelongsToMany.php | 12 +- .../Eloquent/Relations/HasOneOrMany.php | 12 +- ...EloquentBelongsToManyCreateOrFirstTest.php | 110 +++++++++++ ...tabaseEloquentBuilderCreateOrFirstTest.php | 172 ++++++++++++++++++ ...tabaseEloquentHasManyCreateOrFirstTest.php | 86 +++++++++ 6 files changed, 386 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 7a014287e757..7a071ff5ac19 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -687,16 +687,16 @@ public function findOr($id, $columns = ['*'], ?Closure $callback = null) * Get the first record matching the attributes or instantiate it. * * @param array $attributes - * @param array $values + * @param (\Closure(): array)|array $values * @return TModel */ - public function firstOrNew(array $attributes = [], array $values = []) + public function firstOrNew(array $attributes = [], Closure|array $values = []) { if (! is_null($instance = $this->where($attributes)->first())) { return $instance; } - return $this->newModelInstance(array_merge($attributes, $values)); + return $this->newModelInstance(array_merge($attributes, value($values))); } /** @@ -737,14 +737,14 @@ public function createOrFirst(array $attributes = [], Closure|array $values = [] * Create or update a record matching the attributes, and fill it with values. * * @param array $attributes - * @param array $values + * @param (\Closure(): array)|array $values * @return TModel */ - public function updateOrCreate(array $attributes, array $values = []) + public function updateOrCreate(array $attributes, Closure|array $values = []) { return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { if (! $instance->wasRecentlyCreated) { - $instance->fill($values)->save(); + $instance->fill(value($values))->save(); } }); } diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index b58536a39c29..3b2dfe91cb5f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -631,13 +631,13 @@ public function findOrNew($id, $columns = ['*']) * Get the first related model record matching the attributes or instantiate it. * * @param array $attributes - * @param array $values + * @param (\Closure(): array)|array $values * @return TRelatedModel&object{pivot: TPivotModel} */ - public function firstOrNew(array $attributes = [], array $values = []) + public function firstOrNew(array $attributes = [], Closure|array $values = []) { if (is_null($instance = $this->related->where($attributes)->first())) { - $instance = $this->related->newInstance(array_merge($attributes, $values)); + $instance = $this->related->newInstance(array_merge($attributes, value($values))); } return $instance; @@ -701,16 +701,16 @@ public function createOrFirst(array $attributes = [], Closure|array $values = [] * Create or update a related record matching the attributes, and fill it with values. * * @param array $attributes - * @param array $values + * @param (\Closure(): array)|array $values * @param array $joining * @param bool $touch * @return TRelatedModel&object{pivot: TPivotModel} */ - public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) + public function updateOrCreate(array $attributes, Closure|array $values = [], array $joining = [], $touch = true) { return tap($this->firstOrCreate($attributes, $values, $joining, $touch), function ($instance) use ($values) { if (! $instance->wasRecentlyCreated) { - $instance->fill($values); + $instance->fill(value($values)); $instance->save(['touch' => false]); } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index 7261159749ae..2cf725ceba0e 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -240,13 +240,13 @@ public function findOrNew($id, $columns = ['*']) * Get the first related model record matching the attributes or instantiate it. * * @param array $attributes - * @param array $values + * @param (\Closure(): array)|array $values * @return TRelatedModel */ - public function firstOrNew(array $attributes = [], array $values = []) + public function firstOrNew(array $attributes = [], Closure|array $values = []) { if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->related->newInstance(array_merge($attributes, $values)); + $instance = $this->related->newInstance(array_merge($attributes, value($values))); $this->setForeignAttributesForCreate($instance); } @@ -292,14 +292,14 @@ public function createOrFirst(array $attributes = [], Closure|array $values = [] * Create or update a related record matching the attributes, and fill it with values. * * @param array $attributes - * @param array $values + * @param (\Closure(): array)|array $values * @return TRelatedModel */ - public function updateOrCreate(array $attributes, array $values = []) + public function updateOrCreate(array $attributes, Closure|array $values = []) { return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { if (! $instance->wasRecentlyCreated) { - $instance->fill($values)->save(); + $instance->fill(value($values))->save(); } }); } diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php index 47b69abc4b08..502827072c34 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -456,6 +456,116 @@ protected function newBelongsToMany(Builder $query, Model $parent, $table, $fore ], $result->toArray()); } + public function testUpdateOrCreateMethodAcceptsClosureValuesAndCreates(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = m::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = true; + $instance->syncOriginal(); + $relation + ->expects('firstOrCreate') + ->withArgs(function ($attributes, $values, $joining, $touch) { + return $attributes === ['attr' => 'foo'] + && $values instanceof Closure + && $joining === [] + && $touch === true; + }) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + + $callCount = 0; + $result = $source->related()->updateOrCreate(['attr' => 'foo'], function () use (&$callCount) { + $callCount++; + + return ['val' => 'baz']; + }); + + // Closure is forwarded to firstOrCreate which would resolve it on the create path. + // Because we mocked firstOrCreate above, the closure was never invoked here. + $this->assertSame(0, $callCount); + $this->assertSame('bar', $result->val); + } + + public function testUpdateOrCreateMethodAcceptsClosureValuesAndUpdates(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = m::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = false; + $instance->syncOriginal(); + $relation + ->expects('firstOrCreate') + ->withArgs(function ($attributes, $values, $joining, $touch) { + return $attributes === ['attr' => 'foo'] + && $values instanceof Closure + && $joining === [] + && $touch === true; + }) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('update') + ->with( + 'update "related_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + ) + ->andReturn(1); + + $callCount = 0; + $result = $source->related()->updateOrCreate(['attr' => 'foo'], function () use (&$callCount) { + $callCount++; + + return ['val' => 'baz']; + }); + + // On the update path firstOrCreate was mocked away, so the closure + // is only resolved once for the fill() call. + $this->assertSame(1, $callCount); + $this->assertSame('baz', $result->val); + } + public static function createOrFirstValues(): array { return [ diff --git a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php index 7f88d05387cf..b061a5f3bca9 100755 --- a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php @@ -473,6 +473,178 @@ public function testIncrementOrCreateMethodRetrievesRecordCreatedJustNow(): void ], $result->toArray()); } + #[DataProvider('createOrFirstValues')] + public function testUpdateOrCreateMethodAcceptsClosureValuesAndCreates(Closure|array $values): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true, []) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], $values); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodAcceptsClosureValuesAndUpdates(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true, []) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 123], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], fn () => ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals('baz', $result->val); + } + + public function testUpdateOrCreateInvokesClosureExactlyOnceWhenCreating(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true, []) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $callCount = 0; + $model->newQuery()->updateOrCreate(['attr' => 'foo'], function () use (&$callCount) { + $callCount++; + + return ['val' => 'bar']; + }); + + $this->assertSame(1, $callCount); + } + + public function testUpdateOrCreateInvokesClosureExactlyOnceWhenUpdating(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true, []) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 123], + ) + ->andReturn(1); + + $callCount = 0; + $model->newQuery()->updateOrCreate(['attr' => 'foo'], function () use (&$callCount) { + $callCount++; + + return ['val' => 'baz']; + }); + + $this->assertSame(1, $callCount); + } + + #[DataProvider('createOrFirstValues')] + public function testFirstOrNewMethodAcceptsClosureValuesAndInstantiates(Closure|array $values): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true, []) + ->andReturn([]); + + $result = $model->newQuery()->firstOrNew(['attr' => 'foo'], $values); + $this->assertFalse($result->exists); + $this->assertSame('foo', $result->attr); + $this->assertSame('bar', $result->val); + } + + public function testFirstOrNewDoesNotInvokeClosureWhenRecordExists(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true, []) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $callCount = 0; + $result = $model->newQuery()->firstOrNew(['attr' => 'foo'], function () use (&$callCount) { + $callCount++; + + return ['val' => 'should-not-be-called']; + }); + + $this->assertSame(0, $callCount); + $this->assertTrue($result->exists); + $this->assertSame('bar', $result->val); + } + public static function createOrFirstValues(): array { return [ diff --git a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php index ef76fb665051..75f45885796b 100755 --- a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php @@ -320,6 +320,92 @@ public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void ], $result->toArray()); } + #[DataProvider('createOrFirstValues')] + public function testUpdateOrCreateMethodAcceptsClosureValuesAndCreates(Closure|array $values): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true, []) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], $values); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertSame('bar', $result->val); + } + + public function testUpdateOrCreateMethodAcceptsClosureValuesAndUpdates(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true, []) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + )->andReturn(1); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], fn () => ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertSame('baz', $result->val); + } + + public function testFirstOrNewDoesNotInvokeClosureWhenRecordExists(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true, []) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $callCount = 0; + $result = $model->children()->firstOrNew(['attr' => 'foo'], function () use (&$callCount) { + $callCount++; + + return ['val' => 'should-not-be-called']; + }); + + $this->assertSame(0, $callCount); + $this->assertTrue($result->exists); + $this->assertSame('bar', $result->val); + } + public static function createOrFirstValues(): array { return [ From 69a4acb877cca1703e5e5dccf08baabd883f03d5 Mon Sep 17 00:00:00 2001 From: yousef kadah Date: Sun, 12 Apr 2026 20:45:35 +0300 Subject: [PATCH 140/596] [13.x] Add enum support to MailManager mailer and driver methods (#59645) Allows passing a BackedEnum (or any UnitEnum) to MailManager::mailer(), driver(), and purge(), mirroring the enum support already added to CacheManager (#59637), QueueManager (#59389), LogManager (#59391), DatabaseManager, FilesystemManager, RedisManager, and BroadcastManager. --- src/Illuminate/Contracts/Mail/Factory.php | 2 +- src/Illuminate/Mail/MailManager.php | 12 ++++--- src/Illuminate/Support/Facades/Mail.php | 6 ++-- tests/Mail/MailManagerTest.php | 44 +++++++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Contracts/Mail/Factory.php b/src/Illuminate/Contracts/Mail/Factory.php index fe45a2fd9cd8..c84821795b3f 100644 --- a/src/Illuminate/Contracts/Mail/Factory.php +++ b/src/Illuminate/Contracts/Mail/Factory.php @@ -7,7 +7,7 @@ interface Factory /** * Get a mailer instance by name. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null); diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index 78c02a9f16ce..14c5cd6d3b6b 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -29,6 +29,8 @@ use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Mail\Mailer */ @@ -68,12 +70,12 @@ public function __construct($app) /** * Get a mailer instance by name. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->mailers[$name] = $this->get($name); } @@ -81,7 +83,7 @@ public function mailer($name = null) /** * Get a mailer driver instance. * - * @param string|null $driver + * @param \UnitEnum|string|null $driver * @return \Illuminate\Mail\Mailer */ public function driver($driver = null) @@ -552,12 +554,12 @@ public function setDefaultDriver(string $name) /** * Disconnect the given mailer and remove from local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return void */ public function purge($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); unset($this->mailers[$name]); } diff --git a/src/Illuminate/Support/Facades/Mail.php b/src/Illuminate/Support/Facades/Mail.php index 4d9a838226be..a3ccceb00768 100755 --- a/src/Illuminate/Support/Facades/Mail.php +++ b/src/Illuminate/Support/Facades/Mail.php @@ -5,13 +5,13 @@ use Illuminate\Support\Testing\Fakes\MailFake; /** - * @method static \Illuminate\Contracts\Mail\Mailer mailer(string|null $name = null) - * @method static \Illuminate\Mail\Mailer driver(string|null $driver = null) + * @method static \Illuminate\Contracts\Mail\Mailer mailer(\UnitEnum|string|null $name = null) + * @method static \Illuminate\Mail\Mailer driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Mail\Mailer build(array $config) * @method static \Symfony\Component\Mailer\Transport\TransportInterface createSymfonyTransport(array $config) * @method static string getDefaultDriver() * @method static void setDefaultDriver(string $name) - * @method static void purge(string|null $name = null) + * @method static void purge(\UnitEnum|string|null $name = null) * @method static \Illuminate\Mail\MailManager extend(string $driver, \Closure $callback) * @method static \Illuminate\Contracts\Foundation\Application getApplication() * @method static \Illuminate\Mail\MailManager setApplication(\Illuminate\Contracts\Foundation\Application $app) diff --git a/tests/Mail/MailManagerTest.php b/tests/Mail/MailManagerTest.php index 9363cfe5077c..1b62064c7f19 100644 --- a/tests/Mail/MailManagerTest.php +++ b/tests/Mail/MailManagerTest.php @@ -125,6 +125,45 @@ public function testBuild(): void $this->assertSame(5876, $transport->getStream()->getPort()); } + public function testMailManagerCanResolveBackedEnumMailer(): void + { + $this->app['config']->set('mail.mailers.array', [ + 'transport' => 'array', + ]); + + $mailer1 = $this->app['mail.manager']->mailer(MailerName::ArrayMailer); + $mailer2 = $this->app['mail.manager']->mailer('array'); + + $this->assertSame($mailer1, $mailer2); + } + + public function testMailManagerCanResolveBackedEnumDriver(): void + { + $this->app['config']->set('mail.mailers.array', [ + 'transport' => 'array', + ]); + + $mailer1 = $this->app['mail.manager']->driver(MailerName::ArrayMailer); + $mailer2 = $this->app['mail.manager']->driver('array'); + + $this->assertSame($mailer1, $mailer2); + } + + public function testPurgeAcceptsBackedEnum(): void + { + $this->app['config']->set('mail.mailers.array', [ + 'transport' => 'array', + ]); + + $manager = $this->app['mail.manager']; + + $mailer1 = $manager->mailer(MailerName::ArrayMailer); + $manager->purge(MailerName::ArrayMailer); + $mailer2 = $manager->mailer(MailerName::ArrayMailer); + + $this->assertNotSame($mailer1, $mailer2); + } + public static function emptyTransportConfigDataProvider() { return [ @@ -132,3 +171,8 @@ public static function emptyTransportConfigDataProvider() ]; } } + +enum MailerName: string +{ + case ArrayMailer = 'array'; +} From 74f16e15c7a884f7e37d97cc4f9ac415c71a6366 Mon Sep 17 00:00:00 2001 From: yousef kadah Date: Sun, 12 Apr 2026 20:46:48 +0300 Subject: [PATCH 141/596] [13.x] Add enum support to AuthManager guard and shouldUse methods (#59646) Allows passing a BackedEnum (or any UnitEnum) to AuthManager::guard(), shouldUse(), and setDefaultDriver(), mirroring the enum support already added to CacheManager (#59637), QueueManager (#59389), LogManager (#59391), DatabaseManager, FilesystemManager, RedisManager, and BroadcastManager. The matching Illuminate\Contracts\Auth\Factory interface and Auth facade docblocks are updated for consistency. --- src/Illuminate/Auth/AuthManager.php | 14 +++++---- src/Illuminate/Contracts/Auth/Factory.php | 4 +-- src/Illuminate/Support/Facades/Auth.php | 6 ++-- tests/Auth/AuthenticateMiddlewareTest.php | 35 +++++++++++++++++++++++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index e3fadf3354e7..dad7faa01fac 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -8,6 +8,8 @@ use RuntimeException; use Throwable; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Contracts\Auth\Guard * @mixin \Illuminate\Contracts\Auth\StatefulGuard @@ -61,12 +63,12 @@ public function __construct($app) /** * Attempt to get the guard from the local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard */ public function guard($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->guards[$name] ??= $this->resolve($name); } @@ -197,12 +199,12 @@ public function getDefaultDriver() /** * Set the default guard driver the factory should serve. * - * @param string $name + * @param \UnitEnum|string|null $name * @return void */ public function shouldUse($name) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); $this->setDefaultDriver($name); @@ -212,12 +214,12 @@ public function shouldUse($name) /** * Set the default authentication driver name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->app['config']['auth.defaults.guard'] = $name; + $this->app['config']['auth.defaults.guard'] = enum_value($name); } /** diff --git a/src/Illuminate/Contracts/Auth/Factory.php b/src/Illuminate/Contracts/Auth/Factory.php index d76ee76489a4..d5b04b5e639c 100644 --- a/src/Illuminate/Contracts/Auth/Factory.php +++ b/src/Illuminate/Contracts/Auth/Factory.php @@ -7,7 +7,7 @@ interface Factory /** * Get a guard instance by name. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard */ public function guard($name = null); @@ -15,7 +15,7 @@ public function guard($name = null); /** * Set the default guard the factory should serve. * - * @param string $name + * @param \UnitEnum|string|null $name * @return void */ public function shouldUse($name); diff --git a/src/Illuminate/Support/Facades/Auth.php b/src/Illuminate/Support/Facades/Auth.php index 879e28b7771b..77389210e77c 100755 --- a/src/Illuminate/Support/Facades/Auth.php +++ b/src/Illuminate/Support/Facades/Auth.php @@ -6,12 +6,12 @@ use RuntimeException; /** - * @method static \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard guard(string|null $name = null) + * @method static \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard guard(\UnitEnum|string|null $name = null) * @method static \Illuminate\Auth\SessionGuard createSessionDriver(string $name, array $config) * @method static \Illuminate\Auth\TokenGuard createTokenDriver(string $name, array $config) * @method static string getDefaultDriver() - * @method static void shouldUse(string $name) - * @method static void setDefaultDriver(string $name) + * @method static void shouldUse(\UnitEnum|string|null $name) + * @method static void setDefaultDriver(\UnitEnum|string $name) * @method static \Illuminate\Auth\AuthManager viaRequest(string $driver, callable $callback) * @method static \Closure userResolver() * @method static \Illuminate\Auth\AuthManager resolveUsersUsing(\Closure $userResolver) diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index ddbb57a863fd..935f9e7b79b6 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -162,6 +162,35 @@ public function testCustomDriverStatic() $this->assertSame($driver, $this->auth->guard(__CLASS__)); } + public function testAuthManagerCanResolveBackedEnumGuard() + { + $driver = $this->registerAuthDriver('default', true); + + $guard1 = $this->auth->guard(GuardName::Default); + $guard2 = $this->auth->guard('default'); + + $this->assertSame($guard1, $guard2); + $this->assertSame($driver, $guard1); + } + + public function testShouldUseAcceptsBackedEnum() + { + $this->registerAuthDriver('default', true); + $secondary = $this->registerAuthDriver('secondary', true); + + $this->auth->shouldUse(GuardName::Secondary); + + $this->assertSame('secondary', $this->auth->getDefaultDriver()); + $this->assertSame($secondary, $this->auth->guard()); + } + + public function testSetDefaultDriverAcceptsBackedEnum() + { + $this->auth->setDefaultDriver(GuardName::Secondary); + + $this->assertSame('secondary', $this->auth->getDefaultDriver()); + } + /** * Create a new config repository instance. * @@ -237,3 +266,9 @@ protected function authenticate(...$guards) $this->assertSame($request, $nextParam); } } + +enum GuardName: string +{ + case Default = 'default'; + case Secondary = 'secondary'; +} From 9dbefecc7d7abc0094b74aa5e939d7c35d40fc12 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:59:35 +0200 Subject: [PATCH 142/596] [12.x] Use PDO subclass polyfill (#59640) * DRAFT: Bump symfony/polyfill-php8.4 to latest version * Simplify code by leveraging PDO subclass Symfony polyfill See also https://github.com/symfony/polyfill/pull/549 * Apply suggestion from @crynobone * Apply suggestion from @crynobone --------- Co-authored-by: Mior Muhammad Zaki --- composer.json | 4 ++-- config/database.php | 5 +++-- .../Database/Schema/MySqlSchemaState.php | 21 +++++++------------ src/Illuminate/Database/composer.json | 3 ++- .../DatabaseMariaDbSchemaStateTest.php | 11 +++++----- .../Database/DatabaseMySqlSchemaStateTest.php | 11 +++++----- 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 4e7cf9826071..c718fe26b9dd 100644 --- a/composer.json +++ b/composer.json @@ -52,8 +52,8 @@ "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", diff --git a/config/database.php b/config/database.php index 8298041ee1b8..870d753cbc0c 100644 --- a/config/database.php +++ b/config/database.php @@ -1,6 +1,7 @@ true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - (PHP_VERSION_ID >= 80500 ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -81,7 +82,7 @@ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - (PHP_VERSION_ID >= 80500 ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 71830908bc93..b72440935981 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Database\Connection; use Illuminate\Support\Str; +use Pdo\Mysql; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; @@ -117,25 +118,19 @@ protected function connectionString(array $versionInfo) ? ' --socket="${:LARAVEL_LOAD_SOCKET}"' : ' --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}"'; - /** @phpstan-ignore class.notFound */ - if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA])) { + if (isset($config['options'][Mysql::ATTR_SSL_CA])) { $value .= ' --ssl-ca="${:LARAVEL_LOAD_SSL_CA}"'; } - /** @phpstan-ignore class.notFound */ - if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT])) { + if (isset($config['options'][Mysql::ATTR_SSL_CERT])) { $value .= ' --ssl-cert="${:LARAVEL_LOAD_SSL_CERT}"'; } - /** @phpstan-ignore class.notFound */ - if (isset($config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY])) { + if (isset($config['options'][Mysql::ATTR_SSL_KEY])) { $value .= ' --ssl-key="${:LARAVEL_LOAD_SSL_KEY}"'; } - /** @phpstan-ignore class.notFound */ - $verifyCertOption = PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT : \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT; - - if (isset($config['options'][$verifyCertOption]) && $config['options'][$verifyCertOption] === false) { + if (($config['options'][Mysql::ATTR_SSL_VERIFY_SERVER_CERT] ?? null) === false) { if (version_compare($versionInfo['version'], '5.7.11', '>=') && ! $versionInfo['isMariaDb']) { $value .= ' --ssl-mode=DISABLED'; } else { @@ -163,9 +158,9 @@ protected function baseVariables(array $config) 'LARAVEL_LOAD_USER' => $config['username'], 'LARAVEL_LOAD_PASSWORD' => $config['password'] ?? '', 'LARAVEL_LOAD_DATABASE' => $config['database'], - 'LARAVEL_LOAD_SSL_CA' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA] ?? '', // @phpstan-ignore class.notFound - 'LARAVEL_LOAD_SSL_CERT' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT] ?? '', // @phpstan-ignore class.notFound - 'LARAVEL_LOAD_SSL_KEY' => $config['options'][PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY] ?? '', // @phpstan-ignore class.notFound + 'LARAVEL_LOAD_SSL_CA' => $config['options'][Mysql::ATTR_SSL_CA] ?? '', + 'LARAVEL_LOAD_SSL_CERT' => $config['options'][Mysql::ATTR_SSL_CERT] ?? '', + 'LARAVEL_LOAD_SSL_KEY' => $config['options'][Mysql::ATTR_SSL_KEY] ?? '', ]; } diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index f713fdd6ce70..c22793ee0e5b 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -25,7 +25,8 @@ "illuminate/support": "^12.0", "laravel/serializable-closure": "^1.3|^2.0", "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php85": "^1.33" + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34" }, "autoload": { "psr-4": { diff --git a/tests/Database/DatabaseMariaDbSchemaStateTest.php b/tests/Database/DatabaseMariaDbSchemaStateTest.php index 96b992b93218..1e8cf7ded35a 100644 --- a/tests/Database/DatabaseMariaDbSchemaStateTest.php +++ b/tests/Database/DatabaseMariaDbSchemaStateTest.php @@ -5,6 +5,7 @@ use Generator; use Illuminate\Database\MariaDbConnection; use Illuminate\Database\Schema\MariaDbSchemaState; +use Pdo\Mysql; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -69,7 +70,7 @@ public static function provider(): Generator 'username' => 'root', 'database' => 'forge', 'options' => [ - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA => 'ssl.ca', + Mysql::ATTR_SSL_CA => 'ssl.ca', ], ], ]; @@ -89,9 +90,9 @@ public static function provider(): Generator 'username' => 'root', 'database' => 'forge', 'options' => [ - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA => 'ssl.ca', - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT => '/path/to/client-cert.pem', - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY => '/path/to/client-key.pem', + Mysql::ATTR_SSL_CA => 'ssl.ca', + Mysql::ATTR_SSL_CERT => '/path/to/client-cert.pem', + Mysql::ATTR_SSL_KEY => '/path/to/client-key.pem', ], ], ]; @@ -111,7 +112,7 @@ public static function provider(): Generator 'username' => 'root', 'database' => 'forge', 'options' => [ - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT : \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, + Mysql::ATTR_SSL_VERIFY_SERVER_CERT => false, ], ], ]; diff --git a/tests/Database/DatabaseMySqlSchemaStateTest.php b/tests/Database/DatabaseMySqlSchemaStateTest.php index 30aedb9fa250..4f1d309bf3a5 100644 --- a/tests/Database/DatabaseMySqlSchemaStateTest.php +++ b/tests/Database/DatabaseMySqlSchemaStateTest.php @@ -6,6 +6,7 @@ use Generator; use Illuminate\Database\MySqlConnection; use Illuminate\Database\Schema\MySqlSchemaState; +use Pdo\Mysql; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -71,7 +72,7 @@ public static function provider(): Generator 'username' => 'root', 'database' => 'forge', 'options' => [ - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA => 'ssl.ca', + Mysql::ATTR_SSL_CA => 'ssl.ca', ], ], ]; @@ -91,9 +92,9 @@ public static function provider(): Generator 'username' => 'root', 'database' => 'forge', 'options' => [ - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA => 'ssl.ca', - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CERT : \PDO::MYSQL_ATTR_SSL_CERT => '/path/to/client-cert.pem', - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_KEY : \PDO::MYSQL_ATTR_SSL_KEY => '/path/to/client-key.pem', + Mysql::ATTR_SSL_CA => 'ssl.ca', + Mysql::ATTR_SSL_CERT => '/path/to/client-cert.pem', + Mysql::ATTR_SSL_KEY => '/path/to/client-key.pem', ], ], ]; @@ -113,7 +114,7 @@ public static function provider(): Generator 'username' => 'root', 'database' => 'forge', 'options' => [ - PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT : \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, + Mysql::ATTR_SSL_VERIFY_SERVER_CERT => false, ], ], ]; From 5e9da10e15778af8e4b4f5d8fff895fa67ca0585 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:00:08 +0000 Subject: [PATCH 143/596] Update facade docblocks --- src/Illuminate/Support/Facades/App.php | 16 +++++++------- src/Illuminate/Support/Facades/Bus.php | 2 +- src/Illuminate/Support/Facades/Cache.php | 6 ++--- src/Illuminate/Support/Facades/Config.php | 6 ++--- src/Illuminate/Support/Facades/Context.php | 22 +++++++++---------- src/Illuminate/Support/Facades/DB.php | 4 ++-- src/Illuminate/Support/Facades/Exceptions.php | 8 +++---- src/Illuminate/Support/Facades/Http.php | 18 +++++++-------- src/Illuminate/Support/Facades/Process.php | 6 ++--- src/Illuminate/Support/Facades/Queue.php | 6 ++--- src/Illuminate/Support/Facades/Request.php | 8 +++---- src/Illuminate/Support/Facades/Schedule.php | 14 ++++++------ src/Illuminate/Support/Facades/Schema.php | 22 +++++++++---------- src/Illuminate/Support/Facades/Storage.php | 10 ++++----- 14 files changed, 74 insertions(+), 74 deletions(-) diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 5bad0f492df4..541792f5f5e5 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -53,7 +53,7 @@ * @method static void loadDeferredProviders() * @method static void loadDeferredProvider(string $service) * @method static void registerDeferredProvider(string $provider, string|null $service = null) - * @method static object|mixed make(string $abstract, array $parameters = []) + * @method static object|mixed make(string|string $abstract, array $parameters = []) * @method static bool bound(string $abstract) * @method static bool isBooted() * @method static void boot() @@ -79,7 +79,7 @@ * @method static never abort(int $code, string $message = '', array $headers = []) * @method static \Illuminate\Foundation\Application terminating(callable|string $callback) * @method static void terminate() - * @method static array getLoadedProviders() + * @method static array getLoadedProviders() * @method static bool providerIsLoaded(string $provider) * @method static array getDeferredServices() * @method static void setDeferredServices(array $services) @@ -119,11 +119,11 @@ * @method static mixed rebinding(string $abstract, \Closure $callback) * @method static mixed refresh(string $abstract, mixed $target, string $method) * @method static \Closure wrap(\Closure $callback, array $parameters = []) - * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) - * @method static \Closure|\Closure factory(string $abstract) - * @method static object|mixed makeWith(string|callable $abstract, array $parameters = []) - * @method static object|mixed get(string $id) - * @method static object build(\Closure|string $concrete) + * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) + * @method static \Closure|\Closure factory(string|string $abstract) + * @method static object|mixed makeWith(string|string|callable $abstract, array $parameters = []) + * @method static object|mixed get(string|string $id) + * @method static object build(\Closure|string $concrete) * @method static mixed resolveFromAttribute(\ReflectionAttribute $attribute) * @method static void beforeResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void resolving(\Closure|string $abstract, \Closure|null $callback = null) @@ -138,7 +138,7 @@ * @method static void forgetInstances() * @method static void forgetScopedInstances() * @method static void resolveEnvironmentUsing(callable|string|null $callback) - * @method static bool currentEnvironmentIs(array|string $environments) + * @method static bool currentEnvironmentIs(array|string $environments) * @method static \Illuminate\Foundation\Application getInstance() * @method static \Illuminate\Contracts\Container\Container|\Illuminate\Foundation\Application setInstance(\Illuminate\Contracts\Container\Container|null $container = null) * @method static void macro(string $name, object|callable $macro) diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index ab09857f2a6d..49d80828b37d 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -45,7 +45,7 @@ * @method static \Illuminate\Support\Collection dispatched(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedSync(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedAfterResponse(string $command, callable|null $callback = null) - * @method static \Illuminate\Support\Collection batched(callable $callback) + * @method static \Illuminate\Support\Collection batched(callable $callback) * @method static bool hasDispatched(string $command) * @method static bool hasDispatchedSync(string $command) * @method static bool hasDispatchedAfterResponse(string $command) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 5d98d4c980d6..68880ba704d6 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -22,13 +22,13 @@ * @method static bool missing(\UnitEnum|string $key) * @method static mixed get(\UnitEnum|array|string $key, mixed $default = null) * @method static array many(array $keys) - * @method static iterable getMultiple(iterable $keys, mixed $default = null) + * @method static iterable getMultiple(iterable $keys, mixed $default = null) * @method static mixed pull(\UnitEnum|array|string $key, mixed $default = null) * @method static string string(\UnitEnum|string $key, \Closure|string|null $default = null) * @method static int integer(\UnitEnum|string $key, \Closure|int|null $default = null) * @method static float float(\UnitEnum|string $key, \Closure|float|null $default = null) * @method static bool boolean(\UnitEnum|string $key, \Closure|bool|null $default = null) - * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) + * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) * @method static bool put(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool set(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) @@ -45,7 +45,7 @@ * @method static \Illuminate\Cache\Limiters\ConcurrencyLimiterBuilder funnel(\UnitEnum|string $name) * @method static bool forget(\UnitEnum|array|string $key) * @method static bool delete(\UnitEnum|array|string $key) - * @method static bool deleteMultiple(iterable $keys) + * @method static bool deleteMultiple(iterable $keys) * @method static bool clear() * @method static \Illuminate\Cache\TaggedCache tags(mixed $names) * @method static string|null getName() diff --git a/src/Illuminate/Support/Facades/Config.php b/src/Illuminate/Support/Facades/Config.php index 09228769a306..990e34739f68 100755 --- a/src/Illuminate/Support/Facades/Config.php +++ b/src/Illuminate/Support/Facades/Config.php @@ -5,13 +5,13 @@ /** * @method static bool has(string $key) * @method static mixed get(array|string $key, mixed $default = null) - * @method static array getMany(array $keys) + * @method static array getMany(array $keys) * @method static string string(string $key, \Closure|string|null $default = null) * @method static int integer(string $key, \Closure|int|null $default = null) * @method static float float(string $key, \Closure|float|null $default = null) * @method static bool boolean(string $key, \Closure|bool|null $default = null) - * @method static array array(string $key, \Closure|array|null $default = null) - * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) + * @method static array array(string $key, \Closure|array|null $default = null) + * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) * @method static void set(array|string $key, mixed $value = null) * @method static void prepend(string $key, mixed $value) * @method static void push(string $key, mixed $value) diff --git a/src/Illuminate/Support/Facades/Context.php b/src/Illuminate/Support/Facades/Context.php index be57f00fa6d0..714ec2b6ddd8 100644 --- a/src/Illuminate/Support/Facades/Context.php +++ b/src/Illuminate/Support/Facades/Context.php @@ -7,22 +7,22 @@ * @method static bool missing(string $key) * @method static bool hasHidden(string $key) * @method static bool missingHidden(string $key) - * @method static array all() - * @method static array allHidden() + * @method static array all() + * @method static array allHidden() * @method static mixed get(string $key, mixed $default = null) * @method static mixed getHidden(string $key, mixed $default = null) * @method static mixed pull(string $key, mixed $default = null) * @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 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 mixed remember(string $key, mixed $value) * @method static mixed rememberHidden(string $key, mixed $value) - * @method static \Illuminate\Log\Context\Repository forget(string|array $key) - * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) + * @method static \Illuminate\Log\Context\Repository forget(string|array $key) + * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) * @method static \Illuminate\Log\Context\Repository addIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository addHiddenIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository push(string $key, mixed ...$values) @@ -33,7 +33,7 @@ * @method static \Illuminate\Log\Context\Repository decrement(string $key, int $amount = 1) * @method static bool stackContains(string $key, mixed $value, bool $strict = false) * @method static bool hiddenStackContains(string $key, mixed $value, bool $strict = false) - * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) + * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) * @method static bool isEmpty() * @method static \Illuminate\Log\Context\Repository dehydrating(callable $callback) * @method static \Illuminate\Log\Context\Repository hydrated(callable $callback) diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 13e87d3923bf..94f118cfad47 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -23,7 +23,7 @@ * @method static string[] availableDrivers() * @method static void extend(string $name, callable $resolver) * @method static void forgetExtension(string $name) - * @method static array getConnections() + * @method static array getConnections() * @method static void setReconnector(callable $reconnector) * @method static \Illuminate\Database\DatabaseManager setApplication(\Illuminate\Contracts\Foundation\Application $app) * @method static void macro(string $name, object|callable $macro) @@ -42,7 +42,7 @@ * @method static array selectFromWriteConnection(string $query, array $bindings = []) * @method static array select(string $query, array $bindings = [], bool $useReadPdo = true) * @method static array selectResultSets(string $query, array $bindings = [], bool $useReadPdo = true) - * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true) + * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true) * @method static bool insert(string $query, array $bindings = []) * @method static int update(string $query, array $bindings = []) * @method static int delete(string $query, array $bindings = []) diff --git a/src/Illuminate/Support/Facades/Exceptions.php b/src/Illuminate/Support/Facades/Exceptions.php index 263b95bd0418..59b4b07ef2d7 100644 --- a/src/Illuminate/Support/Facades/Exceptions.php +++ b/src/Illuminate/Support/Facades/Exceptions.php @@ -15,7 +15,7 @@ * @method static \Illuminate\Foundation\Exceptions\Handler dontReportWhen(callable $dontReportWhen) * @method static \Illuminate\Foundation\Exceptions\Handler ignore(array|string $exceptions) * @method static \Illuminate\Foundation\Exceptions\Handler dontFlash(array|string $attributes) - * @method static \Illuminate\Foundation\Exceptions\Handler level(string $type, string $level) + * @method static \Illuminate\Foundation\Exceptions\Handler level(string<\Throwable> $type, string $level) * @method static void report(\Throwable $e) * @method static bool shouldReport(\Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler throttleUsing(callable $throttleUsing) @@ -26,14 +26,14 @@ * @method static \Illuminate\Foundation\Exceptions\Handler shouldRenderJsonWhen(callable $callback) * @method static \Illuminate\Foundation\Exceptions\Handler dontReportDuplicates() * @method static \Illuminate\Contracts\Debug\ExceptionHandler handler() - * @method static void assertReported(\Closure|string $exception) + * @method static void assertReported(\Closure|string<\Throwable> $exception) * @method static void assertReportedCount(int $count) - * @method static void assertNotReported(\Closure|string $exception) + * @method static void assertNotReported(\Closure|string<\Throwable> $exception) * @method static void assertNothingReported() * @method static void renderForConsole(\Symfony\Component\Console\Output\OutputInterface $output, \Throwable $e) * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwOnReport() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwFirstReported() - * @method static array reported() + * @method static array<\Throwable> reported() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake setHandler(\Illuminate\Contracts\Debug\ExceptionHandler $handler) * * @see \Illuminate\Foundation\Exceptions\Handler diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 50bd17818fca..25e3478e183b 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -10,21 +10,21 @@ * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalOptions(\Closure|array $options) * @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\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 \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(array|null $only = null) + * @method static \Illuminate\Http\Client\Factory allowStrayRequests(array|null $only = null) * @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|\Closure $callback) - * @method static void assertSentInOrder(array $callbacks) + * @method static void assertSentInOrder(array $callbacks) * @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(\Closure|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() @@ -65,7 +65,7 @@ * @method static \Illuminate\Http\Client\PendingRequest withMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withResponseMiddleware(callable $middleware) - * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) + * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) * @method static \Illuminate\Http\Client\PendingRequest beforeSending(callable $callback) * @method static \Illuminate\Http\Client\PendingRequest afterResponse(callable|null $callback) * @method static \Illuminate\Http\Client\PendingRequest throw(callable|null $callback = null) @@ -79,7 +79,7 @@ * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface patch(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface put(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface delete(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) - * @method static array pool(callable $callback, int|null $concurrency = null) + * @method static array pool(callable $callback, int|null $concurrency = null) * @method static \Illuminate\Http\Client\Batch batch(callable $callback) * @method static \Illuminate\Http\Client\Response|\Illuminate\Http\Client\Promises\LazyPromise send(string $method, string $url, array $options = []) * @method static \GuzzleHttp\Client buildClient() @@ -93,9 +93,9 @@ * @method static array mergeOptions(array ...$options) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) * @method static bool isAllowedRequestUrl(string $url) - * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) + * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) * @method static \GuzzleHttp\Promise\PromiseInterface|null getPromise() - * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) + * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) * @method static \Illuminate\Http\Client\PendingRequest dontTruncateExceptions() * @method static \Illuminate\Http\Client\PendingRequest setClient(\GuzzleHttp\Client $client) * @method static \Illuminate\Http\Client\PendingRequest setHandler(callable $handler) diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index 35dfdca79345..ecaaae3b24a8 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -6,7 +6,7 @@ use Illuminate\Process\Factory; /** - * @method static \Illuminate\Process\PendingProcess command(array|string $command) + * @method static \Illuminate\Process\PendingProcess command(array|string $command) * @method static \Illuminate\Process\PendingProcess path(string $path) * @method static \Illuminate\Process\PendingProcess timeout(int $timeout) * @method static \Illuminate\Process\PendingProcess idleTimeout(int $timeout) @@ -16,8 +16,8 @@ * @method static \Illuminate\Process\PendingProcess quietly() * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) - * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) - * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) * @method static bool supportsTty() * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) * @method static \Illuminate\Process\PendingProcess|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 710365458658..4c86d10e72b2 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -61,12 +61,12 @@ * @method static void assertCount(int $expectedCount) * @method static void assertNothingPushed() * @method static \Illuminate\Support\Collection pushed(string $job, callable|null $callback = null) - * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) - * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) + * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) + * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) * @method static bool hasPushed(string $job) * @method static bool shouldFakeJob(object $job) * @method static array pushedJobs() - * @method static array rawPushes() + * @method static array rawPushes() * @method static \Illuminate\Support\Testing\Fakes\QueueFake serializeAndRestore(bool $serializeAndRestore = true) * @method static void releaseUniqueJobLocks() * diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 2865715dcb98..0ff5c7aac60c 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -152,9 +152,9 @@ * @method static string|array|null post(string|null $key = null, string|array|null $default = null) * @method static bool hasCookie(string $key) * @method static string|array|null cookie(string|null $key = null, string|array|null $default = null) - * @method static array allFiles() + * @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) @@ -175,8 +175,8 @@ * @method static float|int clamp(string $key, int|float $min, int|float $max, int|float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, \UnitEnum|string|null $tz = null) * @method static \Carbon\CarbonInterval|null interval(string $key, \Carbon\Unit|string|null $unit = null) - * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string $enumClass, \BackedEnum|null $default = null) - * @method static \BackedEnum[] enums(string $key, string $enumClass) + * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string<\BackedEnum> $enumClass, \BackedEnum|null $default = null) + * @method static \BackedEnum[] enums(string $key, string<\BackedEnum> $enumClass) * @method static array array(array|string|null $key = null) * @method static \Illuminate\Support\Collection collect(array|string|null $key = null) * @method static array only(mixed $keys) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index eabe3ef7cb3a..6b2f0d261122 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -50,7 +50,7 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyFifteenMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThirtyMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyOddHour(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyTwoHours(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThreeHours(array|string|int $offset = 0) @@ -59,8 +59,8 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daily() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes at(string $time) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes dailyAt(string $time) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekdays() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekends() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes mondays() @@ -73,14 +73,14 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weeklyOn(mixed $dayOfWeek, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes lastDayOfMonth(string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array|int ...$days) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array>|int ...$days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterlyOn(int $dayOfQuarter = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes days(mixed $days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes timezone(\UnitEnum|\DateTimeZone|string $timezone) * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 5c617687bb3a..523813f228ed 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -10,31 +10,31 @@ * @method static void morphUsingUlids() * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) - * @method static array getSchemas() + * @method static array getSchemas() * @method static bool hasTable(string $table) * @method static bool hasView(string $view) - * @method static array getTables(string|string[]|null $schema = null) - * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) - * @method static array getViews(string|string[]|null $schema = null) - * @method static array getTypes(string|string[]|null $schema = null) + * @method static array getTables(string|string[]|null $schema = null) + * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) + * @method static array getViews(string|string[]|null $schema = null) + * @method static array getTypes(string|string[]|null $schema = null) * @method static bool hasColumn(string $table, string $column) - * @method static bool hasColumns(string $table, array $columns) + * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) * @method static void whenTableDoesntHaveColumn(string $table, string $column, \Closure $callback) * @method static void whenTableHasIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static void whenTableDoesntHaveIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static string getColumnType(string $table, string $column, bool $fullDefinition = false) - * @method static array getColumnListing(string $table) - * @method static array getColumns(string $table) - * @method static array getIndexes(string $table) - * @method static array getIndexListing(string $table) + * @method static array getColumnListing(string $table) + * @method static array getColumns(string $table) + * @method static array getIndexes(string $table) + * @method static array getIndexListing(string $table) * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) * @method static void dropIfExists(string $table) - * @method static void dropColumns(string $table, string|array $columns) + * @method static void dropColumns(string $table, string|array $columns) * @method static void dropAllTables() * @method static void dropAllViews() * @method static void dropAllTypes() diff --git a/src/Illuminate/Support/Facades/Storage.php b/src/Illuminate/Support/Facades/Storage.php index 6a12ab40e92b..bac3e2a814bd 100644 --- a/src/Illuminate/Support/Facades/Storage.php +++ b/src/Illuminate/Support/Facades/Storage.php @@ -40,10 +40,10 @@ * @method static bool move(string $from, string $to) * @method static int size(string $path) * @method static int lastModified(string $path) - * @method static array files(string|null $directory = null, bool $recursive = false) - * @method static array allFiles(string|null $directory = null) - * @method static array directories(string|null $directory = null, bool $recursive = false) - * @method static array allDirectories(string|null $directory = null) + * @method static array files(string|null $directory = null, bool $recursive = false) + * @method static array allFiles(string|null $directory = null) + * @method static array directories(string|null $directory = null, bool $recursive = false) + * @method static array allDirectories(string|null $directory = null) * @method static bool makeDirectory(string $path) * @method static bool deleteDirectory(string $directory) * @method static \Illuminate\Filesystem\FilesystemAdapter assertExists(string|array $path, string|null $content = null) @@ -81,7 +81,7 @@ * @method static mixed macroCall(string $method, array $parameters) * @method static bool has(string $location) * @method static string read(string $location) - * @method static \League\Flysystem\DirectoryListing listContents(string $location, bool $deep = false) + * @method static \League\Flysystem\DirectoryListing<\League\Flysystem\StorageAttributes> listContents(string $location, bool $deep = false) * @method static int fileSize(string $path) * @method static string visibility(string $path) * @method static void write(string $location, string $contents, array $config = []) From 43c68acf8952db318c2e28fdf52583329f5ecc8c Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:00:40 +0200 Subject: [PATCH 144/596] [13.x] Add spatie/fork to composer suggestions (#59660) * Add spatie/fork to suggested packages * Explicitly list why spatie/fork is missing for future reference --- composer.json | 1 + src/Illuminate/Concurrency/ForkDriver.php | 2 +- src/Illuminate/Concurrency/composer.json | 3 +++ .../Concurrency/ConcurrencyTest.php | 24 ++++++++++--------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 030f911c01dc..218e76f03a43 100644 --- a/composer.json +++ b/composer.json @@ -168,6 +168,7 @@ "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 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", diff --git a/src/Illuminate/Concurrency/ForkDriver.php b/src/Illuminate/Concurrency/ForkDriver.php index 732873a72ee9..52d05e7be831 100644 --- a/src/Illuminate/Concurrency/ForkDriver.php +++ b/src/Illuminate/Concurrency/ForkDriver.php @@ -22,7 +22,7 @@ public function run(Closure|array $tasks): array $keys = array_keys($tasks); $values = array_values($tasks); - /** @phpstan-ignore class.notFound */ + /** @phpstan-ignore class.notFound (spatie/fork is not installed as it is practically incompatible with Windows) */ $results = Fork::new()->run(...$values); ksort($results); diff --git a/src/Illuminate/Concurrency/composer.json b/src/Illuminate/Concurrency/composer.json index 91f343fc5403..87dfa669efe5 100644 --- a/src/Illuminate/Concurrency/composer.json +++ b/src/Illuminate/Concurrency/composer.json @@ -21,6 +21,9 @@ "illuminate/support": "^13.0", "laravel/serializable-closure": "^2.0.10" }, + "suggest": { + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2)." + }, "minimum-stability": "dev", "autoload": { "psr-4": { diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index 6de1ac4bbb6f..19a75d9ba491 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -72,18 +72,20 @@ public function testOutputIsMappedToArrayInput() $this->assertArrayHasKey('first', $syncOutput); $this->assertArrayHasKey('second', $syncOutput); - /** As of now, the spatie/fork package is not included by default. - * $forkOutput = Concurrency::driver('fork')->run([ - * 'first' => fn() => 1 + 1, - * 'second' => fn() => 2 + 2, - * ]);. - * - * $this->assertIsArray($forkOutput); - * $this->assertArrayHasKey('first', $forkOutput); - * $this->assertArrayHasKey('second', $forkOutput); - * $this->assertEquals(2, $forkOutput['first']); - * $this->assertEquals(4, $forkOutput['second']); + /** + * As of now, the spatie/fork package is not included by default, + * as it is practically incompatible with Windows. */ + // $forkOutput = Concurrency::driver('fork')->run([ + // 'first' => fn () => 1 + 1, + // 'second' => fn () => 2 + 2, + // ]); + + // $this->assertIsArray($forkOutput); + // $this->assertArrayHasKey('first', $forkOutput); + // $this->assertArrayHasKey('second', $forkOutput); + // $this->assertEquals(2, $forkOutput['first']); + // $this->assertEquals(4, $forkOutput['second']); } public function testRunHandlerProcessErrorWithDefaultExceptionWithoutParam() From 9152f353b78969dcdc9ffd068d6bd544136db60b Mon Sep 17 00:00:00 2001 From: Sebastian Cabarcas Berrio <42840369+scabarcas17@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:02:05 -0500 Subject: [PATCH 145/596] [13.x] Add enum support to Manager driver method (#59659) * [13.x] Add enum support to Manager driver method * Remove unnecessary enum_value import --------- Co-authored-by: sebastian cabarcas --- src/Illuminate/Support/Manager.php | 4 +- tests/Integration/Support/ManagerTest.php | 52 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Manager.php b/src/Illuminate/Support/Manager.php index 4a82fa665277..e9f01f5962f3 100755 --- a/src/Illuminate/Support/Manager.php +++ b/src/Illuminate/Support/Manager.php @@ -58,14 +58,14 @@ abstract public function getDefaultDriver(); /** * Get a driver instance. * - * @param string|null $driver + * @param \UnitEnum|string|null $driver * @return mixed * * @throws \InvalidArgumentException */ public function driver($driver = null) { - $driver = $driver ?: $this->getDefaultDriver(); + $driver = enum_value($driver) ?: $this->getDefaultDriver(); if (is_null($driver)) { throw new InvalidArgumentException(sprintf( diff --git a/tests/Integration/Support/ManagerTest.php b/tests/Integration/Support/ManagerTest.php index 87e9b52f0261..53865a758610 100644 --- a/tests/Integration/Support/ManagerTest.php +++ b/tests/Integration/Support/ManagerTest.php @@ -31,4 +31,56 @@ public function testCustomDriverStaticClosure() $manager->extend(__CLASS__, static fn () => $driver); $this->assertSame($driver, $manager->driver(__CLASS__)); } + + public function testEnumDriverCanBeResolved() + { + $manager = new NullableManager($this->app); + $driver = new stdClass; + + $manager->extend('my_driver', static fn () => $driver); + $this->assertSame($driver, $manager->driver(ManagerDriverName::MyDriver)); + } + + public function testEnumDriverIsCached() + { + $manager = new NullableManager($this->app); + + $manager->extend('my_driver', static fn () => new stdClass); + + $driver1 = $manager->driver(ManagerDriverName::MyDriver); + $driver2 = $manager->driver(ManagerDriverName::MyDriver); + + $this->assertSame($driver1, $driver2); + } + + public function testEnumDriverMatchesStringDriver() + { + $manager = new NullableManager($this->app); + + $manager->extend('my_driver', static fn () => new stdClass); + + $fromEnum = $manager->driver(ManagerDriverName::MyDriver); + $fromString = $manager->driver('my_driver'); + + $this->assertSame($fromEnum, $fromString); + } + + public function testUnitEnumDriverCanBeResolved() + { + $manager = new NullableManager($this->app); + $driver = new stdClass; + + $manager->extend('MyDriver', static fn () => $driver); + $this->assertSame($driver, $manager->driver(ManagerUnitDriverName::MyDriver)); + } +} + +enum ManagerDriverName: string +{ + case MyDriver = 'my_driver'; +} + +enum ManagerUnitDriverName +{ + case MyDriver; } From c1bc8e758a63ab3e404ead77f7fd85ca1dbd3c01 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:02:41 +0000 Subject: [PATCH 146/596] Update facade docblocks --- src/Illuminate/Support/Facades/Hash.php | 2 +- src/Illuminate/Support/Facades/MaintenanceMode.php | 2 +- src/Illuminate/Support/Facades/Notification.php | 2 +- src/Illuminate/Support/Facades/Session.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Support/Facades/Hash.php b/src/Illuminate/Support/Facades/Hash.php index 450705ef0215..f18a5fcee1f2 100755 --- a/src/Illuminate/Support/Facades/Hash.php +++ b/src/Illuminate/Support/Facades/Hash.php @@ -12,7 +12,7 @@ * @method static bool needsRehash(string $hashedValue, array $options = []) * @method static bool isHashed(string $value) * @method static string getDefaultDriver() - * @method static mixed driver(string|null $driver = null) + * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Hashing\HashManager extend(string $driver, \Closure $callback) * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() diff --git a/src/Illuminate/Support/Facades/MaintenanceMode.php b/src/Illuminate/Support/Facades/MaintenanceMode.php index 78c46e70926e..d88a01c7944a 100644 --- a/src/Illuminate/Support/Facades/MaintenanceMode.php +++ b/src/Illuminate/Support/Facades/MaintenanceMode.php @@ -6,7 +6,7 @@ /** * @method static string getDefaultDriver() - * @method static mixed driver(string|null $driver = null) + * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Foundation\MaintenanceModeManager extend(string $driver, \Closure $callback) * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() diff --git a/src/Illuminate/Support/Facades/Notification.php b/src/Illuminate/Support/Facades/Notification.php index 9b6eb0b34bc9..bb731b170056 100644 --- a/src/Illuminate/Support/Facades/Notification.php +++ b/src/Illuminate/Support/Facades/Notification.php @@ -14,7 +14,7 @@ * @method static string deliversVia() * @method static void deliverVia(string $channel) * @method static \Illuminate\Notifications\ChannelManager locale(string $locale) - * @method static mixed driver(string|null $driver = null) + * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Notifications\ChannelManager extend(string $driver, \Closure $callback) * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() diff --git a/src/Illuminate/Support/Facades/Session.php b/src/Illuminate/Support/Facades/Session.php index 2999e3814da3..3cef4609632f 100755 --- a/src/Illuminate/Support/Facades/Session.php +++ b/src/Illuminate/Support/Facades/Session.php @@ -10,7 +10,7 @@ * @method static array getSessionConfig() * @method static string|null getDefaultDriver() * @method static void setDefaultDriver(string $name) - * @method static mixed driver(string|null $driver = null) + * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Session\SessionManager extend(string $driver, \Closure $callback) * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() From 76025599b3ac105b21124a52a50e5756cf4dee4a Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 13 Apr 2026 15:52:21 +0100 Subject: [PATCH 147/596] Improve custom driver binding (#59614) --- src/Illuminate/Auth/AuthManager.php | 11 +++--- .../Broadcasting/BroadcastManager.php | 10 +++--- src/Illuminate/Cache/CacheManager.php | 11 +++--- .../Filesystem/FilesystemManager.php | 11 +++--- src/Illuminate/Log/LogManager.php | 10 +++--- src/Illuminate/Redis/RedisManager.php | 10 ++++-- src/Illuminate/Support/Manager.php | 12 ++++--- .../Support/MultipleInstanceManager.php | 9 +++-- .../Support/RebindsCallbacksToSelf.php | 34 +++++++++++++++++++ tests/Auth/AuthenticateMiddlewareTest.php | 21 ++++++++++++ tests/Cache/CacheManagerTest.php | 31 ++++++++++++++++- tests/Filesystem/FilesystemManagerTest.php | 27 +++++++++++++++ .../Broadcasting/BroadcastManagerTest.php | 31 ++++++++++++++++- tests/Integration/Support/ManagerTest.php | 19 +++++++++++ 14 files changed, 213 insertions(+), 34 deletions(-) create mode 100644 src/Illuminate/Support/RebindsCallbacksToSelf.php diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index dad7faa01fac..09c19fb9998e 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -4,9 +4,10 @@ use Closure; use Illuminate\Contracts\Auth\Factory as FactoryContract; +use Illuminate\Support\RebindsCallbacksToSelf; use InvalidArgumentException; +use ReflectionException; use RuntimeException; -use Throwable; use function Illuminate\Support\enum_value; @@ -16,7 +17,7 @@ */ class AuthManager implements FactoryContract { - use CreatesUserProviders; + use CreatesUserProviders, RebindsCallbacksToSelf; /** * The application instance. @@ -276,9 +277,9 @@ public function resolveUsersUsing(Closure $userResolver) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index fa94658a325c..da90f13855b4 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -22,9 +22,11 @@ use Illuminate\Queue\Attributes\Queue as QueueAttribute; use Illuminate\Queue\Attributes\ReadsQueueAttributes; use Illuminate\Support\Queue\Concerns\ResolvesQueueRoutes; +use Illuminate\Support\RebindsCallbacksToSelf; use InvalidArgumentException; use Psr\Log\LoggerInterface; use Pusher\Pusher; +use ReflectionException; use RuntimeException; use Throwable; @@ -33,7 +35,7 @@ */ class BroadcastManager implements FactoryContract { - use ReadsQueueAttributes, ResolvesQueueRoutes; + use ReadsQueueAttributes, RebindsCallbacksToSelf, ResolvesQueueRoutes; /** * The application instance. @@ -505,9 +507,9 @@ public function purge($name = null) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 011d33e89ac4..0ee3c7e479ab 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -8,11 +8,12 @@ use Illuminate\Contracts\Cache\Store; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Support\Arr; +use Illuminate\Support\RebindsCallbacksToSelf; use InvalidArgumentException; use Mockery; use Mockery\LegacyMockInterface; +use ReflectionException; use RuntimeException; -use Throwable; use function Illuminate\Support\enum_value; @@ -22,6 +23,8 @@ */ class CacheManager implements FactoryContract { + use RebindsCallbacksToSelf; + /** * The application instance. * @@ -535,9 +538,9 @@ public function purge($name = null) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index c840fe09d5ac..9d2e39642a51 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -6,6 +6,7 @@ use Closure; use Illuminate\Contracts\Filesystem\Factory as FactoryContract; use Illuminate\Support\Arr; +use Illuminate\Support\RebindsCallbacksToSelf; use InvalidArgumentException; use League\Flysystem\AwsS3V3\AwsS3V3Adapter as S3Adapter; use League\Flysystem\AwsS3V3\PortableVisibilityConverter as AwsS3PortableVisibilityConverter; @@ -20,8 +21,8 @@ use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; use League\Flysystem\UnixVisibility\PortableVisibilityConverter; use League\Flysystem\Visibility; +use ReflectionException; use RuntimeException; -use Throwable; use function Illuminate\Support\enum_value; @@ -31,6 +32,8 @@ */ class FilesystemManager implements FactoryContract { + use RebindsCallbacksToSelf; + /** * The application instance. * @@ -442,9 +445,9 @@ public function purge($name = null) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index b6762bdbd5b5..f8d9ff1e8b34 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Log\ContextLogProcessor; use Illuminate\Support\Collection; +use Illuminate\Support\RebindsCallbacksToSelf; use Illuminate\Support\Str; use InvalidArgumentException; use Monolog\Formatter\LineFormatter; @@ -21,6 +22,7 @@ use Monolog\Processor\ProcessorInterface; use Monolog\Processor\PsrLogMessageProcessor; use Psr\Log\LoggerInterface; +use ReflectionException; use RuntimeException; use Throwable; @@ -31,7 +33,7 @@ */ class LogManager implements LoggerInterface { - use ParsesLogConfiguration; + use ParsesLogConfiguration, RebindsCallbacksToSelf; /** * The application instance. @@ -602,9 +604,9 @@ public function setDefaultDriver($name) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index d67464887d66..a1c7fe25b285 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -9,7 +9,9 @@ use Illuminate\Redis\Connectors\PredisConnector; use Illuminate\Support\Arr; use Illuminate\Support\ConfigurationUrlParser; +use Illuminate\Support\RebindsCallbacksToSelf; use InvalidArgumentException; +use ReflectionException; use RuntimeException; use Throwable; @@ -20,6 +22,8 @@ */ class RedisManager implements Factory { + use RebindsCallbacksToSelf; + /** * The application instance. * @@ -266,9 +270,9 @@ public function purge($name = null) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Support/Manager.php b/src/Illuminate/Support/Manager.php index e9f01f5962f3..c6491c5a5178 100755 --- a/src/Illuminate/Support/Manager.php +++ b/src/Illuminate/Support/Manager.php @@ -5,10 +5,13 @@ use Closure; use Illuminate\Contracts\Container\Container; use InvalidArgumentException; -use Throwable; +use ReflectionException; +use RuntimeException; abstract class Manager { + use RebindsCallbacksToSelf; + /** * The container instance. * @@ -120,7 +123,6 @@ protected function callCustomCreator($driver) * Register a custom driver creator Closure. * * @param string $driver - * @param \Closure $callback * * @param-closure-this $this $callback * @@ -129,9 +131,9 @@ protected function callCustomCreator($driver) public function extend($driver, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$driver] = $callback; diff --git a/src/Illuminate/Support/MultipleInstanceManager.php b/src/Illuminate/Support/MultipleInstanceManager.php index 6b1d8d0bc3c8..fd041384588b 100644 --- a/src/Illuminate/Support/MultipleInstanceManager.php +++ b/src/Illuminate/Support/MultipleInstanceManager.php @@ -4,11 +4,14 @@ use Closure; use InvalidArgumentException; +use ReflectionException; use RuntimeException; use Throwable; abstract class MultipleInstanceManager { + use RebindsCallbacksToSelf; + /** * The application instance. * @@ -200,9 +203,9 @@ public function purge($name = null) public function extend($name, Closure $callback) { try { - $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; - } catch (Throwable) { - $callback = $callback->bindTo(null, static::class); + $callback = $this->bindCallbackToSelf($callback) ?? throw new RuntimeException('Unable to bind custom driver callback'); + } catch (ReflectionException $e) { + throw new RuntimeException('Unable to bind custom driver callback', previous: $e); } $this->customCreators[$name] = $callback; diff --git a/src/Illuminate/Support/RebindsCallbacksToSelf.php b/src/Illuminate/Support/RebindsCallbacksToSelf.php new file mode 100644 index 000000000000..722f601bee4a --- /dev/null +++ b/src/Illuminate/Support/RebindsCallbacksToSelf.php @@ -0,0 +1,34 @@ +isAnonymous()) { + if ($reflector->isStatic()) { + // Static functions are bound without $this. + $callback = $callback->bindTo(null, static::class); + } else { + // Non-static functions are bound to $this. + $callback = $callback->bindTo($this, static::class); + } + } + + return $callback; + } +} diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index 935f9e7b79b6..dd5426c40e51 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -8,6 +8,7 @@ use Illuminate\Auth\Middleware\Authenticate; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\RequestGuard; +use Illuminate\Config\Repository; use Illuminate\Config\Repository as Config; use Illuminate\Container\Container; use Illuminate\Http\Request; @@ -162,6 +163,15 @@ public function testCustomDriverStatic() $this->assertSame($driver, $this->auth->guard(__CLASS__)); } + public function testCustomInvokableDriver() + { + $driver = new stdClass; + $creator = new CustomAuthDriver($driver); + + $this->auth->extend(__CLASS__, $creator(...)); + $this->assertSame($driver, $this->auth->guard(__CLASS__)); + } + public function testAuthManagerCanResolveBackedEnumGuard() { $driver = $this->registerAuthDriver('default', true); @@ -272,3 +282,14 @@ enum GuardName: string case Default = 'default'; case Secondary = 'secondary'; } + +class CustomAuthDriver { + public function __construct(private object $driver) + { + } + + public function __invoke() + { + return $this->driver; + } +} diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index 5efceadcf0bc..e916284d6ebb 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -49,7 +49,26 @@ public function testCustomDriverStaticClosure() $this->assertSame($driver, $manager->store(__CLASS__)); } - public function testCustomDriverOverridesInternalDrivers() + public function testInvokableObjectDriverClosure() + { + $manager = new CacheManager($this->getApp([ + 'cache' => [ + 'stores' => [ + __CLASS__ => [ + 'driver' => __CLASS__, + ], + ], + ], + ])); + + $driver = new stdClass; + $creator = new CustomCacheDriver($driver); + + $manager->extend(__CLASS__, $creator(...)); + $this->assertSame($driver, $manager->store(__CLASS__)); + } + + public function test_custom_driver_overrides_internal_drivers() { $userConfig = [ 'cache' => [ @@ -443,6 +462,16 @@ protected function getApp(array $userConfig) } } +class CustomCacheDriver +{ + public function __construct(private object $driver) {} + + public function __invoke() + { + return $this->driver; + } +} + enum CacheStoreName: string { case ArrayStore = 'array'; diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 772cbe603399..303daf8ce352 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -247,6 +247,23 @@ public function testCustomDriverStaticClosure() $this->assertSame($driver, $manager->disk(__CLASS__)); } + public function testInvokableObjectDriverClosure() + { + $manager = new FilesystemManager(tap(new Application, static function ($app) { + $app['config'] = [ + 'filesystems.disks.'.__CLASS__ => [ + 'driver' => __CLASS__, + ], + ]; + })); + + $driver = new stdClass; + $creator = new CustomFilesystemDriver($driver); + + $manager->extend(__CLASS__, $creator(...)); + $this->assertSame($driver, $manager->disk(__CLASS__)); + } + // public function testKeepTrackOfAdapterDecoration() // { // try { @@ -271,3 +288,13 @@ public function testCustomDriverStaticClosure() // } // } } + +class CustomFilesystemDriver +{ + public function __construct(private object $driver) {} + + public function __invoke() + { + return $this->driver; + } +} diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index b6d7e504813d..5932861e3756 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -176,7 +176,26 @@ public function testCustomDriverStaticClosure() $this->assertSame($driver, $manager->connection(__CLASS__)); } - public function testThrowExceptionWhenDriverCreationFails() + public function testInvokableObjectDriverClosure() + { + $manager = new BroadcastManager($this->getApp([ + 'broadcasting' => [ + 'connections' => [ + __CLASS__ => [ + 'driver' => __CLASS__, + ], + ], + ], + ])); + + $driver = new stdClass; + $creator = new CustomBroadcastDriver($driver); + + $manager->extend(__CLASS__, $creator(...)); + $this->assertSame($driver, $manager->connection(__CLASS__)); + } + + public function test_throw_exception_when_driver_creation_fails() { $userConfig = [ 'broadcasting' => [ @@ -288,3 +307,13 @@ public function broadcastOn() // } } + +class CustomBroadcastDriver +{ + public function __construct(private object $driver) {} + + public function __invoke() + { + return $this->driver; + } +} diff --git a/tests/Integration/Support/ManagerTest.php b/tests/Integration/Support/ManagerTest.php index 53865a758610..798318e7a0d3 100644 --- a/tests/Integration/Support/ManagerTest.php +++ b/tests/Integration/Support/ManagerTest.php @@ -32,6 +32,25 @@ public function testCustomDriverStaticClosure() $this->assertSame($driver, $manager->driver(__CLASS__)); } + public function testInvokableObjectDriverClosure() + { + $manager = new NullableManager($this->app); + $driver = new stdClass; + $creator = new CustomDriver($driver); + + $manager->extend(__CLASS__, $creator(...)); + $this->assertSame($driver, $manager->driver(__CLASS__)); + } +} + +class CustomDriver { + public function __construct(private object $object) {} + + public function __invoke() + { + return $this->object; + } + public function testEnumDriverCanBeResolved() { $manager = new NullableManager($this->app); From a3960e8ff8ae2daa7ff609a245c51d9fe0aca684 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 13 Apr 2026 14:52:53 +0000 Subject: [PATCH 148/596] Apply fixes from StyleCI --- src/Illuminate/Redis/RedisManager.php | 1 - src/Illuminate/Support/MultipleInstanceManager.php | 1 - tests/Auth/AuthenticateMiddlewareTest.php | 3 ++- tests/Cache/CacheManagerTest.php | 4 +++- tests/Filesystem/FilesystemManagerTest.php | 4 +++- tests/Integration/Broadcasting/BroadcastManagerTest.php | 4 +++- tests/Integration/Support/ManagerTest.php | 7 +++++-- 7 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index a1c7fe25b285..80a5aa56c125 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -13,7 +13,6 @@ use InvalidArgumentException; use ReflectionException; use RuntimeException; -use Throwable; use function Illuminate\Support\enum_value; diff --git a/src/Illuminate/Support/MultipleInstanceManager.php b/src/Illuminate/Support/MultipleInstanceManager.php index fd041384588b..d5e49a13a8f6 100644 --- a/src/Illuminate/Support/MultipleInstanceManager.php +++ b/src/Illuminate/Support/MultipleInstanceManager.php @@ -6,7 +6,6 @@ use InvalidArgumentException; use ReflectionException; use RuntimeException; -use Throwable; abstract class MultipleInstanceManager { diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index dd5426c40e51..0e9cd3d8be80 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -283,7 +283,8 @@ enum GuardName: string case Secondary = 'secondary'; } -class CustomAuthDriver { +class CustomAuthDriver +{ public function __construct(private object $driver) { } diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index e916284d6ebb..a4f0eca03693 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -464,7 +464,9 @@ protected function getApp(array $userConfig) class CustomCacheDriver { - public function __construct(private object $driver) {} + public function __construct(private object $driver) + { + } public function __invoke() { diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 303daf8ce352..f20892f200eb 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -291,7 +291,9 @@ public function testInvokableObjectDriverClosure() class CustomFilesystemDriver { - public function __construct(private object $driver) {} + public function __construct(private object $driver) + { + } public function __invoke() { diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index 5932861e3756..6f33ac054fd9 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -310,7 +310,9 @@ public function broadcastOn() class CustomBroadcastDriver { - public function __construct(private object $driver) {} + public function __construct(private object $driver) + { + } public function __invoke() { diff --git a/tests/Integration/Support/ManagerTest.php b/tests/Integration/Support/ManagerTest.php index 798318e7a0d3..e119b778502f 100644 --- a/tests/Integration/Support/ManagerTest.php +++ b/tests/Integration/Support/ManagerTest.php @@ -43,8 +43,11 @@ public function testInvokableObjectDriverClosure() } } -class CustomDriver { - public function __construct(private object $object) {} +class CustomDriver +{ + public function __construct(private object $object) + { + } public function __invoke() { From 66884508c1a4e99060006ff2cfdd4500c9dbea3c Mon Sep 17 00:00:00 2001 From: Leonardo Yanes Batista Date: Tue, 14 Apr 2026 09:31:23 -0400 Subject: [PATCH 149/596] improve PHPDoc for "safe" method with conditional return type (#59684) --- src/Illuminate/Foundation/Http/FormRequest.php | 6 ++++-- src/Illuminate/Validation/Validator.php | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 415ba1db8ed7..606fbdb79124 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -336,8 +336,10 @@ protected function failedAuthorization() /** * Get a validated input container for the validated input. * - * @param array|null $keys - * @return \Illuminate\Support\ValidatedInput|array + * @param array|null $keys + * @return ($keys is array ? array : \Illuminate\Support\ValidatedInput) + * + * @throws \Illuminate\Validation\ValidationException */ public function safe(?array $keys = null) { diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index b8c3e6691336..36164df9d026 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -622,8 +622,10 @@ public function validateWithBag(string $errorBag) /** * Get a validated input container for the validated input. * - * @param array|null $keys - * @return \Illuminate\Support\ValidatedInput|array + * @param array|null $keys + * @return ($keys is array ? array : \Illuminate\Support\ValidatedInput) + * + * @throws \Illuminate\Validation\ValidationException */ public function safe(?array $keys = null) { From 42f12de645f9640af80857e12524f8d4b2f5a89b Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 14 Apr 2026 14:31:35 +0100 Subject: [PATCH 150/596] [13.x] Bump Retry action in CI (#59681) * bump retry * test in ci * Revert "test in ci" This reverts commit 47c25691c4db8b1de63fb8b67f075afa5a3282ce. --- .github/workflows/databases-nightly.yml | 4 ++-- .github/workflows/databases.yml | 18 +++++++++--------- .github/workflows/facades.yml | 2 +- .github/workflows/queues.yml | 6 +++--- .github/workflows/redis.yml | 6 +++--- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/databases-nightly.yml b/.github/workflows/databases-nightly.yml index de3252a2f3ff..b33176a5afb7 100644 --- a/.github/workflows/databases-nightly.yml +++ b/.github/workflows/databases-nightly.yml @@ -39,7 +39,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -85,7 +85,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 505d9459eb64..f97a2fa331a9 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -43,7 +43,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -90,7 +90,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -136,7 +136,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -183,7 +183,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -232,7 +232,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -281,7 +281,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -328,7 +328,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -376,7 +376,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -415,7 +415,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index 8add036db8b3..ef7e9c4b84f7 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -35,7 +35,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 6ac13d02aa99..f437cd52018e 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -32,7 +32,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -67,7 +67,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -119,7 +119,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/redis.yml b/.github/workflows/redis.yml index b7b9400cf4f3..d45e7dead357 100644 --- a/.github/workflows/redis.yml +++ b/.github/workflows/redis.yml @@ -41,7 +41,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -87,7 +87,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -104,7 +104,7 @@ jobs: redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 --cluster-replicas 0 --cluster-yes - name: Check Redis Cluster is ready - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_seconds: 5 max_attempts: 5 diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 214219a55c80..d66649d25584 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -33,7 +33,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a568584c770e..7a763d5dd183 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 @@ -128,7 +128,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 5 max_attempts: 5 From b89e2f1015f60c113d2f99a108786d695f260d0f Mon Sep 17 00:00:00 2001 From: "Kay W." Date: Tue, 14 Apr 2026 21:33:14 +0800 Subject: [PATCH 151/596] Move Scope interface @template from method-level to class-level to fix LSP violation (#59675) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous method-level @template on apply() meant every implementation had to accept any Model subtype (∀TModel), making it impossible for user-defined scopes to narrow the model type without violating the Liskov Substitution Principle. Moving the template to the interface level allows implementations to bind a specific model via @implements Scope. --- src/Illuminate/Database/Eloquent/Scope.php | 5 ++- .../Database/Eloquent/SoftDeletingScope.php | 12 ++---- types/Database/Eloquent/Scope.php | 37 +++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 types/Database/Eloquent/Scope.php diff --git a/src/Illuminate/Database/Eloquent/Scope.php b/src/Illuminate/Database/Eloquent/Scope.php index cfb1d9b97bc1..4eeeac0545b2 100644 --- a/src/Illuminate/Database/Eloquent/Scope.php +++ b/src/Illuminate/Database/Eloquent/Scope.php @@ -2,13 +2,14 @@ namespace Illuminate\Database\Eloquent; +/** + * @template TModel of \Illuminate\Database\Eloquent\Model + */ interface Scope { /** * Apply the scope to a given Eloquent query builder. * - * @template TModel of \Illuminate\Database\Eloquent\Model - * * @param \Illuminate\Database\Eloquent\Builder $builder * @param TModel $model * @return void diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index d1ef0d22b9b9..4b4a52552318 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -2,6 +2,9 @@ namespace Illuminate\Database\Eloquent; +/** + * @implements \Illuminate\Database\Eloquent\Scope<\Illuminate\Database\Eloquent\Model> + */ class SoftDeletingScope implements Scope { /** @@ -11,15 +14,6 @@ class SoftDeletingScope implements Scope */ protected $extensions = ['Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed']; - /** - * Apply the scope to a given Eloquent query builder. - * - * @template TModel of \Illuminate\Database\Eloquent\Model - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @param TModel $model - * @return void - */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); diff --git a/types/Database/Eloquent/Scope.php b/types/Database/Eloquent/Scope.php new file mode 100644 index 000000000000..b651be3a318c --- /dev/null +++ b/types/Database/Eloquent/Scope.php @@ -0,0 +1,37 @@ + + */ +class UserScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + assertType('Illuminate\Database\Eloquent\Builder', $builder); + assertType('Illuminate\Types\Scope\User', $model); + } +} + +/** + * @implements Scope + */ +class GenericScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + assertType('Illuminate\Database\Eloquent\Builder', $builder); + assertType('Illuminate\Database\Eloquent\Model', $model); + } +} + +class User extends Model +{ +} From a88b4f23799bebcb0bed74f575359185475696cd Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 15:50:02 +0200 Subject: [PATCH 152/596] Combine consecutive isset and unset (#59685) Co-authored-by: Lucas Michot --- pint.json | 2 ++ src/Illuminate/Foundation/Testing/TestCase.php | 6 ++---- src/Illuminate/Http/Client/PendingRequest.php | 2 +- tests/Database/DatabaseEloquentModelTest.php | 6 ++---- tests/Foundation/FoundationApplicationBuilderTest.php | 4 +--- tests/Integration/Console/CommandEventsTest.php | 3 +-- tests/Pipeline/PipelineTest.php | 3 +-- 7 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pint.json b/pint.json index c80a30c5c0f7..40596e008b7e 100644 --- a/pint.json +++ b/pint.json @@ -31,6 +31,8 @@ "class_definition": true, "class_reference_name_casing": true, "clean_namespace": true, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, "compact_nullable_type_declaration": true, "concat_space": true, "constant_case": { diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 7ab44a6e88d0..af46b18f171b 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -48,13 +48,11 @@ public function createApplication() $this->traitsUsedByTest = class_uses_recursive(static::class); - if (isset(CachedState::$cachedConfig) && - isset($this->traitsUsedByTest[WithCachedConfig::class])) { + if (isset(CachedState::$cachedConfig, $this->traitsUsedByTest[WithCachedConfig::class])) { $this->markConfigCached($app); } - if (isset(CachedState::$cachedRoutes) && - isset($this->traitsUsedByTest[WithCachedRoutes::class])) { + if (isset(CachedState::$cachedRoutes, $this->traitsUsedByTest[WithCachedRoutes::class])) { $app->booting(fn () => $this->markRoutesCached($app)); } diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index a6722db7d9ad..56eb19ba53e7 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1158,7 +1158,7 @@ protected function parseMultipartBodyFormat(array $data) ->flatMap(function ($value, $key) { if (is_array($value)) { // If the array has 'name' and 'contents' keys, it's already formatted for multipart... - if (isset($value['name']) && isset($value['contents'])) { + if (isset($value['name'], $value['contents'])) { return [$value]; } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 0f0e117bd877..e77d15e3690b 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -4165,8 +4165,7 @@ class EloquentModelBootingTestStub extends Model { public static function unboot() { - unset(static::$booted[static::class]); - unset(static::$bootedCallbacks[static::class]); + unset(static::$booted[static::class], static::$bootedCallbacks[static::class]); } public static function isBooted() @@ -4756,8 +4755,7 @@ class EloquentModelBootingCallbackTestStub extends Model public static function unboot() { - unset(static::$booted[static::class]); - unset(static::$bootedCallbacks[static::class]); + unset(static::$booted[static::class], static::$bootedCallbacks[static::class]); static::$bootHasFinished = false; } } diff --git a/tests/Foundation/FoundationApplicationBuilderTest.php b/tests/Foundation/FoundationApplicationBuilderTest.php index 267d7e8873ad..5ae2c5886b60 100644 --- a/tests/Foundation/FoundationApplicationBuilderTest.php +++ b/tests/Foundation/FoundationApplicationBuilderTest.php @@ -9,9 +9,7 @@ class FoundationApplicationBuilderTest extends TestCase { protected function tearDown(): void { - unset($_ENV['APP_BASE_PATH']); - - unset($_ENV['LARAVEL_STORAGE_PATH'], $_SERVER['LARAVEL_STORAGE_PATH']); + unset($_ENV['APP_BASE_PATH'], $_ENV['LARAVEL_STORAGE_PATH'], $_SERVER['LARAVEL_STORAGE_PATH']); parent::tearDown(); } diff --git a/tests/Integration/Console/CommandEventsTest.php b/tests/Integration/Console/CommandEventsTest.php index 175f765eefa7..3281789e5404 100644 --- a/tests/Integration/Console/CommandEventsTest.php +++ b/tests/Integration/Console/CommandEventsTest.php @@ -42,8 +42,7 @@ protected function setUp(): void $this->beforeApplicationDestroyed(function () { $this->files->delete($this->logfile); - unset($this->files); - unset($this->logfile); + unset($this->files, $this->logfile); }); parent::setUp(); diff --git a/tests/Pipeline/PipelineTest.php b/tests/Pipeline/PipelineTest.php index 1c95aaf867c9..ebf22a2e14d9 100644 --- a/tests/Pipeline/PipelineTest.php +++ b/tests/Pipeline/PipelineTest.php @@ -210,8 +210,7 @@ public function testThenMethodInputValue() $this->assertSame('pipe::then::not_foo::', $result); $this->assertSame('::not_foo::', $_SERVER['__test.then.arg']); - unset($_SERVER['__test.then.arg']); - unset($_SERVER['__test.pipe.return']); + unset($_SERVER['__test.then.arg'], $_SERVER['__test.pipe.return']); } public function testPipelineUsageWithParameters() From 3eda20ca566e646e8578f437347131f0dddd129a Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 15:54:04 +0200 Subject: [PATCH 153/596] Changes strlen comparison to 0 to direct empty string compare (#59686) Co-authored-by: Lucas Michot --- rector.php | 4 ++++ src/Illuminate/Console/Concerns/ConfiguresPrompts.php | 2 +- .../Database/Eloquent/Concerns/TransformsToResource.php | 2 +- src/Illuminate/Database/Schema/SqliteSchemaState.php | 2 +- src/Illuminate/Hashing/AbstractHasher.php | 2 +- src/Illuminate/Hashing/Argon2IdHasher.php | 2 +- src/Illuminate/Hashing/ArgonHasher.php | 2 +- src/Illuminate/Hashing/BcryptHasher.php | 2 +- src/Illuminate/Process/FakeProcessDescription.php | 4 ++-- src/Illuminate/Routing/RouteParameterBinder.php | 2 +- tests/Integration/Console/PromptsAssertionTest.php | 6 +++--- 11 files changed, 17 insertions(+), 13 deletions(-) diff --git a/rector.php b/rector.php index 6c88c6a52c53..c519edb80d22 100644 --- a/rector.php +++ b/rector.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Rector\CodeQuality\Rector\Identical\StrlenZeroToIdenticalEmptyStringRector; use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector; @@ -81,6 +82,9 @@ ThisCallOnStaticMethodToStaticCallRector::class, 'tests/Foundation/fixtures/bad-syntax-strategy.php', ]) + ->withRules([ + StrlenZeroToIdenticalEmptyStringRector::class, + ]) ->withPreparedSets( deadCode: false, codeQuality: false, diff --git a/src/Illuminate/Console/Concerns/ConfiguresPrompts.php b/src/Illuminate/Console/Concerns/ConfiguresPrompts.php index f5ba37fa6ce4..8ab67efde6ab 100644 --- a/src/Illuminate/Console/Concerns/ConfiguresPrompts.php +++ b/src/Illuminate/Console/Concerns/ConfiguresPrompts.php @@ -141,7 +141,7 @@ protected function promptUntilValid($prompt, $required, $validate) $error = is_callable($validate) ? $validate($result) : $this->validatePrompt($result, $validate); - if (is_string($error) && strlen($error) > 0) { + if (is_string($error) && $error !== '') { $this->components->error($error); if ($this->laravel->runningUnitTests()) { diff --git a/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php b/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php index 6006c355099f..86fa6c641212 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php +++ b/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php @@ -71,7 +71,7 @@ public static function guessResourceName(): array $potentialResource = sprintf( '%s\\Http\\Resources\\%s%s', Str::before($modelClass, '\\Models'), - strlen($relativeNamespace) > 0 ? $relativeNamespace.'\\' : '', + (string) $relativeNamespace !== '' ? $relativeNamespace.'\\' : '', class_basename($modelClass) ); diff --git a/src/Illuminate/Database/Schema/SqliteSchemaState.php b/src/Illuminate/Database/Schema/SqliteSchemaState.php index 3d954a39de17..276894f4ac32 100644 --- a/src/Illuminate/Database/Schema/SqliteSchemaState.php +++ b/src/Illuminate/Database/Schema/SqliteSchemaState.php @@ -46,7 +46,7 @@ protected function appendMigrationData(string $path) ])); $migrations = (new Collection(preg_split("/\r\n|\n|\r/", $process->getOutput()))) - ->filter(fn ($line) => preg_match('/^\s*(--|INSERT\s)/iu', $line) === 1 && strlen($line) > 0) + ->filter(fn ($line) => preg_match('/^\s*(--|INSERT\s)/iu', $line) === 1 && $line !== '') ->all(); $this->files->append($path, implode(PHP_EOL, $migrations).PHP_EOL); diff --git a/src/Illuminate/Hashing/AbstractHasher.php b/src/Illuminate/Hashing/AbstractHasher.php index 08151424bf36..6251b6cee072 100644 --- a/src/Illuminate/Hashing/AbstractHasher.php +++ b/src/Illuminate/Hashing/AbstractHasher.php @@ -25,7 +25,7 @@ public function info($hashedValue) */ public function check(#[\SensitiveParameter] $value, $hashedValue, array $options = []) { - if (is_null($hashedValue) || strlen($hashedValue) === 0) { + if (is_null($hashedValue) || (string) $hashedValue === '') { return false; } diff --git a/src/Illuminate/Hashing/Argon2IdHasher.php b/src/Illuminate/Hashing/Argon2IdHasher.php index 55601882c2e6..a7efdd45a842 100644 --- a/src/Illuminate/Hashing/Argon2IdHasher.php +++ b/src/Illuminate/Hashing/Argon2IdHasher.php @@ -18,7 +18,7 @@ class Argon2IdHasher extends ArgonHasher */ public function check(#[\SensitiveParameter] $value, $hashedValue, array $options = []) { - if (is_null($hashedValue) || strlen($hashedValue) === 0) { + if (is_null($hashedValue) || (string) $hashedValue === '') { return false; } diff --git a/src/Illuminate/Hashing/ArgonHasher.php b/src/Illuminate/Hashing/ArgonHasher.php index 74ff9301ed7b..1b14371988cc 100644 --- a/src/Illuminate/Hashing/ArgonHasher.php +++ b/src/Illuminate/Hashing/ArgonHasher.php @@ -95,7 +95,7 @@ protected function algorithm() */ public function check(#[\SensitiveParameter] $value, $hashedValue, array $options = []) { - if (is_null($hashedValue) || strlen($hashedValue) === 0) { + if (is_null($hashedValue) || (string) $hashedValue === '') { return false; } diff --git a/src/Illuminate/Hashing/BcryptHasher.php b/src/Illuminate/Hashing/BcryptHasher.php index 32e0f2090714..af40f159cc2f 100755 --- a/src/Illuminate/Hashing/BcryptHasher.php +++ b/src/Illuminate/Hashing/BcryptHasher.php @@ -81,7 +81,7 @@ public function make(#[\SensitiveParameter] $value, array $options = []) */ public function check(#[\SensitiveParameter] $value, $hashedValue, array $options = []) { - if (is_null($hashedValue) || strlen($hashedValue) === 0) { + if (is_null($hashedValue) || (string) $hashedValue === '') { return false; } diff --git a/src/Illuminate/Process/FakeProcessDescription.php b/src/Illuminate/Process/FakeProcessDescription.php index 1c397176eafb..fc2f8c3a462a 100644 --- a/src/Illuminate/Process/FakeProcessDescription.php +++ b/src/Illuminate/Process/FakeProcessDescription.php @@ -99,7 +99,7 @@ public function replaceOutput(string $output) ->values() ->all(); - if (strlen($output) > 0) { + if ($output !== '') { $this->output[] = [ 'type' => 'out', 'buffer' => rtrim($output, "\n")."\n", @@ -122,7 +122,7 @@ public function replaceErrorOutput(string $output) ->values() ->all(); - if (strlen($output) > 0) { + if ($output !== '') { $this->output[] = [ 'type' => 'err', 'buffer' => rtrim($output, "\n")."\n", diff --git a/src/Illuminate/Routing/RouteParameterBinder.php b/src/Illuminate/Routing/RouteParameterBinder.php index 5c53e5c786e3..d3ed0782fce3 100644 --- a/src/Illuminate/Routing/RouteParameterBinder.php +++ b/src/Illuminate/Routing/RouteParameterBinder.php @@ -89,7 +89,7 @@ protected function matchToKeys(array $matches) $parameters = array_intersect_key($matches, array_flip($parameterNames)); return array_filter($parameters, function ($value) { - return is_string($value) && strlen($value) > 0; + return is_string($value) && $value !== ''; }); } diff --git a/tests/Integration/Console/PromptsAssertionTest.php b/tests/Integration/Console/PromptsAssertionTest.php index 27fdb6995c52..eae7e22428f4 100644 --- a/tests/Integration/Console/PromptsAssertionTest.php +++ b/tests/Integration/Console/PromptsAssertionTest.php @@ -311,7 +311,7 @@ public function handle() $name = search( label: 'What is your name?', - options: fn (string $value) => strlen($value) > 0 + options: fn (string $value) => $value !== '' ? $options->filter(fn ($title) => str_contains($title, $value))->values()->toArray() : [] ); @@ -340,7 +340,7 @@ public function handle() $names = multisearch( label: 'Which names do you like?', - options: fn (string $value) => strlen($value) > 0 + options: fn (string $value) => $value !== '' ? $options->filter(fn ($title) => str_contains($title, $value))->values()->toArray() : [] ); @@ -382,7 +382,7 @@ public function handle() $titles = collect(['Mr', 'Mrs', 'Ms', 'Dr']); $title = multisearch( label: 'What is your title?', - options: fn (string $value) => strlen($value) > 0 + options: fn (string $value) => $value !== '' ? $titles->filter(fn ($title) => str_contains($title, $value))->values()->toArray() : [] ); From ffa1850049a691b93129808f27ecd10e65c9d1a5 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:55:03 +0000 Subject: [PATCH 154/596] Update version to v13.5.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index fdc3cdebfbd1..e219d391b4dc 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 = '13.4.0'; + const VERSION = '13.5.0'; /** * The base path for the Laravel installation. From 330608c59b9315c492683ce822fb38e41db45e7e Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:56:45 +0000 Subject: [PATCH 155/596] Update CHANGELOG --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa99e7b31e45..7a07f060a8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,35 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.4.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.5.0...13.x) + +## [v13.5.0](https://github.com/laravel/framework/compare/v13.4.0...v13.5.0) - 2026-04-14 + +* [13.x] Support #[Delay] attribute on queued mailables by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59580 +* [13.x] Added inheritance support for Controller Middleware attributes. by [@niduranga](https://github.com/niduranga) in https://github.com/laravel/framework/pull/59597 +* [13.x] Normalize phpredis SSL context for single and cluster connections by [@timmylindh](https://github.com/timmylindh) in https://github.com/laravel/framework/pull/59569 +* [13.x] Memoize the result of `TestCase@withoutBootingFramework()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59610 +* [13.x] Add missing [@throws](https://github.com/throws) and docblocks for concurrency and model in… by [@scabarcas17](https://github.com/scabarcas17) in https://github.com/laravel/framework/pull/59602 +* [13.x] Fix that retries of `ShouldBeUniqueUntilProcessing` jobs are force-releasing locks they don't own by [@kohlerdominik](https://github.com/kohlerdominik) in https://github.com/laravel/framework/pull/59567 +* [13.x] Add first-class Redis Cluster support for Queue and ConcurrencyLimiter by [@timmylindh](https://github.com/timmylindh) in https://github.com/laravel/framework/pull/59533 +* [13.x] chore: Update PHP version from 8.2 to 8.3 in `bin/test.sh` script by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/59605 +* [13.x] Fix RedisQueueTest by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59613 +* [13.x] Add enum support to CacheManager store and driver methods by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/59637 +* [13.x] Fix redirectUsersTo() overwriting redirectGuestsTo() callback by [@timmylindh](https://github.com/timmylindh) in https://github.com/laravel/framework/pull/59633 +* [13.x] Add ability to detect unserializable values returned from cache by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59630 +* [13.x] Fix loose comparison false positive in NotPwnedVerifier with magic hash passwords by [@scabarcas17](https://github.com/scabarcas17) in https://github.com/laravel/framework/pull/59644 +* [13.x] Refactor `Skip` middleware by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59651 +* [13.x] Resolve stan errors on MySqlSchemaState by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59652 +* [13.x] Allow closure values in updateOrCreate and firstOrNew by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/59647 +* [13.x] Add enum support to MailManager mailer and driver methods by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/59645 +* [13.x] Add enum support to AuthManager guard and shouldUse methods by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/59646 +* [13.x] Add spatie/fork to composer suggestions by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59660 +* [13.x] Add enum support to Manager driver method by [@scabarcas17](https://github.com/scabarcas17) in https://github.com/laravel/framework/pull/59659 +* [13.x] Fix custom driver binding bug and improve by [@ollieread](https://github.com/ollieread) in https://github.com/laravel/framework/pull/59614 +* [13.x] Improve PHPDoc for "safe" method with conditional return type by [@leo95batista](https://github.com/leo95batista) in https://github.com/laravel/framework/pull/59684 +* [13.x] Bump Retry action in CI by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59681 +* [13.x] Move Scope interface [@template](https://github.com/template) from method-level to class-level to fix LSP violation by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/59675 +* [13.x] Combine consecutive `isset` and `unset` by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59685 +* [13.x] Changes `strlen` comparison to 0 to direct empty string compare by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59686 ## [v13.4.0](https://github.com/laravel/framework/compare/v13.3.0...v13.4.0) - 2026-04-07 From 06e994e9695fdbb854d3403e67251da2635b7c09 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 19:44:11 +0200 Subject: [PATCH 156/596] use version_compare function (#59687) Co-authored-by: Lucas Michot --- tests/Integration/Database/MySql/JoinLateralTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Database/MySql/JoinLateralTest.php b/tests/Integration/Database/MySql/JoinLateralTest.php index 2dce04b8f5d0..87548ddcc30a 100644 --- a/tests/Integration/Database/MySql/JoinLateralTest.php +++ b/tests/Integration/Database/MySql/JoinLateralTest.php @@ -58,7 +58,7 @@ protected function checkMySqlVersion() if (str_contains($mySqlVersion, 'Maria')) { $this->markTestSkipped('Lateral joins are not supported on MariaDB'.__CLASS__); - } elseif ((float) $mySqlVersion < '8.0.14') { + } elseif (version_compare($mySqlVersion, '8.0.14', '<')) { $this->markTestSkipped('Lateral joins are not supported on MySQL < 8.0.14'.__CLASS__); } } From dcebf75f9c113cc2b3eb249b87703dd72d9b575d Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 19:44:34 +0200 Subject: [PATCH 157/596] Flip misordered assertions arguments (#59691) Co-authored-by: Lucas Michot --- tests/Config/RepositoryTest.php | 8 +- .../ContainerResolveNonInstantiableTest.php | 2 +- .../Database/DatabaseEloquentFactoryTest.php | 16 ++-- ...eEloquentHasManyThroughIntegrationTest.php | 2 +- ...seEloquentHasOneThroughIntegrationTest.php | 2 +- .../DatabaseEloquentIntegrationTest.php | 18 ++--- tests/Database/DatabaseEloquentModelTest.php | 4 +- .../Database/DatabaseMigrationCreatorTest.php | 2 +- tests/Filesystem/FilesystemAdapterTest.php | 2 +- tests/Filesystem/FilesystemManagerTest.php | 2 +- .../Foundation/FoundationDocsCommandTest.php | 38 ++++----- tests/Http/HttpRequestTest.php | 4 +- tests/Http/Middleware/TrustProxiesTest.php | 10 +-- tests/Integration/Cache/MemoizedStoreTest.php | 8 +- .../Generators/ProviderMakeCommandTest.php | 4 +- .../Integration/Routing/HasMiddlewareTest.php | 4 +- tests/Log/ContextTest.php | 20 ++--- tests/Log/LogManagerTest.php | 6 +- tests/Pagination/CursorPaginatorTest.php | 2 +- tests/Routing/RouteRegistrarTest.php | 78 +++++++++---------- tests/Support/SleepTest.php | 32 ++++---- tests/Support/SupportHelpersTest.php | 4 +- tests/Validation/ValidationAddFailureTest.php | 4 +- .../Validation/ValidationPasswordRuleTest.php | 8 +- tests/Validation/ValidationRuleParserTest.php | 2 +- tests/Validation/ValidationValidatorTest.php | 4 +- tests/Validation/ValidatorAfterRuleTest.php | 4 +- .../Blade/BladeComponentTagCompilerTest.php | 8 +- tests/View/Blade/BladePropsTest.php | 8 +- tests/View/ViewFactoryTest.php | 2 +- 30 files changed, 154 insertions(+), 154 deletions(-) diff --git a/tests/Config/RepositoryTest.php b/tests/Config/RepositoryTest.php index cf59e78b0c49..4ea2d3b128cd 100644 --- a/tests/Config/RepositoryTest.php +++ b/tests/Config/RepositoryTest.php @@ -52,7 +52,7 @@ protected function setUp(): void public function testGetValueWhenKeyContainDot() { $this->assertSame( - $this->repository->get('a.b'), 'c' + 'c', $this->repository->get('a.b') ); $this->assertNull( $this->repository->get('a.b.c') @@ -308,7 +308,7 @@ public function testItThrowsAnExceptionWhenTryingToGetNonStringValueAsString(): public function testItGetsAsArray(): void { $this->assertSame( - $this->repository->array('array'), ['aaa', 'zzz'] + ['aaa', 'zzz'], $this->repository->array('array') ); } @@ -346,7 +346,7 @@ public function testItThrowsAnExceptionWhenTryingToGetNonBooleanValueAsBoolean() public function testItGetsAsInteger(): void { $this->assertSame( - $this->repository->integer('integer'), 1 + 1, $this->repository->integer('integer') ); } @@ -361,7 +361,7 @@ public function testItThrowsAnExceptionWhenTryingToGetNonIntegerValueAsInteger() public function testItGetsAsFloat(): void { $this->assertSame( - $this->repository->float('float'), 1.1 + 1.1, $this->repository->float('float') ); } diff --git a/tests/Container/ContainerResolveNonInstantiableTest.php b/tests/Container/ContainerResolveNonInstantiableTest.php index 5d568cb10e8e..0b42cf09add8 100644 --- a/tests/Container/ContainerResolveNonInstantiableTest.php +++ b/tests/Container/ContainerResolveNonInstantiableTest.php @@ -29,7 +29,7 @@ public function testResolveVariadicPrimitive() $container = new Container; $parent = $container->make(VariadicPrimitive::class); - $this->assertSame($parent->params, []); + $this->assertSame([], $parent->params); } } diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index 2cb37c377bb8..b995a58868d5 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -628,7 +628,6 @@ public function test_sequence_with_has_many_relationship() $this->assertCount(6, FactoryTestPost::all()); $this->assertCount(3, FactoryTestUser::latest()->first()->posts); $this->assertEquals( - FactoryTestPost::orderBy('title')->pluck('title')->all(), [ 'Abigail Otwell Post 1', 'Abigail Otwell Post 2', @@ -636,7 +635,8 @@ public function test_sequence_with_has_many_relationship() 'Taylor Otwell Post 1', 'Taylor Otwell Post 2', 'Taylor Otwell Post 3', - ] + ], + FactoryTestPost::orderBy('title')->pluck('title')->all() ); } @@ -999,8 +999,8 @@ public function test_can_default_to_without_parents() public function test_factory_model_names_correct() { - $this->assertEquals(FactoryTestUseFactoryAttribute::factory()->modelName(), FactoryTestUseFactoryAttribute::class); - $this->assertEquals(FactoryTestGuessModel::factory()->modelName(), FactoryTestGuessModel::class); + $this->assertEquals(FactoryTestUseFactoryAttribute::class, FactoryTestUseFactoryAttribute::factory()->modelName()); + $this->assertEquals(FactoryTestGuessModel::class, FactoryTestGuessModel::factory()->modelName()); } public function test_factory_global_model_resolver() @@ -1009,11 +1009,11 @@ public function test_factory_global_model_resolver() return __NAMESPACE__.'\\'.Str::replaceLast('Factory', '', class_basename($factory::class)); }); - $this->assertEquals(FactoryTestGuessModel::factory()->modelName(), FactoryTestGuessModel::class); - $this->assertEquals(FactoryTestUseFactoryAttribute::factory()->modelName(), FactoryTestUseFactoryAttribute::class); + $this->assertEquals(FactoryTestGuessModel::class, FactoryTestGuessModel::factory()->modelName()); + $this->assertEquals(FactoryTestUseFactoryAttribute::class, FactoryTestUseFactoryAttribute::factory()->modelName()); - $this->assertEquals(FactoryTestUseFactoryAttributeFactory::new()->modelName(), FactoryTestUseFactoryAttribute::class); - $this->assertEquals(FactoryTestGuessModelFactory::new()->modelName(), FactoryTestGuessModel::class); + $this->assertEquals(FactoryTestUseFactoryAttribute::class, FactoryTestUseFactoryAttributeFactory::new()->modelName()); + $this->assertEquals(FactoryTestGuessModel::class, FactoryTestGuessModelFactory::new()->modelName()); } public function test_factory_model_has_many_relationship_has_pending_attributes() diff --git a/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php b/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php index 45dbd67e817a..327c0f6de546 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php @@ -132,7 +132,7 @@ public function testWithWhereHasOnARelationWithCustomIntermediateAndLocalKey() $this->assertCount(1, $country); $this->assertTrue($country->first()->relationLoaded('posts')); - $this->assertEquals($country->first()->posts->pluck('title')->unique()->toArray(), ['A title']); + $this->assertEquals(['A title'], $country->first()->posts->pluck('title')->unique()->toArray()); } public function testFindMethod() diff --git a/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php b/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php index 5cf6bcb0b1be..677695827d43 100644 --- a/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php +++ b/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php @@ -127,7 +127,7 @@ public function testWithWhereHasOnARelationWithCustomIntermediateAndLocalKey() $this->assertCount(1, $position); $this->assertTrue($position->first()->relationLoaded('contract')); - $this->assertEquals($position->first()->contract->pluck('title')->unique()->toArray(), ['A title']); + $this->assertEquals(['A title'], $position->first()->contract->pluck('title')->unique()->toArray()); } public function testFirstOrFailThrowsAnException() diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 77b45f3ec565..5560e2fa2db9 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -1205,7 +1205,7 @@ public function testWithWhereHasOnSelfReferencingBelongsToManyRelationship() $this->assertCount(1, $results); $this->assertSame('taylorotwell@gmail.com', $results->first()->email); $this->assertTrue($results->first()->relationLoaded('friends')); - $this->assertSame($results->first()->friends->pluck('email')->unique()->toArray(), ['abigailotwell@gmail.com']); + $this->assertSame(['abigailotwell@gmail.com'], $results->first()->friends->pluck('email')->unique()->toArray()); } public function testHasOnNestedSelfReferencingBelongsToManyRelationship() @@ -1247,8 +1247,8 @@ public function testWithWhereHasOnNestedSelfReferencingBelongsToManyRelationship $this->assertCount(1, $results); $this->assertSame('taylorotwell@gmail.com', $results->first()->email); $this->assertTrue($results->first()->relationLoaded('friends')); - $this->assertSame($results->first()->friends->pluck('email')->unique()->toArray(), ['abigailotwell@gmail.com']); - $this->assertSame($results->first()->friends->pluck('friends')->flatten()->pluck('email')->unique()->toArray(), ['foo@gmail.com']); + $this->assertSame(['abigailotwell@gmail.com'], $results->first()->friends->pluck('email')->unique()->toArray()); + $this->assertSame(['foo@gmail.com'], $results->first()->friends->pluck('friends')->flatten()->pluck('email')->unique()->toArray()); } public function testHasOnSelfReferencingBelongsToManyRelationshipWithWherePivot() @@ -1321,7 +1321,7 @@ public function testWithWhereHasOnSelfReferencingBelongsToRelationship() $this->assertCount(1, $results); $this->assertSame('Child Post', $results->first()->name); $this->assertTrue($results->first()->relationLoaded('parentPost')); - $this->assertSame($results->first()->parentPost->name, 'Parent Post'); + $this->assertSame('Parent Post', $results->first()->parentPost->name); } public function testHasOnNestedSelfReferencingBelongsToRelationship() @@ -1363,9 +1363,9 @@ public function testWithWhereHasOnNestedSelfReferencingBelongsToRelationship() $this->assertCount(1, $results); $this->assertSame('Child Post', $results->first()->name); $this->assertTrue($results->first()->relationLoaded('parentPost')); - $this->assertSame($results->first()->parentPost->name, 'Parent Post'); + $this->assertSame('Parent Post', $results->first()->parentPost->name); $this->assertTrue($results->first()->parentPost->relationLoaded('parentPost')); - $this->assertSame($results->first()->parentPost->parentPost->name, 'Grandparent Post'); + $this->assertSame('Grandparent Post', $results->first()->parentPost->parentPost->name); } public function testHasOnSelfReferencingHasManyRelationship() @@ -1404,7 +1404,7 @@ public function testWithWhereHasOnSelfReferencingHasManyRelationship() $this->assertCount(1, $results); $this->assertSame('Parent Post', $results->first()->name); $this->assertTrue($results->first()->relationLoaded('childPosts')); - $this->assertSame($results->first()->childPosts->pluck('name')->unique()->toArray(), ['Child Post']); + $this->assertSame(['Child Post'], $results->first()->childPosts->pluck('name')->unique()->toArray()); } public function testHasOnNestedSelfReferencingHasManyRelationship() @@ -1446,8 +1446,8 @@ public function testWithWhereHasOnNestedSelfReferencingHasManyRelationship() $this->assertCount(1, $results); $this->assertSame('Grandparent Post', $results->first()->name); $this->assertTrue($results->first()->relationLoaded('childPosts')); - $this->assertSame($results->first()->childPosts->pluck('name')->unique()->toArray(), ['Parent Post']); - $this->assertSame($results->first()->childPosts->pluck('childPosts')->flatten()->pluck('name')->unique()->toArray(), ['Child Post']); + $this->assertSame(['Parent Post'], $results->first()->childPosts->pluck('name')->unique()->toArray()); + $this->assertSame(['Child Post'], $results->first()->childPosts->pluck('childPosts')->flatten()->pluck('name')->unique()->toArray()); } public function testHasWithNonWhereBindings() diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index e77d15e3690b..4aa3342fbac8 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3138,8 +3138,8 @@ public function testMergeCastsMergesCastsUsingArrays() $this->assertCount($castCount + 2, $model->getCasts()); $this->assertArrayHasKey('foo', $model->getCasts()); - $this->assertEquals($model->getCasts()['foo'], 'MyClass:myArgumentA'); - $this->assertEquals($model->getCasts()['bar'], 'MyClass:myArgumentA,myArgumentB'); + $this->assertEquals('MyClass:myArgumentA', $model->getCasts()['foo']); + $this->assertEquals('MyClass:myArgumentA,myArgumentB', $model->getCasts()['bar']); } public function testUnsetCastAttributes() diff --git a/tests/Database/DatabaseMigrationCreatorTest.php b/tests/Database/DatabaseMigrationCreatorTest.php index e1c380e92f4c..a558110953c3 100755 --- a/tests/Database/DatabaseMigrationCreatorTest.php +++ b/tests/Database/DatabaseMigrationCreatorTest.php @@ -47,7 +47,7 @@ public function testBasicCreateMethodCallsPostCreateHooks() $creator->create('create_bar', 'foo', $table); $this->assertEquals($_SERVER['__migration.creator.table'], $table); - $this->assertEquals($_SERVER['__migration.creator.path'], 'foo/foo_create_bar.php'); + $this->assertEquals('foo/foo_create_bar.php', $_SERVER['__migration.creator.path']); unset($_SERVER['__migration.creator.table'], $_SERVER['__migration.creator.path']); } diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 001dca2345cb..1b0bcb892543 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -677,7 +677,7 @@ public function testGetAllFiles() $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $this->assertSame($filesystemAdapter->files(), ['body.txt', 'existing.txt', 'file.txt', 'file1.txt']); + $this->assertSame(['body.txt', 'existing.txt', 'file.txt', 'file1.txt'], $filesystemAdapter->files()); } public function testProvidesTemporaryUrls() diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index f20892f200eb..501db8a5bdcd 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -209,7 +209,7 @@ public function testCanBuildInlineScopedDisks() $scoped->put('dirname/filename.txt', 'file content'); $this->assertTrue(is_dir(__DIR__.'/../../to-be-scoped/path-prefix')); - $this->assertEquals(file_get_contents(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt'), 'file content'); + $this->assertEquals('file content', file_get_contents(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt')); } finally { unlink(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt'); rmdir(__DIR__.'/../../to-be-scoped/path-prefix/dirname'); diff --git a/tests/Foundation/FoundationDocsCommandTest.php b/tests/Foundation/FoundationDocsCommandTest.php index d00c55f754b6..6634178adbe5 100644 --- a/tests/Foundation/FoundationDocsCommandTest.php +++ b/tests/Foundation/FoundationDocsCommandTest.php @@ -51,7 +51,7 @@ public function testItCanOpenTheLaravelDocumentation(): void ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/installation') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/installation'); + $this->assertSame('https://laravel.com/docs/8.x/installation', $this->openedUrl); } public function testItCanSpecifyAutocompleteInOriginalCasing(): void @@ -61,7 +61,7 @@ public function testItCanSpecifyAutocompleteInOriginalCasing(): void ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/dusk') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/dusk'); + $this->assertSame('https://laravel.com/docs/8.x/dusk', $this->openedUrl); } public function testItCanSpecifyAutocompleteInLowerCasing(): void @@ -71,7 +71,7 @@ public function testItCanSpecifyAutocompleteInLowerCasing(): void ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/dusk') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/dusk'); + $this->assertSame('https://laravel.com/docs/8.x/dusk', $this->openedUrl); } public function testItMatchesSectionsThatStartWithInput() @@ -80,7 +80,7 @@ public function testItMatchesSectionsThatStartWithInput() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent-collections#method-unique') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent-collections#method-unique'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent-collections#method-unique', $this->openedUrl); } public function testItMatchesSectionsWithFuzzyMatching() @@ -89,7 +89,7 @@ public function testItMatchesSectionsWithFuzzyMatching() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent-collections#method-toquery') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent-collections#method-toquery'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent-collections#method-toquery', $this->openedUrl); } public function testItCanProvidePageToVisit(): void @@ -98,7 +98,7 @@ public function testItCanProvidePageToVisit(): void ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent-collections') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent-collections'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent-collections', $this->openedUrl); } public function testItCanUseHyphensInsteadOfEscapingSpaces(): void @@ -107,7 +107,7 @@ public function testItCanUseHyphensInsteadOfEscapingSpaces(): void ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent-collections') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent-collections'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent-collections', $this->openedUrl); } public function testItHasMinimumScoreToMatch(): void @@ -116,7 +116,7 @@ public function testItHasMinimumScoreToMatch(): void ->expectsOutputToContain('Unable to determine the page you are trying to visit.') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x'); + $this->assertSame('https://laravel.com/docs/8.x', $this->openedUrl); } public function testItMinimumScoreAccountsForInputLength(): void @@ -125,7 +125,7 @@ public function testItMinimumScoreAccountsForInputLength(): void ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/localization') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/localization'); + $this->assertSame('https://laravel.com/docs/8.x/localization', $this->openedUrl); } public function testItCanUseCustomAskStrategy() @@ -136,7 +136,7 @@ public function testItCanUseCustomAskStrategy() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/dusk') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/dusk'); + $this->assertSame('https://laravel.com/docs/8.x/dusk', $this->openedUrl); } public function testItFallsbackToAutocompleteWhenAskStrategyContainsBadSyntax(): void @@ -148,7 +148,7 @@ public function testItFallsbackToAutocompleteWhenAskStrategyContainsBadSyntax(): ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/dusk') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/dusk'); + $this->assertSame('https://laravel.com/docs/8.x/dusk', $this->openedUrl); } public function testItFallsbackToAutocompleteWithBadAskStrategyReturnValue(): void @@ -160,7 +160,7 @@ public function testItFallsbackToAutocompleteWithBadAskStrategyReturnValue(): vo ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/dusk') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/dusk'); + $this->assertSame('https://laravel.com/docs/8.x/dusk', $this->openedUrl); } public function testItCatchesAndHandlesProcessInterruptExceptionsInAskStrategies() @@ -209,7 +209,7 @@ public function testItCanGuessTheRequestedPageWhenItIsTheStartOfAPageTitle() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent', $this->openedUrl); } public function testItCanGuessTheRequestedPageWhenItIsContainedSomewhereInThePageTitle() @@ -218,7 +218,7 @@ public function testItCanGuessTheRequestedPageWhenItIsContainedSomewhereInThePag ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent', $this->openedUrl); } public function testItCanGuessTheWithTopAndTailMatching() @@ -227,7 +227,7 @@ public function testItCanGuessTheWithTopAndTailMatching() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/eloquent-collections') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/eloquent-collections'); + $this->assertSame('https://laravel.com/docs/8.x/eloquent-collections', $this->openedUrl); } public function testItCanSpecifyCustomOpenCommandsViaEnvVariables() @@ -282,7 +282,7 @@ public function testItCanPerformSearchAgainstLaravelDotCom() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x?q=here%20is%20my%20search%20term%20for%20the%20laravel%20website') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x?q=here%20is%20my%20search%20term%20for%20the%20laravel%20website'); + $this->assertSame('https://laravel.com/docs/8.x?q=here%20is%20my%20search%20term%20for%20the%20laravel%20website', $this->openedUrl); $_SERVER['argv'] = $argCache; } @@ -302,7 +302,7 @@ public function testGuessedMatchesThatDirectlyContainTheGivenStringRankHigherTha ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/filesystem') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/filesystem'); + $this->assertSame('https://laravel.com/docs/8.x/filesystem', $this->openedUrl); } public function testItHandlesPoorSpelling() @@ -311,7 +311,7 @@ public function testItHandlesPoorSpelling() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x/views') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x/views'); + $this->assertSame('https://laravel.com/docs/8.x/views', $this->openedUrl); } public function testItHandlesNoInteractionOption() @@ -320,7 +320,7 @@ public function testItHandlesNoInteractionOption() ->expectsOutputToContain('Opening the docs to: https://laravel.com/docs/8.x') ->assertSuccessful(); - $this->assertSame($this->openedUrl, 'https://laravel.com/docs/8.x'); + $this->assertSame('https://laravel.com/docs/8.x', $this->openedUrl); } public function testCanGetHelpWithoutInstantiatingDependencies() diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index d3b589fb68e5..0013113c3bc3 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -1764,12 +1764,12 @@ public function testNonJsonRequestDoesntFillRequestBodyParams() $params = ['foo' => 'bar']; $getRequest = Request::create('/', 'GET', $params, [], [], []); - $this->assertEquals($getRequest->request->all(), []); + $this->assertEquals([], $getRequest->request->all()); $this->assertEquals($getRequest->query->all(), $params); $postRequest = Request::create('/', 'POST', $params, [], [], []); $this->assertEquals($postRequest->request->all(), $params); - $this->assertEquals($postRequest->query->all(), []); + $this->assertEquals([], $postRequest->query->all()); } /** diff --git a/tests/Http/Middleware/TrustProxiesTest.php b/tests/Http/Middleware/TrustProxiesTest.php index 3d964fb06e9d..54f636f76755 100644 --- a/tests/Http/Middleware/TrustProxiesTest.php +++ b/tests/Http/Middleware/TrustProxiesTest.php @@ -346,27 +346,27 @@ public function test_is_reading_text_based_configurations() $this->assertEquals($request->getTrustedHeaderSet(), $this->headerAll, 'Assert trusted proxy used all "X-Forwarded-*" header'); - $this->assertEquals($request->getTrustedProxies(), ['192.168.1.1', '192.168.1.2'], + $this->assertEquals(['192.168.1.1', '192.168.1.2'], $request->getTrustedProxies(), 'Assert trusted proxy using proxies as string separated by comma.'); }); // or, if your proxy instead uses the "Forwarded" header $trustedProxy = $this->createTrustedProxy('HEADER_FORWARDED', '192.168.1.1, 192.168.1.2'); $trustedProxy->handle($request, function (Request $request) { - $this->assertEquals($request->getTrustedHeaderSet(), Request::HEADER_FORWARDED, + $this->assertEquals(Request::HEADER_FORWARDED, $request->getTrustedHeaderSet(), 'Assert trusted proxy used forwarded header'); - $this->assertEquals($request->getTrustedProxies(), ['192.168.1.1', '192.168.1.2'], + $this->assertEquals(['192.168.1.1', '192.168.1.2'], $request->getTrustedProxies(), 'Assert trusted proxy using proxies as string separated by comma.'); }); // or, if you're using AWS ELB $trustedProxy = $this->createTrustedProxy('HEADER_X_FORWARDED_AWS_ELB', '192.168.1.1, 192.168.1.2'); $trustedProxy->handle($request, function (Request $request) { - $this->assertEquals($request->getTrustedHeaderSet(), Request::HEADER_X_FORWARDED_AWS_ELB, + $this->assertEquals(Request::HEADER_X_FORWARDED_AWS_ELB, $request->getTrustedHeaderSet(), 'Assert trusted proxy used AWS ELB header'); - $this->assertEquals($request->getTrustedProxies(), ['192.168.1.1', '192.168.1.2'], + $this->assertEquals(['192.168.1.1', '192.168.1.2'], $request->getTrustedProxies(), 'Assert trusted proxy using proxies as string separated by comma.'); }); } diff --git a/tests/Integration/Cache/MemoizedStoreTest.php b/tests/Integration/Cache/MemoizedStoreTest.php index 264fa753db54..8cafe8ca84a5 100644 --- a/tests/Integration/Cache/MemoizedStoreTest.php +++ b/tests/Integration/Cache/MemoizedStoreTest.php @@ -143,16 +143,16 @@ 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']); - $this->assertSame($live, ['name.0' => null, 'name.1' => null]); - $this->assertSame($memoized, ['name.0' => null, 'name.1' => null]); + $this->assertSame(['name.0' => null, 'name.1' => null], $live); + $this->assertSame(['name.0' => null, 'name.1' => null], $memoized); Cache::put('name.0', 'MacDonald', 60); Cache::put('name.1', 'Otwell', 60); $live = Cache::getMultiple(['name.0', 'name.1']); $memoized = Cache::memo()->getMultiple(['name.0', 'name.1']); - $this->assertSame($live, ['name.0' => 'MacDonald', 'name.1' => 'Otwell']); - $this->assertSame($memoized, ['name.0' => null, 'name.1' => null]); + $this->assertSame(['name.0' => 'MacDonald', 'name.1' => 'Otwell'], $live); + $this->assertSame(['name.0' => null, 'name.1' => null], $memoized); } public function test_it_can_retrieve_already_memoized_and_not_yet_memoized_values_when_retrieving_multiple_values() diff --git a/tests/Integration/Generators/ProviderMakeCommandTest.php b/tests/Integration/Generators/ProviderMakeCommandTest.php index b42bba736b4d..16e5a0b0a4ea 100644 --- a/tests/Integration/Generators/ProviderMakeCommandTest.php +++ b/tests/Integration/Generators/ProviderMakeCommandTest.php @@ -21,8 +21,8 @@ public function testItCanGenerateServiceProviderFile() 'public function boot()', ], 'app/Providers/FooServiceProvider.php'); - $this->assertEquals(require $this->app->getBootstrapProvidersPath(), [ + $this->assertEquals([ 'App\Providers\FooServiceProvider', - ]); + ], require $this->app->getBootstrapProvidersPath()); } } diff --git a/tests/Integration/Routing/HasMiddlewareTest.php b/tests/Integration/Routing/HasMiddlewareTest.php index 89fa3d20752f..8e62493ac19b 100644 --- a/tests/Integration/Routing/HasMiddlewareTest.php +++ b/tests/Integration/Routing/HasMiddlewareTest.php @@ -12,10 +12,10 @@ class HasMiddlewareTest extends TestCase public function test_has_middleware_is_respected() { $route = Route::get('/', [HasMiddlewareTestController::class, 'index']); - $this->assertEquals($route->controllerMiddleware(), ['all', 'only-index']); + $this->assertEquals(['all', 'only-index'], $route->controllerMiddleware()); $route = Route::get('/', [HasMiddlewareTestController::class, 'show']); - $this->assertEquals($route->controllerMiddleware(), ['all', 'except-index']); + $this->assertEquals(['all', 'except-index'], $route->controllerMiddleware()); } } diff --git a/tests/Log/ContextTest.php b/tests/Log/ContextTest.php index 45a9fceca1e9..18506ca93863 100644 --- a/tests/Log/ContextTest.php +++ b/tests/Log/ContextTest.php @@ -165,17 +165,17 @@ public function test_it_can_serialize_values() Context::hydrate($dehydrated); - $this->assertSame(Context::get('string'), 'string'); - $this->assertSame(Context::get('bool'), false); - $this->assertSame(Context::get('int'), 5); - $this->assertSame(Context::get('float'), 5.5); - $this->assertSame(Context::get('null'), null); - $this->assertSame(Context::get('array'), [1, 2, 3]); - $this->assertSame(Context::get('hash'), ['foo' => 'bar']); + $this->assertSame('string', Context::get('string')); + $this->assertSame(false, Context::get('bool')); + $this->assertSame(5, Context::get('int')); + $this->assertSame(5.5, Context::get('float')); + $this->assertSame(null, Context::get('null')); + $this->assertSame([1, 2, 3], Context::get('array')); + $this->assertSame(['foo' => 'bar'], Context::get('hash')); $this->assertEquals(Context::get('object'), (object) ['foo' => 'bar']); - $this->assertSame(Context::get('enum'), Suit::Clubs); - $this->assertSame(Context::get('backed_enum'), StringBackedSuit::Clubs); - $this->assertSame(Context::getHidden('number'), 55); + $this->assertSame(Suit::Clubs, Context::get('enum')); + $this->assertSame(StringBackedSuit::Clubs, Context::get('backed_enum')); + $this->assertSame(55, Context::getHidden('number')); } public function test_it_can_push_to_list() diff --git a/tests/Log/LogManagerTest.php b/tests/Log/LogManagerTest.php index fc195468ee04..3345328a73d2 100755 --- a/tests/Log/LogManagerTest.php +++ b/tests/Log/LogManagerTest.php @@ -111,8 +111,8 @@ public function testParsingStackChannels() $manager->channel('stack'); $this->assertSame( - array_keys($manager->getChannels()), - ['single', 'daily', 'stderr', 'stack'] + ['single', 'daily', 'stderr', 'stack'], + array_keys($manager->getChannels()) ); } @@ -646,7 +646,7 @@ public function testContextCanBePubliclyAccessedByOtherLoggingSystems() 'invocation-id' => 'expected-id', ]); - $this->assertSame($manager->sharedContext(), ['invocation-id' => 'expected-id']); + $this->assertSame(['invocation-id' => 'expected-id'], $manager->sharedContext()); } public function testItSharesContextWithStacksWhenTheyAreResolved() diff --git a/tests/Pagination/CursorPaginatorTest.php b/tests/Pagination/CursorPaginatorTest.php index 7175fe5a85b8..6b5b4ea9a213 100644 --- a/tests/Pagination/CursorPaginatorTest.php +++ b/tests/Pagination/CursorPaginatorTest.php @@ -61,7 +61,7 @@ public function testPaginatorReturnsPath() $p = new CursorPaginator($array = [['id' => 4], ['id' => 5], ['id' => 6]], 2, null, $options = ['path' => 'http://website.com/test', 'parameters' => ['id']]); - $this->assertSame($p->path(), 'http://website.com/test'); + $this->assertSame('http://website.com/test', $p->path()); } public function testCanTransformPaginatorItems() diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index 68dc5bb9091c..1fcc8ffcf52f 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -953,13 +953,13 @@ public function testCanSetMiddlewareForSpecifiedMethodsOnRegisteredResource() ->middlewareFor(['edit'], ['one', 'two']); $this->router->getRoutes()->refreshNameLookups(); - $this->assertEquals($this->router->getRoutes()->getByName('users.index')->gatherMiddleware(), ['default', RouteRegistrarMiddlewareStub::class]); - $this->assertEquals($this->router->getRoutes()->getByName('users.create')->gatherMiddleware(), ['default', 'one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.store')->gatherMiddleware(), ['default', 'one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.show')->gatherMiddleware(), ['default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.edit')->gatherMiddleware(), ['default', 'one', 'two']); - $this->assertEquals($this->router->getRoutes()->getByName('users.update')->gatherMiddleware(), ['default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware(), ['default']); + $this->assertEquals(['default', RouteRegistrarMiddlewareStub::class], $this->router->getRoutes()->getByName('users.index')->gatherMiddleware()); + $this->assertEquals(['default', 'one'], $this->router->getRoutes()->getByName('users.create')->gatherMiddleware()); + $this->assertEquals(['default', 'one'], $this->router->getRoutes()->getByName('users.store')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.show')->gatherMiddleware()); + $this->assertEquals(['default', 'one', 'two'], $this->router->getRoutes()->getByName('users.edit')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.update')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware()); $this->router->resource('users', RouteRegistrarControllerStub::class) ->middlewareFor('index', RouteRegistrarMiddlewareStub::class) @@ -968,13 +968,13 @@ public function testCanSetMiddlewareForSpecifiedMethodsOnRegisteredResource() ->middleware('default'); $this->router->getRoutes()->refreshNameLookups(); - $this->assertEquals($this->router->getRoutes()->getByName('users.index')->gatherMiddleware(), [RouteRegistrarMiddlewareStub::class, 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.create')->gatherMiddleware(), ['one', 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.store')->gatherMiddleware(), ['one', 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.show')->gatherMiddleware(), ['default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.edit')->gatherMiddleware(), ['one', 'two', 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.update')->gatherMiddleware(), ['default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware(), ['default']); + $this->assertEquals([RouteRegistrarMiddlewareStub::class, 'default'], $this->router->getRoutes()->getByName('users.index')->gatherMiddleware()); + $this->assertEquals(['one', 'default'], $this->router->getRoutes()->getByName('users.create')->gatherMiddleware()); + $this->assertEquals(['one', 'default'], $this->router->getRoutes()->getByName('users.store')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.show')->gatherMiddleware()); + $this->assertEquals(['one', 'two', 'default'], $this->router->getRoutes()->getByName('users.edit')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.update')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware()); } public function testResourceWithoutMiddlewareRegistration() @@ -997,13 +997,13 @@ public function testCanSetExcludedMiddlewareForSpecifiedMethodsOnRegisteredResou ->withoutMiddlewareFor(['create', 'store'], 'three') ->withoutMiddlewareFor(['edit'], ['four', 'five']); - $this->assertEquals($this->router->getRoutes()->getByName('users.index')->excludedMiddleware(), ['one', 'two']); - $this->assertEquals($this->router->getRoutes()->getByName('users.create')->excludedMiddleware(), ['one', 'three']); - $this->assertEquals($this->router->getRoutes()->getByName('users.store')->excludedMiddleware(), ['one', 'three']); - $this->assertEquals($this->router->getRoutes()->getByName('users.show')->excludedMiddleware(), ['one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.edit')->excludedMiddleware(), ['one', 'four', 'five']); - $this->assertEquals($this->router->getRoutes()->getByName('users.update')->excludedMiddleware(), ['one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.destroy')->excludedMiddleware(), ['one']); + $this->assertEquals(['one', 'two'], $this->router->getRoutes()->getByName('users.index')->excludedMiddleware()); + $this->assertEquals(['one', 'three'], $this->router->getRoutes()->getByName('users.create')->excludedMiddleware()); + $this->assertEquals(['one', 'three'], $this->router->getRoutes()->getByName('users.store')->excludedMiddleware()); + $this->assertEquals(['one'], $this->router->getRoutes()->getByName('users.show')->excludedMiddleware()); + $this->assertEquals(['one', 'four', 'five'], $this->router->getRoutes()->getByName('users.edit')->excludedMiddleware()); + $this->assertEquals(['one'], $this->router->getRoutes()->getByName('users.update')->excludedMiddleware()); + $this->assertEquals(['one'], $this->router->getRoutes()->getByName('users.destroy')->excludedMiddleware()); } public function testResourceWithMiddlewareAsStringable() @@ -1499,12 +1499,12 @@ public function testCanSetMiddlewareForSpecifiedMethodsOnRegisteredSingletonReso ->middlewareFor(['edit'], ['one', 'two']); $this->router->getRoutes()->refreshNameLookups(); - $this->assertEquals($this->router->getRoutes()->getByName('users.create')->gatherMiddleware(), ['default', 'one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.store')->gatherMiddleware(), ['default', 'one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.show')->gatherMiddleware(), ['default', RouteRegistrarMiddlewareStub::class]); - $this->assertEquals($this->router->getRoutes()->getByName('users.edit')->gatherMiddleware(), ['default', 'one', 'two']); - $this->assertEquals($this->router->getRoutes()->getByName('users.update')->gatherMiddleware(), ['default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware(), ['default']); + $this->assertEquals(['default', 'one'], $this->router->getRoutes()->getByName('users.create')->gatherMiddleware()); + $this->assertEquals(['default', 'one'], $this->router->getRoutes()->getByName('users.store')->gatherMiddleware()); + $this->assertEquals(['default', RouteRegistrarMiddlewareStub::class], $this->router->getRoutes()->getByName('users.show')->gatherMiddleware()); + $this->assertEquals(['default', 'one', 'two'], $this->router->getRoutes()->getByName('users.edit')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.update')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware()); $this->router->singleton('users', RouteRegistrarControllerStub::class) ->creatable() @@ -1515,12 +1515,12 @@ public function testCanSetMiddlewareForSpecifiedMethodsOnRegisteredSingletonReso ->middleware('default'); $this->router->getRoutes()->refreshNameLookups(); - $this->assertEquals($this->router->getRoutes()->getByName('users.create')->gatherMiddleware(), ['one', 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.store')->gatherMiddleware(), ['one', 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.show')->gatherMiddleware(), [RouteRegistrarMiddlewareStub::class, 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.edit')->gatherMiddleware(), ['one', 'two', 'default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.update')->gatherMiddleware(), ['default']); - $this->assertEquals($this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware(), ['default']); + $this->assertEquals(['one', 'default'], $this->router->getRoutes()->getByName('users.create')->gatherMiddleware()); + $this->assertEquals(['one', 'default'], $this->router->getRoutes()->getByName('users.store')->gatherMiddleware()); + $this->assertEquals([RouteRegistrarMiddlewareStub::class, 'default'], $this->router->getRoutes()->getByName('users.show')->gatherMiddleware()); + $this->assertEquals(['one', 'two', 'default'], $this->router->getRoutes()->getByName('users.edit')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.update')->gatherMiddleware()); + $this->assertEquals(['default'], $this->router->getRoutes()->getByName('users.destroy')->gatherMiddleware()); } public function testCanSetExcludedMiddlewareForSpecifiedMethodsOnRegisteredSingletonResource() @@ -1533,12 +1533,12 @@ public function testCanSetExcludedMiddlewareForSpecifiedMethodsOnRegisteredSingl ->withoutMiddlewareFor(['create', 'store'], 'three') ->withoutMiddlewareFor(['edit'], ['four', 'five']); - $this->assertEquals($this->router->getRoutes()->getByName('users.create')->excludedMiddleware(), ['one', 'three']); - $this->assertEquals($this->router->getRoutes()->getByName('users.store')->excludedMiddleware(), ['one', 'three']); - $this->assertEquals($this->router->getRoutes()->getByName('users.show')->excludedMiddleware(), ['one', 'two']); - $this->assertEquals($this->router->getRoutes()->getByName('users.edit')->excludedMiddleware(), ['one', 'four', 'five']); - $this->assertEquals($this->router->getRoutes()->getByName('users.update')->excludedMiddleware(), ['one']); - $this->assertEquals($this->router->getRoutes()->getByName('users.destroy')->excludedMiddleware(), ['one']); + $this->assertEquals(['one', 'three'], $this->router->getRoutes()->getByName('users.create')->excludedMiddleware()); + $this->assertEquals(['one', 'three'], $this->router->getRoutes()->getByName('users.store')->excludedMiddleware()); + $this->assertEquals(['one', 'two'], $this->router->getRoutes()->getByName('users.show')->excludedMiddleware()); + $this->assertEquals(['one', 'four', 'five'], $this->router->getRoutes()->getByName('users.edit')->excludedMiddleware()); + $this->assertEquals(['one'], $this->router->getRoutes()->getByName('users.update')->excludedMiddleware()); + $this->assertEquals(['one'], $this->router->getRoutes()->getByName('users.destroy')->excludedMiddleware()); } /** diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index 6ee9c747d9a2..024ab4c256fe 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -80,7 +80,7 @@ public function testItCanSpecifyMinutes() $sleep = Sleep::for(1.5)->minutes(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 90_000_000.0); + $this->assertSame(90_000_000.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifyMinute() @@ -89,7 +89,7 @@ public function testItCanSpecifyMinute() $sleep = Sleep::for(1)->minute(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 60_000_000.0); + $this->assertSame(60_000_000.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifySeconds() @@ -98,7 +98,7 @@ public function testItCanSpecifySeconds() $sleep = Sleep::for(1.5)->seconds(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1_500_000.0); + $this->assertSame(1_500_000.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifySecond() @@ -107,7 +107,7 @@ public function testItCanSpecifySecond() $sleep = Sleep::for(1)->second(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1_000_000.0); + $this->assertSame(1_000_000.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifyMilliseconds() @@ -116,7 +116,7 @@ public function testItCanSpecifyMilliseconds() $sleep = Sleep::for(1.5)->milliseconds(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1_500.0); + $this->assertSame(1_500.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifyMillisecond() @@ -125,7 +125,7 @@ public function testItCanSpecifyMillisecond() $sleep = Sleep::for(1)->millisecond(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1_000.0); + $this->assertSame(1_000.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifyMicroseconds() @@ -135,7 +135,7 @@ public function testItCanSpecifyMicroseconds() $sleep = Sleep::for(1.5)->microseconds(); // rounded as microseconds is the smallest unit supported... - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1.0); + $this->assertSame(1.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSpecifyMicrosecond() @@ -144,7 +144,7 @@ public function testItCanSpecifyMicrosecond() $sleep = Sleep::for(1)->microsecond(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1.0); + $this->assertSame(1.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanChainDurations() @@ -154,7 +154,7 @@ public function testItCanChainDurations() $sleep = Sleep::for(1)->second() ->and(500)->microseconds(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1000500.0); + $this->assertSame(1000500.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanUseDateInterval() @@ -163,7 +163,7 @@ public function testItCanUseDateInterval() $sleep = Sleep::for(CarbonInterval::seconds(1)->addMilliseconds(5)); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1_005_000.0); + $this->assertSame(1_005_000.0, (float) $sleep->duration->totalMicroseconds); } public function testItThrowsForUnknownTimeUnit() @@ -486,15 +486,15 @@ public function testItCanCreateMacrosViaMacroable() // A static macro can be referenced $sleep = Sleep::forSomeConfiguredAmountOfTime(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 3000000.0); + $this->assertSame(3000000.0, (float) $sleep->duration->totalMicroseconds); // A macro can specify a new duration $sleep = $sleep->useSomeOtherAmountOfTime(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1234000.0); + $this->assertSame(1234000.0, (float) $sleep->duration->totalMicroseconds); // A macro can supplement an existing duration $sleep = $sleep->andSomeMoreGranularControl(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1234567.0); + $this->assertSame(1234567.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanReplacePreviouslyDefinedDurations() @@ -506,13 +506,13 @@ public function testItCanReplacePreviouslyDefinedDurations() }); $sleep = Sleep::for(1)->second(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 1000000.0); + $this->assertSame(1000000.0, (float) $sleep->duration->totalMicroseconds); $sleep->setDuration(2)->second(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 2000000.0); + $this->assertSame(2000000.0, (float) $sleep->duration->totalMicroseconds); $sleep->setDuration(500)->milliseconds(); - $this->assertSame((float) $sleep->duration->totalMicroseconds, 500000.0); + $this->assertSame(500000.0, (float) $sleep->duration->totalMicroseconds); } public function testItCanSleepConditionallyWhen() diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 8bb7c86467bd..738a79890a93 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -818,11 +818,11 @@ public function testStr() $strAccessor = str(); $this->assertTrue((new ReflectionClass($strAccessor))->isAnonymous()); - $this->assertSame($strAccessor->limit('string-value', 3), 'str...'); + $this->assertSame('str...', $strAccessor->limit('string-value', 3)); $strAccessor = str(); $this->assertTrue((new ReflectionClass($strAccessor))->isAnonymous()); - $this->assertSame((string) $strAccessor, ''); + $this->assertSame('', (string) $strAccessor); } public function testTap() diff --git a/tests/Validation/ValidationAddFailureTest.php b/tests/Validation/ValidationAddFailureTest.php index dd65f9f7a4f9..d804677071a3 100644 --- a/tests/Validation/ValidationAddFailureTest.php +++ b/tests/Validation/ValidationAddFailureTest.php @@ -34,7 +34,7 @@ public function testAddFailureIsFunctional() $validator = $this->makeValidator(); $validator->addFailure($attribute, 'not_in'); $messages = json_decode($validator->messages()); - $this->assertSame($messages->{'foo.bar.baz'}[0], 'validation.required', 'initial data in messages is lost'); - $this->assertSame($messages->{$attribute}[0], 'validation.not_in', 'new data in messages was not added'); + $this->assertSame('validation.required', $messages->{'foo.bar.baz'}[0], 'initial data in messages is lost'); + $this->assertSame('validation.not_in', $messages->{$attribute}[0], 'new data in messages was not added'); } } diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 05f2cd20ab6d..8a4486b1365e 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -348,7 +348,7 @@ public function testCanRetrieveAllRulesApplied() ->letters() ->symbols(); - $this->assertSame($password->appliedRules(), [ + $this->assertSame([ 'min' => 2, 'max' => 4, 'mixedCase' => true, @@ -358,11 +358,11 @@ public function testCanRetrieveAllRulesApplied() 'uncompromised' => false, 'compromisedThreshold' => 0, 'customRules' => [], - ]); + ], $password->appliedRules()); $password = Password::min(2); - $this->assertSame($password->appliedRules(), [ + $this->assertSame([ 'min' => 2, 'max' => null, 'mixedCase' => false, @@ -372,7 +372,7 @@ public function testCanRetrieveAllRulesApplied() 'uncompromised' => false, 'compromisedThreshold' => 0, 'customRules' => [], - ]); + ], $password->appliedRules()); } public function testRequired() diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php index 8da3e84afdd3..4f5cd7d51f69 100644 --- a/tests/Validation/ValidationRuleParserTest.php +++ b/tests/Validation/ValidationRuleParserTest.php @@ -187,7 +187,7 @@ public function testExplodeGeneratesNestedRules() 'users.*.name' => Rule::forEach(function ($value, $attribute, $data, $context) { $this->assertSame('Taylor Otwell', $value); $this->assertSame('users.0.name', $attribute); - $this->assertEquals($data['users.0.name'], 'Taylor Otwell'); + $this->assertEquals('Taylor Otwell', $data['users.0.name']); $this->assertEquals(['name' => 'Taylor Otwell', 'email' => 'taylor@laravel.com'], $context); return [Rule::requiredIf(true)]; diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index d004acb5b883..2e878ad805ae 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1178,9 +1178,9 @@ public function testCustomValidationIsAppendedToMessages() ], ['foo' => ['required' => 'Foo is required']]); $this->assertFalse($validator->passes()); - $this->assertEquals($validator->errors()->messages(), [ + $this->assertEquals([ 'foo' => ['foo must be false'], - ]); + ], $validator->errors()->messages()); } public function testInlineValidationMessagesAreRespectedWithAsterisks() diff --git a/tests/Validation/ValidatorAfterRuleTest.php b/tests/Validation/ValidatorAfterRuleTest.php index cce9e555b516..776f0bc62fac 100644 --- a/tests/Validation/ValidatorAfterRuleTest.php +++ b/tests/Validation/ValidatorAfterRuleTest.php @@ -19,11 +19,11 @@ public function testAfterAcceptsArrayOfRules() new AfterMethodRule, ])->messages()->messages(); - $this->assertSame($validator->messages()->messages(), [ + $this->assertSame([ 'closure' => ['true'], 'invokableAfterRule' => ['true'], 'afterMethodRule' => ['true'], - ]); + ], $validator->messages()->messages()); } } diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 488c5d504762..f872a1c8d00e 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -892,8 +892,8 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() eval(" ?> $template assertSame($attributes->get('userId'), 'bar'); - $this->assertSame($attributes->get('other'), 'ok'); + $this->assertSame('bar', $attributes->get('userId')); + $this->assertSame('ok', $attributes->get('other')); } public function testOriginalAttributesAreRestoredAfterRenderingChildComponentWithProps() @@ -942,8 +942,8 @@ public function testOriginalAttributesAreRestoredAfterRenderingChildComponentWit eval(" ?> $template assertSame($attributes->get('userId'), 'bar'); - $this->assertSame($attributes->get('other'), 'ok'); + $this->assertSame('bar', $attributes->get('userId')); + $this->assertSame('ok', $attributes->get('other')); } protected function mockViewFactory($existsSucceeds = true) diff --git a/tests/View/Blade/BladePropsTest.php b/tests/View/Blade/BladePropsTest.php index 3c756084171d..24cfe7fecc8d 100644 --- a/tests/View/Blade/BladePropsTest.php +++ b/tests/View/Blade/BladePropsTest.php @@ -51,13 +51,13 @@ public function testPropsAreExtractedFromParentAttributesCorrectly() eval(" ?> $template assertSame($test1, 'value1'); - $this->assertSame($test2, 'value2'); + $this->assertSame('value1', $test1); + $this->assertSame('value2', $test2); $this->assertFalse(isset($test3)); - $this->assertSame($test4, 'default'); + $this->assertSame('default', $test4); $this->assertNull($attributes->get('test1')); $this->assertNull($attributes->get('test2')); - $this->assertSame($attributes->get('test3'), 'value3'); + $this->assertSame('value3', $attributes->get('test3')); } } diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 37689342e69b..326df4900f49 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -675,7 +675,7 @@ public function testComponentHandlingUsingClosure() $factory->getDispatcher()->shouldReceive('hasListeners')->andReturn(false); $factory->startComponent(function ($data) use ($factory) { $this->assertArrayHasKey('name', $data); - $this->assertSame($data['name'], 'Taylor'); + $this->assertSame('Taylor', $data['name']); return $factory->make('component'); }, ['name' => 'Taylor']); From f0ad1b9d7682d43800e6822dc13565fd634310b2 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 19:44:49 +0200 Subject: [PATCH 158/596] [13.x] Remove unused variable in `catch()` (#59689) * Remove unused variable in catch() * Fix createPdoResolverWithHosts logic --------- Co-authored-by: Lucas Michot --- rector.php | 2 -- src/Illuminate/Database/Connectors/ConnectionFactory.php | 8 +++++--- src/Illuminate/Filesystem/ReceiveFile.php | 2 +- src/Illuminate/Filesystem/ServeFile.php | 2 +- src/Illuminate/Queue/Jobs/Job.php | 2 +- src/Illuminate/Support/Str.php | 2 +- tests/Integration/Database/EloquentBelongsToManyTest.php | 2 +- tests/Integration/Events/EventFakeTest.php | 2 +- .../Events/ShouldDispatchAfterCommitEventTest.php | 8 ++++---- tests/Redis/RedisEventsTest.php | 4 ++-- tests/Testing/TestResponseTest.php | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/rector.php b/rector.php index c519edb80d22..24cbb0a12d51 100644 --- a/rector.php +++ b/rector.php @@ -22,7 +22,6 @@ use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php74\Rector\Assign\NullCoalescingOperatorRector; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; -use Rector\Php80\Rector\Catch_\RemoveUnusedVariableInCatchRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php80\Rector\Class_\StringableForToStringRector; use Rector\Php80\Rector\ClassConstFetch\ClassOnThisVariableObjectRector; @@ -73,7 +72,6 @@ ReadOnlyClassRector::class, ReadOnlyPropertyRector::class, RemoveExtraParametersRector::class, - RemoveUnusedVariableInCatchRector::class, ReturnNeverTypeRector::class, StaticCallOnNonStaticToInstanceCallRector::class, StringClassNameToClassConstantRector::class, diff --git a/src/Illuminate/Database/Connectors/ConnectionFactory.php b/src/Illuminate/Database/Connectors/ConnectionFactory.php index 7017c0aa5ce5..7b21bd0f27a9 100755 --- a/src/Illuminate/Database/Connectors/ConnectionFactory.php +++ b/src/Illuminate/Database/Connectors/ConnectionFactory.php @@ -179,18 +179,20 @@ protected function createPdoResolver(array $config) protected function createPdoResolverWithHosts(array $config) { return function () use ($config) { + $exception = null; + foreach (Arr::shuffle($this->parseHosts($config)) as $host) { $config['host'] = $host; try { return $this->createConnector($config)->connect($config); } catch (PDOException $e) { - continue; + $exception = $e; } } - if (isset($e)) { - throw $e; + if ($exception !== null) { + throw $exception; } }; } diff --git a/src/Illuminate/Filesystem/ReceiveFile.php b/src/Illuminate/Filesystem/ReceiveFile.php index bc488d1eb052..8f7616ef28ae 100644 --- a/src/Illuminate/Filesystem/ReceiveFile.php +++ b/src/Illuminate/Filesystem/ReceiveFile.php @@ -34,7 +34,7 @@ public function __invoke(Request $request, string $path): Response Storage::disk($this->disk)->put($path, $request->getContent()); return response()->noContent(); - } catch (PathTraversalDetected $e) { + } catch (PathTraversalDetected) { abort(404); } } diff --git a/src/Illuminate/Filesystem/ServeFile.php b/src/Illuminate/Filesystem/ServeFile.php index e8732cb14775..4e0c653c92b6 100644 --- a/src/Illuminate/Filesystem/ServeFile.php +++ b/src/Illuminate/Filesystem/ServeFile.php @@ -44,7 +44,7 @@ function ($response) use ($headers) { } } ); - } catch (PathTraversalDetected $e) { + } catch (PathTraversalDetected) { abort(404); } } diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index bed7d611bd4e..0f249987d012 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -199,7 +199,7 @@ public function fail($e = null) try { $batchRepository->rollBack(); - } catch (Throwable $e) { + } catch (Throwable) { // ... } } diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index cc3874e9c976..7b38d3a14ae9 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1231,7 +1231,7 @@ private static function toStringOr($value, $fallback) { try { return (string) $value; - } catch (Throwable $e) { + } catch (Throwable) { return $fallback; } } diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index 51bb0487ae51..3bb3b1bcf02e 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -475,7 +475,7 @@ public function testFindSoleMethod() try { $post->tags()->findSole($tag); $this->fail('Expected RecordsNotFoundException was not thrown.'); - } catch (RecordsNotFoundException $e) { + } catch (RecordsNotFoundException) { $this->assertTrue(true); } } diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php index 1f8eaa3999e6..319703f0c827 100644 --- a/tests/Integration/Events/EventFakeTest.php +++ b/tests/Integration/Events/EventFakeTest.php @@ -196,7 +196,7 @@ public function testShouldDispatchAfterCommitEventsAreNotDispatchedIfTransaction throw new Exception('foo'); }); - } catch (Exception $e) { + } catch (Exception) { } Event::assertNotDispatched(ShouldDispatchAfterCommitEvent::class); diff --git a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php index bb3a33a70e19..e68826118dec 100644 --- a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php +++ b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php @@ -186,7 +186,7 @@ public function testItHandlesNestedTransactionsWhereTheSecondOneFails() Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); throw new \Exception; }); - } catch (\Exception $e) { + } catch (\Exception) { } }); @@ -207,7 +207,7 @@ public function testChildCallbacksShouldNotBeDispatchedIfTheirParentFails() throw new \Exception; }); - } catch (\Exception $e) { + } catch (\Exception) { // } }); @@ -232,7 +232,7 @@ public function testItHandlesFailuresWithTransactionsTwoLevelsHigher() Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); throw new \Exception; }); - } catch (\Exception $e) { + } catch (\Exception) { } }); @@ -255,7 +255,7 @@ public function testCommittedTransactionThatWasDeeplyNestedIsRemovedIfTopLevelFa throw new \Exception; }); - } catch (\Exception $e) { + } catch (\Exception) { } }); diff --git a/tests/Redis/RedisEventsTest.php b/tests/Redis/RedisEventsTest.php index a53cb8dfa777..cfaa3b72ca65 100644 --- a/tests/Redis/RedisEventsTest.php +++ b/tests/Redis/RedisEventsTest.php @@ -53,7 +53,7 @@ public function testCommandExecutedEventIsNotDispatchedWhenCommandFails() try { $connection->command('get', ['key']); - } catch (Exception $e) { + } catch (Exception) { // Expected exception } } @@ -77,7 +77,7 @@ public function testCommandFailedEventContainsConnectionName() try { $connection->command('get', ['key']); - } catch (Exception $e) { + } catch (Exception) { // Expected exception } } diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 3b07124fb9b8..ba9653b5ccfc 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1750,7 +1750,7 @@ public function testAssertExactJsonStructure(): void try { $response->assertExactJsonStructure(['*' => ['foo', 'bar']]); $failed = false; - } catch (AssertionFailedError $e) { + } catch (AssertionFailedError) { $failed = true; } From 06c16eabd1f88db5dc636ee4ea4fbc4393e32c53 Mon Sep 17 00:00:00 2001 From: Christos Koumpis <56029580+Button99@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:17:09 +0300 Subject: [PATCH 159/596] [13.x] Fix number abbreviation rollover between unit tiers (#59692) * fix number abbreviation rollover between unit tiers * formatting --- src/Illuminate/Support/Number.php | 10 +++++++++- tests/Support/SupportNumberTest.php | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index f07221855442..6ea2430a98a0 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -289,7 +289,15 @@ protected static function summarize(int|float $number, int $precision = 0, ?int $displayExponent = $numberExponent - ($numberExponent % 3); $number /= pow(10, $displayExponent); - return trim(sprintf('%s%s', static::format($number, $precision, $maxPrecision), $units[$displayExponent] ?? '')); + $formatted = static::format($number, $precision, $maxPrecision); + + if (static::parseFloat($formatted) >= 1000 && isset($units[$displayExponent + 3])) { + $number /= 1000; + $displayExponent += 3; + $formatted = static::format($number, $precision, $maxPrecision); + } + + return trim(sprintf('%s%s', $formatted, $units[$displayExponent] ?? '')); } /** diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 7f7de7f3f1a2..720e4744fbb4 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -251,6 +251,10 @@ public function testToHuman() $this->assertSame('-1.1 trillion', Number::forHumans(-1100000000000, maxPrecision: 1)); $this->assertSame('-1 quadrillion', Number::forHumans(-1000000000000000)); $this->assertSame('-1 thousand quadrillion', Number::forHumans(-1000000000000000000)); + + $this->assertSame('999 thousand', Number::forHumans(999499)); + $this->assertSame('1 million', Number::forHumans(999500)); + $this->assertSame('1 million', Number::forHumans(999999)); } public function testSummarize() @@ -307,6 +311,15 @@ public function testSummarize() $this->assertSame('-1.1T', Number::abbreviate(-1100000000000, maxPrecision: 1)); $this->assertSame('-1Q', Number::abbreviate(-1000000000000000)); $this->assertSame('-1KQ', Number::abbreviate(-1000000000000000000)); + + $this->assertSame('999K', Number::abbreviate(999499)); + $this->assertSame('1M', Number::abbreviate(999500)); + $this->assertSame('1M', Number::abbreviate(999999)); + $this->assertSame('1B', Number::abbreviate(999500000)); + $this->assertSame('1B', Number::abbreviate(999999999)); + + Number::withLocale('de', fn () => $this->assertSame('1M', Number::abbreviate(999500))); + Number::withLocale('fr', fn () => $this->assertSame('1M', Number::abbreviate(999500))); } public function testPairs() From ccdd692d28bb429fca1a08327957c95127f9b7ce Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 20:17:35 +0200 Subject: [PATCH 160/596] Use Null and Isset coalescing when possible (#59690) Co-authored-by: Lucas Michot --- rector.php | 4 ---- src/Illuminate/Database/Eloquent/Model.php | 2 +- .../Eloquent/Relations/HasOneOrManyThrough.php | 4 ++-- src/Illuminate/Foundation/Cloud.php | 2 +- src/Illuminate/Http/Client/Response.php | 2 +- src/Illuminate/Http/Concerns/InteractsWithInput.php | 2 +- src/Illuminate/Http/Middleware/HandleCors.php | 6 +----- src/Illuminate/Process/PendingProcess.php | 2 +- src/Illuminate/Redis/RedisManager.php | 6 +----- src/Illuminate/Routing/Route.php | 12 ++---------- src/Illuminate/Support/Str.php | 6 +----- src/Illuminate/View/DynamicComponent.php | 6 +----- tests/Http/HttpTestingFileFactoryTest.php | 6 +----- 13 files changed, 14 insertions(+), 46 deletions(-) diff --git a/rector.php b/rector.php index 24cbb0a12d51..96608ee30ba3 100644 --- a/rector.php +++ b/rector.php @@ -16,11 +16,9 @@ use Rector\Php70\Rector\If_\IfToSpaceshipRector; use Rector\Php70\Rector\MethodCall\ThisCallOnStaticMethodToStaticCallRector; use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector; -use Rector\Php70\Rector\StmtsAwareInterface\IfIssetToCoalescingRector; use Rector\Php70\Rector\Ternary\TernaryToNullCoalescingRector; use Rector\Php71\Rector\BinaryOp\BinaryOpBetweenNumberAndStringRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; -use Rector\Php74\Rector\Assign\NullCoalescingOperatorRector; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php80\Rector\Class_\StringableForToStringRector; @@ -63,9 +61,7 @@ DynamicClassConstFetchRector::class, FunctionFirstClassCallableRector::class, GetDebugTypeRector::class, - IfIssetToCoalescingRector::class, IfToSpaceshipRector::class, - NullCoalescingOperatorRector::class, NullToStrictStringFuncCallArgRector::class, PowToExpRector::class, RandomFunctionRector::class, diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index ce67d769914f..013fa44ebc70 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2638,7 +2638,7 @@ public function broadcastChannel() */ protected static function resolveClassAttribute(string $attributeClass, ?string $property = null, ?string $class = null) { - $class = $class ?? static::class; + $class ??= static::class; $cacheKey = $class.'@'.$attributeClass; diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php index c4a518378b0c..4961e4c3c89b 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php @@ -593,9 +593,9 @@ public function chunkByIdDesc($count, callable $callback, $column = null, $alias */ public function eachById(callable $callback, $count = 1000, $column = null, $alias = null) { - $column = $column ?? $this->getRelated()->getQualifiedKeyName(); + $column ??= $this->getRelated()->getQualifiedKeyName(); - $alias = $alias ?? $this->getRelated()->getKeyName(); + $alias ??= $this->getRelated()->getKeyName(); return $this->prepareQueryBuilder()->eachById($callback, $count, $column, $alias); } diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 63cfe86a07c8..bdc1afc38ded 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -104,7 +104,7 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): } Migrator::resolveConnectionsUsing(function ($resolver, $connection) use ($app) { - $connection = $connection ?? $app['config']->get('database.default'); + $connection ??= $app['config']->get('database.default'); return $resolver->connection( $connection === 'pgsql' ? 'pgsql-unpooled' : $connection diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index 9d3bd52175e7..f2eebf4661a6 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -99,7 +99,7 @@ public function body() */ public function json($key = null, $default = null, $flags = null) { - $flags = $flags ?? self::$defaultJsonDecodingFlags; + $flags ??= self::$defaultJsonDecodingFlags; if (! $this->decoded || (isset($this->decodingFlags) && $this->decodingFlags !== $flags)) { $this->decoded = json_decode( diff --git a/src/Illuminate/Http/Concerns/InteractsWithInput.php b/src/Illuminate/Http/Concerns/InteractsWithInput.php index e3bd296aceaa..d3805ee07d54 100644 --- a/src/Illuminate/Http/Concerns/InteractsWithInput.php +++ b/src/Illuminate/Http/Concerns/InteractsWithInput.php @@ -184,7 +184,7 @@ public function allFiles() { $files = $this->files->all(); - return $this->convertedFiles = $this->convertedFiles ?? $this->convertUploadedFiles($files); + return $this->convertedFiles ??= $this->convertUploadedFiles($files); } /** diff --git a/src/Illuminate/Http/Middleware/HandleCors.php b/src/Illuminate/Http/Middleware/HandleCors.php index cb96a5f7701c..efabddeb6f5d 100644 --- a/src/Illuminate/Http/Middleware/HandleCors.php +++ b/src/Illuminate/Http/Middleware/HandleCors.php @@ -113,11 +113,7 @@ protected function getPathsByHost(string $host) { $paths = $this->container['config']->get('cors.paths', []); - if (isset($paths[$host])) { - return $paths[$host]; - } - - return array_filter($paths, function ($path) { + return $paths[$host] ?? array_filter($paths, function ($path) { return is_string($path); }); } diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 600fb19d222c..a859d0533fb4 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -297,7 +297,7 @@ public function start(array|string|null $command = null, ?callable $output = nul */ protected function toSymfonyProcess(array|string|null $command) { - $command = $command ?? $this->command; + $command ??= $this->command; $process = is_iterable($command) ? new Process($command, null, $this->environment) diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index 80a5aa56c125..ffe5894a3988 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -89,11 +89,7 @@ public function connection($name = null) { $name = enum_value($name) ?: 'default'; - if (isset($this->connections[$name])) { - return $this->connections[$name]; - } - - return $this->connections[$name] = $this->configure( + return $this->connections[$name] ?? $this->connections[$name] = $this->configure( $this->resolve($name), $name ); } diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index fdf48d27f972..341d07f0e36b 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -515,11 +515,7 @@ public function parametersWithoutNulls() */ public function parameterNames() { - if (isset($this->parameterNames)) { - return $this->parameterNames; - } - - return $this->parameterNames = $this->compileParameterNames(); + return $this->parameterNames ?? $this->parameterNames = $this->compileParameterNames(); } /** @@ -1351,14 +1347,10 @@ public function controllerDispatcher() */ public static function getValidators() { - if (isset(static::$validators)) { - return static::$validators; - } - // To match the route, we will use a chain of responsibility pattern with the // validator implementations. We will spin through each one making sure it // passes and then we will know if the route as a whole matches request. - return static::$validators = [ + return static::$validators ?? static::$validators = [ new UriValidator, new MethodValidator, new SchemeValidator, new HostValidator, ]; diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 7b38d3a14ae9..4b42dd00df76 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -225,11 +225,7 @@ public static function betweenFirst($subject, $from, $to) */ public static function camel($value) { - if (isset(static::$camelCache[$value])) { - return static::$camelCache[$value]; - } - - return static::$camelCache[$value] = lcfirst(static::studly($value)); + return static::$camelCache[$value] ?? static::$camelCache[$value] = lcfirst(static::studly($value)); } /** diff --git a/src/Illuminate/View/DynamicComponent.php b/src/Illuminate/View/DynamicComponent.php index 3de144129557..324d30698f4e 100644 --- a/src/Illuminate/View/DynamicComponent.php +++ b/src/Illuminate/View/DynamicComponent.php @@ -135,11 +135,7 @@ protected function compileSlots(array $slots) */ protected function classForComponent() { - if (isset(static::$componentClasses[$this->component])) { - return static::$componentClasses[$this->component]; - } - - return static::$componentClasses[$this->component] = + return static::$componentClasses[$this->component] ?? static::$componentClasses[$this->component] = $this->compiler()->componentClass($this->component); } diff --git a/tests/Http/HttpTestingFileFactoryTest.php b/tests/Http/HttpTestingFileFactoryTest.php index 5ee649cce689..b0ef78b08411 100644 --- a/tests/Http/HttpTestingFileFactoryTest.php +++ b/tests/Http/HttpTestingFileFactoryTest.php @@ -150,10 +150,6 @@ private function isGDSupported(string $driver = 'GD Version'): bool { $gdInfo = gd_info(); - if (isset($gdInfo[$driver])) { - return $gdInfo[$driver]; - } - - return false; + return $gdInfo[$driver] ?? false; } } From ff97ab481f3e2abf8b122e0294838649c8748ac2 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 14 Apr 2026 20:18:24 +0200 Subject: [PATCH 161/596] Change count array comparison to empty array comparison to improve performance (#59688) Co-authored-by: Lucas Michot --- rector.php | 2 ++ src/Illuminate/Cache/DatabaseStore.php | 2 +- src/Illuminate/Cache/DynamoDbStore.php | 4 ++-- src/Illuminate/Cache/MemcachedConnector.php | 2 +- src/Illuminate/Cache/MemoizedStore.php | 2 +- src/Illuminate/Cache/RedisStore.php | 2 +- src/Illuminate/Cache/RedisTagSet.php | 2 +- src/Illuminate/Collections/Arr.php | 2 +- src/Illuminate/Console/Command.php | 10 +++++----- src/Illuminate/Console/Scheduling/Schedule.php | 2 +- src/Illuminate/Database/Eloquent/Factories/Factory.php | 2 +- src/Illuminate/Database/Eloquent/Model.php | 2 +- .../Database/Eloquent/ModelNotFoundException.php | 2 +- .../Relations/Concerns/InteractsWithPivotTable.php | 6 +++--- src/Illuminate/Database/Eloquent/SoftDeletes.php | 2 +- src/Illuminate/Database/Eloquent/SoftDeletingScope.php | 2 +- src/Illuminate/Database/Migrations/Migrator.php | 4 ++-- src/Illuminate/Database/Query/Builder.php | 6 +++--- src/Illuminate/Foundation/Application.php | 2 +- .../Foundation/Exceptions/Renderer/Frame.php | 2 +- src/Illuminate/Foundation/Http/FormRequest.php | 8 ++++---- .../Testing/Traits/CanConfigureMigrationCommands.php | 2 +- src/Illuminate/Http/Concerns/InteractsWithInput.php | 2 +- src/Illuminate/Http/Resources/CollectsResources.php | 2 +- .../Http/Resources/ConditionallyLoadsAttributes.php | 4 +--- src/Illuminate/Http/Resources/Json/JsonResource.php | 4 +--- src/Illuminate/JsonSchema/Serializer.php | 2 +- .../Routing/Exceptions/UrlGenerationException.php | 2 +- src/Illuminate/Routing/RouteUrlGenerator.php | 6 +++--- .../Support/Testing/Fakes/ExceptionHandlerFake.php | 2 +- src/Illuminate/Support/Traits/ReadsClassAttributes.php | 2 +- src/Illuminate/Support/ValidatedInput.php | 2 +- src/Illuminate/Translation/Translator.php | 2 +- .../Validation/Concerns/ValidatesAttributes.php | 4 ++-- src/Illuminate/Validation/Rules/File.php | 4 ++-- src/Illuminate/View/Compilers/BladeCompiler.php | 2 +- .../View/Compilers/Concerns/CompilesLoops.php | 4 ++-- tests/Process/ProcessTest.php | 4 ++-- 38 files changed, 58 insertions(+), 60 deletions(-) diff --git a/rector.php b/rector.php index 96608ee30ba3..1a1a1697059f 100644 --- a/rector.php +++ b/rector.php @@ -7,6 +7,7 @@ use Rector\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector; use Rector\CodingStyle\Rector\FuncCall\ConsistentImplodeRector; +use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\FunctionFirstClassCallableRector; use Rector\Config\RectorConfig; use Rector\Php55\Rector\Class_\ClassConstantToSelfClassRector; @@ -77,6 +78,7 @@ 'tests/Foundation/fixtures/bad-syntax-strategy.php', ]) ->withRules([ + CountArrayToEmptyArrayComparisonRector::class, StrlenZeroToIdenticalEmptyStringRector::class, ]) ->withPreparedSets( diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 162c1e7335d5..61f100ac4ca1 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -126,7 +126,7 @@ public function get($key) */ public function many(array $keys) { - if (count($keys) === 0) { + if ($keys === []) { return []; } diff --git a/src/Illuminate/Cache/DynamoDbStore.php b/src/Illuminate/Cache/DynamoDbStore.php index c9cde2ff98ea..8c1e8825e3ec 100644 --- a/src/Illuminate/Cache/DynamoDbStore.php +++ b/src/Illuminate/Cache/DynamoDbStore.php @@ -100,7 +100,7 @@ public function get($key) */ public function many(array $keys) { - if (count($keys) === 0) { + if ($keys === []) { return []; } @@ -192,7 +192,7 @@ public function put($key, $value, $seconds) */ public function putMany(array $values, $seconds) { - if (count($values) === 0) { + if ($values === []) { return true; } diff --git a/src/Illuminate/Cache/MemcachedConnector.php b/src/Illuminate/Cache/MemcachedConnector.php index 224d94099bf3..0c0db40269d3 100755 --- a/src/Illuminate/Cache/MemcachedConnector.php +++ b/src/Illuminate/Cache/MemcachedConnector.php @@ -51,7 +51,7 @@ protected function getMemcached($connectionId, array $credentials, array $option $this->setCredentials($memcached, $credentials); } - if (count($options)) { + if ($options !== []) { $memcached->setOptions($options); } diff --git a/src/Illuminate/Cache/MemoizedStore.php b/src/Illuminate/Cache/MemoizedStore.php index 323874585156..de41c015cf59 100644 --- a/src/Illuminate/Cache/MemoizedStore.php +++ b/src/Illuminate/Cache/MemoizedStore.php @@ -66,7 +66,7 @@ public function many(array $keys) } } - if (count($missing) > 0) { + if ($missing !== []) { $retrieved = tap($this->repository->many($missing), function ($values) { foreach ($values as $key => $value) { $this->cache[$this->prefix($key)] = $value; diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 0b297a20e0bf..bd0eec1570d4 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -96,7 +96,7 @@ public function get($key) */ public function many(array $keys) { - if (count($keys) === 0) { + if ($keys === []) { return []; } diff --git a/src/Illuminate/Cache/RedisTagSet.php b/src/Illuminate/Cache/RedisTagSet.php index e65ef6efbd45..9203cd3ade96 100644 --- a/src/Illuminate/Cache/RedisTagSet.php +++ b/src/Illuminate/Cache/RedisTagSet.php @@ -66,7 +66,7 @@ public function entries() $entries = array_unique(array_keys($entries)); - if (count($entries) === 0) { + if ($entries === []) { continue; } diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index f5b8bfe1500f..c064bf729469 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -412,7 +412,7 @@ public static function forget(&$array, $keys) $keys = (array) $keys; - if (count($keys) === 0) { + if ($keys === []) { return; } diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index 99ec8fa4e0d7..b0168856433a 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -146,7 +146,7 @@ protected function configureFromAttributes() $signature = $reflection->getAttributes(Signature::class); - if (count($signature) > 0) { + if ($signature !== []) { $signatureInstance = $signature[0]->newInstance(); $this->signature = $signatureInstance->signature; @@ -158,23 +158,23 @@ protected function configureFromAttributes() $description = $reflection->getAttributes(Description::class); - if (count($description) > 0) { + if ($description !== []) { $this->description = $description[0]->newInstance()->description; } $help = $reflection->getAttributes(Help::class); - if (count($help) > 0) { + if ($help !== []) { $this->help = $help[0]->newInstance()->help; } - if (count($reflection->getAttributes(Hidden::class)) > 0) { + if ($reflection->getAttributes(Hidden::class) !== []) { $this->hidden = true; } $aliases = $reflection->getAttributes(Aliases::class); - if (count($aliases) > 0) { + if ($aliases !== []) { $this->aliases = $aliases[0]->newInstance()->aliases; } } diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index ab3e61f4fbc3..653a0e4d7a7d 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -295,7 +295,7 @@ protected function dispatchNow($job) */ public function exec($command, array $parameters = []) { - if (count($parameters)) { + if ($parameters !== []) { $command .= ' '.$this->compileParameters($parameters); } diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index efaf904d3013..603b23397a5e 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -947,7 +947,7 @@ public function modelName() if (! array_key_exists(static::class, static::$cachedModelAttributes)) { $attribute = (new ReflectionClass($this))->getAttributes(UseModel::class); - static::$cachedModelAttributes[static::class] = count($attribute) > 0 + static::$cachedModelAttributes[static::class] = $attribute !== [] ? $attribute[0]->newInstance()->class : false; } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 013fa44ebc70..b851d15377c9 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1670,7 +1670,7 @@ public static function destroy($ids) $ids = is_array($ids) ? $ids : func_get_args(); - if (count($ids) === 0) { + if ($ids === []) { return 0; } diff --git a/src/Illuminate/Database/Eloquent/ModelNotFoundException.php b/src/Illuminate/Database/Eloquent/ModelNotFoundException.php index 7ee1a30cdf66..1ab560b36a0a 100755 --- a/src/Illuminate/Database/Eloquent/ModelNotFoundException.php +++ b/src/Illuminate/Database/Eloquent/ModelNotFoundException.php @@ -43,7 +43,7 @@ public function setModel($model, $ids = []) $this->message = "No query results for model [{$model}]"; - if (count($this->ids) > 0) { + if ($this->ids !== []) { $this->message .= ' '.implode(', ', $this->ids); } else { $this->message .= '.'; diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index baa67e4b16dd..48e1163c2147 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -35,7 +35,7 @@ public function toggle($ids, $touch = true) array_keys($records) )); - if (count($detach) > 0) { + if ($detach !== []) { $this->detach($detach, false); $changes['detached'] = $this->castKeys($detach); @@ -46,7 +46,7 @@ public function toggle($ids, $touch = true) // this change list and get ready to return these results to the callers. $attach = array_diff_key($records, array_flip($detach)); - if (count($attach) > 0) { + if ($attach !== []) { $this->attach($attach, [], false); $changes['attached'] = array_keys($attach); @@ -119,7 +119,7 @@ public function sync($ids, $detaching = true) if ($detaching) { $detach = array_diff($current, array_keys($records)); - if (count($detach) > 0) { + if ($detach !== []) { $this->detach($detach, false); $changes['detached'] = $this->castKeys($detach); diff --git a/src/Illuminate/Database/Eloquent/SoftDeletes.php b/src/Illuminate/Database/Eloquent/SoftDeletes.php index bdb49727436a..06060adb83c1 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletes.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletes.php @@ -93,7 +93,7 @@ public static function forceDestroy($ids) $ids = is_array($ids) ? $ids : func_get_args(); - if (count($ids) === 0) { + if ($ids === []) { return 0; } diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index 4b4a52552318..83b3cdbfd640 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -48,7 +48,7 @@ public function extend(Builder $builder) */ protected function getDeletedAtColumn(Builder $builder) { - if (count((array) $builder->getQuery()->joins) > 0) { + if ((array) $builder->getQuery()->joins !== []) { return $builder->getModel()->getQualifiedDeletedAtColumn(); } diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 91084fc91073..47dc82916034 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -183,7 +183,7 @@ public function runPending(array $migrations, array $options = []) // First we will just make sure that there are any migrations to run. If there // aren't, we will just make a note of it to the developer so they're aware // that all of the migrations have been run against this database system. - if (count($migrations) === 0) { + if ($migrations === []) { $this->fireMigrationEvent(new NoPendingMigrations('up')); $this->write(Info::class, 'Nothing to migrate'); @@ -362,7 +362,7 @@ public function reset($paths = [], $pretend = false) // the database back into its "empty" state ready for the migrations. $migrations = array_reverse($this->repository->getRan()); - if (count($migrations) === 0) { + if ($migrations === []) { $this->write(Info::class, 'Nothing to rollback.'); return []; diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index cbb1b4655ba4..129c63b3e9b8 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -529,7 +529,7 @@ public function distinct() { $columns = func_get_args(); - if (count($columns) > 0) { + if ($columns !== []) { $this->distinct = is_array($columns[0]) || is_bool($columns[0]) ? $columns[0] : $columns; } else { $this->distinct = true; @@ -3499,7 +3499,7 @@ public function value($column) { $result = (array) $this->first([$column]); - return count($result) > 0 ? array_first($result) : null; + return $result !== [] ? array_first($result) : null; } /** @@ -3512,7 +3512,7 @@ public function rawValue(string $expression, array $bindings = []) { $result = (array) $this->selectRaw($expression, $bindings)->first(); - return count($result) > 0 ? array_first($result) : null; + return $result !== [] ? array_first($result) : null; } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index e219d391b4dc..21f2efe330a8 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -748,7 +748,7 @@ public function environmentFilePath() */ public function environment(...$environments) { - if (count($environments) > 0) { + if ($environments !== []) { $patterns = is_array($environments[0]) ? $environments[0] : $environments; return Str::is($patterns, $this['env']); diff --git a/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php b/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php index 6e037f189b27..3f49e6c3baae 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php @@ -170,7 +170,7 @@ public function callable() */ public function args() { - if (! isset($this->frame['args']) || ! is_array($this->frame['args']) || count($this->frame['args']) === 0) { + if (! isset($this->frame['args']) || ! is_array($this->frame['args']) || $this->frame['args'] === []) { return []; } diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 606fbdb79124..72faf5b0e2ab 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -138,25 +138,25 @@ protected function configureFromAttributes() { $reflection = new ReflectionClass($this); - if (count($reflection->getAttributes(StopOnFirstFailure::class)) > 0) { + if ($reflection->getAttributes(StopOnFirstFailure::class) !== []) { $this->stopOnFirstFailure = true; } $redirectTo = $reflection->getAttributes(RedirectTo::class); - if (count($redirectTo) > 0) { + if ($redirectTo !== []) { $this->redirect = $redirectTo[0]->newInstance()->url; } $redirectToRoute = $reflection->getAttributes(RedirectToRoute::class); - if (count($redirectToRoute) > 0) { + if ($redirectToRoute !== []) { $this->redirectRoute = $redirectToRoute[0]->newInstance()->route; } $errorBag = $reflection->getAttributes(ErrorBag::class); - if (count($errorBag) > 0) { + if ($errorBag !== []) { $this->errorBag = $errorBag[0]->newInstance()->name; } } diff --git a/src/Illuminate/Foundation/Testing/Traits/CanConfigureMigrationCommands.php b/src/Illuminate/Foundation/Testing/Traits/CanConfigureMigrationCommands.php index 2e5029d527ec..72b17a77ec89 100644 --- a/src/Illuminate/Foundation/Testing/Traits/CanConfigureMigrationCommands.php +++ b/src/Illuminate/Foundation/Testing/Traits/CanConfigureMigrationCommands.php @@ -56,7 +56,7 @@ protected function shouldSeed() $class = new ReflectionClass($this); do { - if (count($class->getAttributes(Seed::class)) > 0) { + if ($class->getAttributes(Seed::class) !== []) { return true; } } while ($class = $class->getParentClass()); diff --git a/src/Illuminate/Http/Concerns/InteractsWithInput.php b/src/Illuminate/Http/Concerns/InteractsWithInput.php index d3805ee07d54..4b5339f71f6b 100644 --- a/src/Illuminate/Http/Concerns/InteractsWithInput.php +++ b/src/Illuminate/Http/Concerns/InteractsWithInput.php @@ -293,7 +293,7 @@ public function dump($keys = []) { $keys = is_array($keys) ? $keys : func_get_args(); - dump(count($keys) > 0 ? $this->only($keys) : $this->all()); + dump($keys !== [] ? $this->only($keys) : $this->all()); return $this; } diff --git a/src/Illuminate/Http/Resources/CollectsResources.php b/src/Illuminate/Http/Resources/CollectsResources.php index a03960d6b431..3d0d92b976cc 100644 --- a/src/Illuminate/Http/Resources/CollectsResources.php +++ b/src/Illuminate/Http/Resources/CollectsResources.php @@ -62,7 +62,7 @@ protected function collects() if (! array_key_exists(static::class, static::$cachedCollectsAttributes)) { $attribute = (new ReflectionClass($this))->getAttributes(Collects::class); - static::$cachedCollectsAttributes[static::class] = count($attribute) > 0 + static::$cachedCollectsAttributes[static::class] = $attribute !== [] ? $attribute[0]->newInstance()->class : false; } diff --git a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php index 661ec0a44082..f37f9e1103a7 100644 --- a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php +++ b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php @@ -95,9 +95,7 @@ protected function removeMissingValues($data) } if (! array_key_exists(static::class, static::$cachedPreserveKeysAttributes)) { - static::$cachedPreserveKeysAttributes[static::class] = count( - (new ReflectionClass($this))->getAttributes(PreserveKeys::class) - ) > 0; + static::$cachedPreserveKeysAttributes[static::class] = (new ReflectionClass($this))->getAttributes(PreserveKeys::class) !== []; } if (static::$cachedPreserveKeysAttributes[static::class]) { diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index bda2348e5ee7..538395e3b2c9 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -89,9 +89,7 @@ public static function collection($resource) { return tap(static::newCollection($resource), function ($collection) { if (! array_key_exists(static::class, static::$cachedPreserveKeysAttributes)) { - static::$cachedPreserveKeysAttributes[static::class] = count( - (new ReflectionClass(static::class))->getAttributes(PreserveKeys::class) - ) > 0; + static::$cachedPreserveKeysAttributes[static::class] = (new ReflectionClass(static::class))->getAttributes(PreserveKeys::class) !== []; } if (static::$cachedPreserveKeysAttributes[static::class]) { diff --git a/src/Illuminate/JsonSchema/Serializer.php b/src/Illuminate/JsonSchema/Serializer.php index 7750caebd403..0c60c8382771 100644 --- a/src/Illuminate/JsonSchema/Serializer.php +++ b/src/Illuminate/JsonSchema/Serializer.php @@ -58,7 +58,7 @@ public static function serialize(Types\Type $type): array static fn (Types\Type $property) => static::isRequired($property), )); - if (count($required) > 0) { + if ($required !== []) { $attributes['required'] = $required; } diff --git a/src/Illuminate/Routing/Exceptions/UrlGenerationException.php b/src/Illuminate/Routing/Exceptions/UrlGenerationException.php index eadda8010c0f..c83a94a53d4b 100644 --- a/src/Illuminate/Routing/Exceptions/UrlGenerationException.php +++ b/src/Illuminate/Routing/Exceptions/UrlGenerationException.php @@ -26,7 +26,7 @@ public static function forMissingParameters(Route $route, array $parameters = [] $route->uri() ); - if (count($parameters) > 0) { + if ($parameters !== []) { $message .= sprintf(' [Missing %s: %s]', $parameterLabel, implode(', ', $parameters)); } diff --git a/src/Illuminate/Routing/RouteUrlGenerator.php b/src/Illuminate/Routing/RouteUrlGenerator.php index 245c4c01156d..3859a3fb3e78 100644 --- a/src/Illuminate/Routing/RouteUrlGenerator.php +++ b/src/Illuminate/Routing/RouteUrlGenerator.php @@ -233,7 +233,7 @@ protected function formatParameters(Route $route, $parameters) $offset = 0; $emptyParameters = array_filter($namedParameters, static fn ($val) => $val === ''); - if (count($requiredRouteParametersWithoutDefaultsOrNamedParameters) !== 0 && + if ($requiredRouteParametersWithoutDefaultsOrNamedParameters !== [] && count($parameters) !== count($emptyParameters)) { // Find the index of the first required parameter... $offset = array_search($requiredRouteParametersWithoutDefaultsOrNamedParameters[0], array_keys($namedParameters)); @@ -250,7 +250,7 @@ protected function formatParameters(Route $route, $parameters) if ($offset < 0) { $offset = 0; } - } elseif (count($requiredRouteParametersWithoutDefaultsOrNamedParameters) === 0 && count($parameters) !== 0) { + } elseif ($requiredRouteParametersWithoutDefaultsOrNamedParameters === [] && count($parameters) !== 0) { // Handle the case where all passed parameters are for parameters that have default values... $remainingCount = count($parameters); @@ -401,7 +401,7 @@ protected function getRouteQueryString(array $parameters) // First we will get all of the string parameters that are remaining after we // have replaced the route wildcards. We'll then build a query string from // these string parameters then use it as a starting point for the rest. - if (count($parameters) === 0) { + if ($parameters === []) { return ''; } diff --git a/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php b/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php index e384b56bdca9..f6e560013c45 100644 --- a/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php +++ b/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php @@ -176,7 +176,7 @@ public function report($e) */ protected function isFakedException(Throwable $e) { - return count($this->exceptions) === 0 || in_array(get_class($e), $this->exceptions, true); + return $this->exceptions === [] || in_array(get_class($e), $this->exceptions, true); } /** diff --git a/src/Illuminate/Support/Traits/ReadsClassAttributes.php b/src/Illuminate/Support/Traits/ReadsClassAttributes.php index f5955c8e3764..9860f312ed2a 100644 --- a/src/Illuminate/Support/Traits/ReadsClassAttributes.php +++ b/src/Illuminate/Support/Traits/ReadsClassAttributes.php @@ -51,6 +51,6 @@ protected function extractAttributeValue($instance) { $properties = get_object_vars($instance); - return count($properties) === 0 ? true : reset($properties); + return $properties === [] ? true : reset($properties); } } diff --git a/src/Illuminate/Support/ValidatedInput.php b/src/Illuminate/Support/ValidatedInput.php index f089f4561a78..9cbcbf2ddba3 100644 --- a/src/Illuminate/Support/ValidatedInput.php +++ b/src/Illuminate/Support/ValidatedInput.php @@ -119,7 +119,7 @@ public function file($key, $default = null) */ public function dump(...$keys) { - dump(count($keys) > 0 ? $this->only($keys) : $this->all()); + dump($keys !== [] ? $this->only($keys) : $this->all()); return $this; } diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index 055c2a23ca49..7465895b0a1e 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -252,7 +252,7 @@ protected function getLine($namespace, $group, $locale, $item, array $replace) if (is_string($line)) { return $this->makeReplacements($line, $replace); - } elseif (is_array($line) && count($line) > 0) { + } elseif (is_array($line) && $line !== []) { array_walk_recursive($line, function (&$value, $key) use ($replace) { $value = $this->makeReplacements($value, $replace); }); diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 6bf975678401..35492c65b6e6 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -127,7 +127,7 @@ public function validateActiveUrl($attribute, $value) try { $records = $this->getDnsRecords($url.'.', DNS_A | DNS_AAAA); - if (is_array($records) && count($records) > 0) { + if (is_array($records) && $records !== []) { return true; } } catch (Exception) { @@ -1517,7 +1517,7 @@ public function validateIn($attribute, $value, $parameters) } } - return count(array_diff($value, $parameters)) === 0; + return array_diff($value, $parameters) === []; } return ! is_array($value) && in_array((string) $value, $parameters); diff --git a/src/Illuminate/Validation/Rules/File.php b/src/Illuminate/Validation/Rules/File.php index b3968e035a37..f3d50db9a8f2 100644 --- a/src/Illuminate/Validation/Rules/File.php +++ b/src/Illuminate/Validation/Rules/File.php @@ -341,11 +341,11 @@ protected function buildMimetypes() $mimes = array_diff($this->allowedMimetypes, $mimetypes); - if (count($mimetypes) > 0) { + if ($mimetypes !== []) { $rules[] = 'mimetypes:'.implode(',', $mimetypes); } - if (count($mimes) > 0) { + if ($mimes !== []) { $rules[] = 'mimes:'.implode(',', $mimes); } diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 6f6f6f71b720..7ea1224a6d82 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -307,7 +307,7 @@ public function compileString($value) // If there are any footer lines that need to get added to a template we will // add them here at the end of the template. This gets used mainly for the // template inheritance via the extends keyword that should be appended. - if (count($this->footer) > 0) { + if ($this->footer !== []) { $result = $this->addFooters($result); } diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesLoops.php b/src/Illuminate/View/Compilers/Concerns/CompilesLoops.php index ab6d867d0529..2c91ad871e08 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesLoops.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesLoops.php @@ -27,7 +27,7 @@ protected function compileForelse($expression) preg_match('/\( *(.+) +as +(.+)\)$/is', $expression ?? '', $matches); - if (count($matches) === 0) { + if ($matches === []) { throw new ViewCompilationException('Malformed @forelse statement.'); } @@ -102,7 +102,7 @@ protected function compileForeach($expression) { preg_match('/\( *(.+) +as +(.*)\)$/is', $expression ?? '', $matches); - if (count($matches) === 0) { + if ($matches === []) { throw new ViewCompilationException('Malformed @foreach statement.'); } diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index c27bc90e7457..3b84497a76f0 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -106,8 +106,8 @@ public function testProcessPoolCanReceiveOutputForEachProcessViaStartMethod() $poolResults = $pool->wait(); - $this->assertTrue(count($output[0]['out']) > 0); - $this->assertTrue(count($output[1]['out']) > 0); + $this->assertTrue($output[0]['out'] !== []); + $this->assertTrue($output[1]['out'] !== []); $this->assertInstanceOf(ProcessResult::class, $poolResults[0]); $this->assertInstanceOf(ProcessResult::class, $poolResults[1]); $this->assertTrue(str_contains($poolResults[0]->output(), 'ProcessTest.php')); From 0c22f7bb9d03fcafa653718830bff15b6259c3d4 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Wed, 15 Apr 2026 14:58:39 +0200 Subject: [PATCH 162/596] [13.x] testsuite (#59702) * Add rules for testsuite * Modernize assertions and mocks --------- Co-authored-by: Lucas Michot --- rector.php | 48 +++++ tests/Auth/AuthGuardTest.php | 14 +- ...ficationNotificationHandleFunctionTest.php | 4 +- tests/Cache/CacheApcStoreTest.php | 16 +- tests/Cache/CacheArrayStoreTest.php | 2 +- tests/Cache/CacheDatabaseStoreTest.php | 2 +- tests/Cache/CacheFileStoreTest.php | 82 ++++---- tests/Cache/CacheMemcachedStoreTest.php | 10 +- tests/Cache/CacheSessionStoreTest.php | 2 +- tests/Console/ConsoleApplicationTest.php | 6 +- tests/Console/Scheduling/ScheduleTest.php | 19 +- tests/Container/ContainerExtendTest.php | 12 +- tests/Database/DatabaseConnectionTest.php | 34 ++-- tests/Database/DatabaseConnectorTest.php | 60 +++--- tests/Database/DatabaseEloquentModelTest.php | 10 +- .../DatabaseMariaDbSchemaStateTest.php | 4 +- .../Database/DatabaseMigrationCreatorTest.php | 8 +- .../DatabaseMigrationMigrateCommandTest.php | 2 +- .../Database/DatabaseMySqlSchemaStateTest.php | 8 +- tests/Database/DatabaseProcessorTest.php | 2 +- tests/Filesystem/FilesystemAdapterTest.php | 8 +- tests/Http/HttpClientTest.php | 16 +- tests/Integration/Database/AfterQueryTest.php | 16 +- .../Database/DatabaseConnectionsTest.php | 10 +- tests/Mail/MailableQueuedTest.php | 3 - tests/Queue/QueueDatabaseQueueUnitTest.php | 10 +- tests/Support/DateFacadeTest.php | 10 +- tests/Testing/AssertRedirectToActionTest.php | 15 +- tests/Testing/TestResponseTest.php | 10 +- .../Translation/TranslationTranslatorTest.php | 28 +-- tests/Validation/ValidationValidatorTest.php | 192 +++++++++--------- 31 files changed, 346 insertions(+), 317 deletions(-) diff --git a/rector.php b/rector.php index 1a1a1697059f..106dd8ec12a3 100644 --- a/rector.php +++ b/rector.php @@ -34,8 +34,55 @@ use Rector\Php83\Rector\ClassConst\AddTypeToConstRector; use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector; use Rector\Php83\Rector\FuncCall\DynamicClassConstFetchRector; +use Rector\PHPUnit\CodeQuality\Rector\CallLike\DirectInstanceOverMockArgRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\ConstructClassMethodToSetUpTestCaseRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\NarrowUnusedSetUpDefinedPropertyRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\RemoveNeverUsedMockPropertyRector; +use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\EntityDocumentCreateMockToDirectNewRector; +use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\RemoveEmptyTestMethodRector; +use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\RemoveStandaloneCreateMockRector; +use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\ReplaceTestAnnotationWithPrefixedFunctionRector; +use Rector\PHPUnit\CodeQuality\Rector\Foreach_\SimplifyForeachInstanceOfRector; +use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\MergeWithCallableAndWillReturnRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\NarrowIdenticalWithConsecutiveRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\NarrowSingleWillReturnCallbackRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\RemoveExpectAnyFromMockRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\SimplerWithIsInstanceOfRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\SingleWithConsecutiveToWithRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWillMethodRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWithMethodRector; +use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; +use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; +$testsuiteRules = [ + AssertFuncCallToPHPUnitAssertRector::class, + ConstructClassMethodToSetUpTestCaseRector::class, + DirectInstanceOverMockArgRector::class, + EntityDocumentCreateMockToDirectNewRector::class, + GetMockBuilderGetMockToCreateMockRector::class, + InlineStubPropertyToCreateStubMethodCallRector::class, + MergeWithCallableAndWillReturnRector::class, + NarrowIdenticalWithConsecutiveRector::class, + NarrowSingleWillReturnCallbackRector::class, + NarrowUnusedSetUpDefinedPropertyRector::class, + PreferPHPUnitThisCallRector::class, + RemoveEmptyTestMethodRector::class, + RemoveExpectAnyFromMockRector::class, + RemoveNeverUsedMockPropertyRector::class, + RemoveStandaloneCreateMockRector::class, + ReplaceAtMethodWithDesiredMatcherRector::class, + ReplaceTestAnnotationWithPrefixedFunctionRector::class, + SimplerWithIsInstanceOfRector::class, + SimplifyForeachInstanceOfRector::class, + SingleWithConsecutiveToWithRector::class, + UseSpecificWillMethodRector::class, + UseSpecificWithMethodRector::class, +]; + return RectorConfig::configure() ->withRootFiles() ->withPaths([ @@ -78,6 +125,7 @@ 'tests/Foundation/fixtures/bad-syntax-strategy.php', ]) ->withRules([ + ...$testsuiteRules, CountArrayToEmptyArrayComparisonRector::class, StrlenZeroToIdenticalEmptyStringRector::class, ]) diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php index f8f4bde1aa58..aa5fb62bfd7e 100755 --- a/tests/Auth/AuthGuardTest.php +++ b/tests/Auth/AuthGuardTest.php @@ -116,7 +116,7 @@ public function testAttemptReturnsUserInterface() $guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->andReturn($user); $guard->getProvider()->shouldReceive('validateCredentials')->with($user, ['foo'])->andReturn(true); $guard->getProvider()->shouldReceive('rehashPasswordIfRequired')->with($user, ['foo'])->once(); - $guard->expects($this->once())->method('login')->with($this->equalTo($user)); + $guard->expects($this->once())->method('login')->with($user); $this->assertTrue($guard->attempt(['foo'])); } @@ -160,15 +160,15 @@ public function testAttemptAndWithCallbacks() $mock->getProvider()->shouldReceive('rehashPasswordIfRequired')->with($user, ['foo'])->once(); $this->assertTrue($mock->attemptWhen(['foo'], function ($user, $guard) { - static::assertInstanceOf(Authenticatable::class, $user); - static::assertInstanceOf(SessionGuard::class, $guard); + $this->assertInstanceOf(Authenticatable::class, $user); + $this->assertInstanceOf(SessionGuard::class, $guard); return true; })); $this->assertFalse($mock->attemptWhen(['foo'], function ($user, $guard) { - static::assertInstanceOf(Authenticatable::class, $user); - static::assertInstanceOf(SessionGuard::class, $guard); + $this->assertInstanceOf(Authenticatable::class, $user); + $this->assertInstanceOf(SessionGuard::class, $guard); return false; })); @@ -196,7 +196,7 @@ public function testAttemptRehashesPasswordWhenRequired() $guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->andReturn($user); $guard->getProvider()->shouldReceive('validateCredentials')->with($user, ['foo'])->andReturn(true); $guard->getProvider()->shouldReceive('rehashPasswordIfRequired')->with($user, ['foo'])->once(); - $guard->expects($this->once())->method('login')->with($this->equalTo($user)); + $guard->expects($this->once())->method('login')->with($user); $this->assertTrue($guard->attempt(['foo'])); } @@ -216,7 +216,7 @@ public function testAttemptDoesntRehashPasswordWhenDisabled() $guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->andReturn($user); $guard->getProvider()->shouldReceive('validateCredentials')->with($user, ['foo'])->andReturn(true); $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); - $guard->expects($this->once())->method('login')->with($this->equalTo($user)); + $guard->expects($this->once())->method('login')->with($user); $this->assertTrue($guard->attempt(['foo'])); } diff --git a/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php b/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php index 9ef72a9fd726..c44c956db83c 100644 --- a/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php +++ b/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php @@ -16,7 +16,7 @@ class AuthListenersSendEmailVerificationNotificationHandleFunctionTest extends T */ public function testWillExecuted() { - $user = $this->getMockBuilder(MustVerifyEmail::class)->getMock(); + $user = $this->createMock(MustVerifyEmail::class); $user->method('hasVerifiedEmail')->willReturn(false); $user->expects($this->once())->method('sendEmailVerificationNotification'); @@ -43,7 +43,7 @@ public function testUserIsNotInstanceOfMustVerifyEmail() */ public function testHasVerifiedEmailAsTrue() { - $user = $this->getMockBuilder(MustVerifyEmail::class)->getMock(); + $user = $this->createMock(MustVerifyEmail::class); $user->method('hasVerifiedEmail')->willReturn(true); $user->expects($this->never())->method('sendEmailVerificationNotification'); diff --git a/tests/Cache/CacheApcStoreTest.php b/tests/Cache/CacheApcStoreTest.php index 6343ecb641be..c428445f88e3 100755 --- a/tests/Cache/CacheApcStoreTest.php +++ b/tests/Cache/CacheApcStoreTest.php @@ -12,7 +12,7 @@ class CacheApcStoreTest extends TestCase public function testGetReturnsNullWhenNotFound() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['get'])->getMock(); - $apc->expects($this->once())->method('get')->with($this->equalTo('foobar'))->willReturn(null); + $apc->expects($this->once())->method('get')->with('foobar')->willReturn(null); $store = new ApcStore($apc, 'foo'); $this->assertNull($store->get('bar')); } @@ -53,7 +53,7 @@ public function testSetMethodProperlyCallsAPC() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['put'])->getMock(); $apc->expects($this->once()) - ->method('put')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo(60)) + ->method('put')->with('foo', 'bar', 60) ->willReturn(true); $store = new ApcStore($apc); $result = $store->put('foo', 'bar', 60); @@ -91,7 +91,7 @@ public function testSetMultipleMethodProperlyCallsAPC() public function testIncrementMethodProperlyCallsAPC() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['increment'])->getMock(); - $apc->expects($this->once())->method('increment')->with($this->equalTo('foo'), $this->equalTo(5)); + $apc->expects($this->once())->method('increment')->with('foo', 5); $store = new ApcStore($apc); $store->increment('foo', 5); } @@ -99,7 +99,7 @@ public function testIncrementMethodProperlyCallsAPC() public function testDecrementMethodProperlyCallsAPC() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['decrement'])->getMock(); - $apc->expects($this->once())->method('decrement')->with($this->equalTo('foo'), $this->equalTo(5)); + $apc->expects($this->once())->method('decrement')->with('foo', 5); $store = new ApcStore($apc); $store->decrement('foo', 5); } @@ -108,7 +108,7 @@ public function testStoreItemForeverProperlyCallsAPC() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['put'])->getMock(); $apc->expects($this->once()) - ->method('put')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo(0)) + ->method('put')->with('foo', 'bar', 0) ->willReturn(true); $store = new ApcStore($apc); $result = $store->forever('foo', 'bar'); @@ -118,7 +118,7 @@ public function testStoreItemForeverProperlyCallsAPC() public function testForgetMethodProperlyCallsAPC() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['delete'])->getMock(); - $apc->expects($this->once())->method('delete')->with($this->equalTo('foo'))->willReturn(true); + $apc->expects($this->once())->method('delete')->with('foo')->willReturn(true); $store = new ApcStore($apc); $result = $store->forget('foo'); $this->assertTrue($result); @@ -131,8 +131,8 @@ public function testTouchMethodProperlyCallsAPC(): void $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['get', 'put'])->getMock(); - $apc->expects($this->once())->method('get')->with($this->equalTo($key))->willReturn('bar'); - $apc->expects($this->once())->method('put')->with($this->equalTo($key), $this->equalTo('bar'), $this->equalTo($ttl))->willReturn(true); + $apc->expects($this->once())->method('get')->with($key)->willReturn('bar'); + $apc->expects($this->once())->method('put')->with($key, 'bar', $ttl)->willReturn(true); $this->assertTrue((new ApcStore($apc))->touch($key, $ttl)); } diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index a45aa842dcbf..98a8cadceb77 100755 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -90,7 +90,7 @@ public function testStoreItemForeverProperlyStoresInArray() { $mock = $this->getMockBuilder(ArrayStore::class)->onlyMethods(['put'])->getMock(); $mock->expects($this->once()) - ->method('put')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo(0)) + ->method('put')->with('foo', 'bar', 0) ->willReturn(true); $result = $mock->forever('foo', 'bar'); $this->assertTrue($result); diff --git a/tests/Cache/CacheDatabaseStoreTest.php b/tests/Cache/CacheDatabaseStoreTest.php index e998ed9f287a..18d9d8770107 100755 --- a/tests/Cache/CacheDatabaseStoreTest.php +++ b/tests/Cache/CacheDatabaseStoreTest.php @@ -114,7 +114,7 @@ public function testValueIsUpsertedOnSqlite() public function testForeverCallsStoreItemWithReallyLongTime() { $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['put'])->setConstructorArgs($this->getMocks())->getMock(); - $store->expects($this->once())->method('put')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo(315360000))->willReturn(true); + $store->expects($this->once())->method('put')->with('foo', 'bar', 315360000)->willReturn(true); $result = $store->forever('foo', 'bar'); $this->assertTrue($result); } diff --git a/tests/Cache/CacheFileStoreTest.php b/tests/Cache/CacheFileStoreTest.php index 892854e3d761..04fc171a23b1 100755 --- a/tests/Cache/CacheFileStoreTest.php +++ b/tests/Cache/CacheFileStoreTest.php @@ -24,7 +24,7 @@ protected function tearDown(): void public function testNullIsReturnedIfFileDoesntExist() { $files = $this->mockFilesystem(); - $files->expects($this->once())->method('get')->will($this->throwException(new FileNotFoundException)); + $files->expects($this->once())->method('get')->willThrowException(new FileNotFoundException); $store = new FileStore($files, __DIR__); $value = $store->get('foo'); $this->assertNull($value); @@ -36,8 +36,8 @@ public function testPutCreatesMissingDirectories() $hash = sha1('foo'); $contents = '0000000000'; $full_dir = __DIR__.'/'.substr($hash, 0, 2).'/'.substr($hash, 2, 2); - $files->expects($this->once())->method('makeDirectory')->with($this->equalTo($full_dir), $this->equalTo(0777), $this->equalTo(true)); - $files->expects($this->once())->method('put')->with($this->equalTo($full_dir.'/'.$hash))->willReturn(strlen($contents)); + $files->expects($this->once())->method('makeDirectory')->with($full_dir, 0777, true); + $files->expects($this->once())->method('put')->with($full_dir.'/'.$hash)->willReturn(strlen($contents)); $store = new FileStore($files, __DIR__); $result = $store->put('foo', $contents, 0); $this->assertTrue($result); @@ -54,9 +54,9 @@ public function testPutWillConsiderZeroAsEternalTime() $exclusiveLock = true; $files->expects($this->once())->method('put')->with( - $this->equalTo($filePath), - $this->equalTo($fileContents), - $this->equalTo($exclusiveLock) // Ensure we do lock the file while putting. + $filePath, + $fileContents, + $exclusiveLock // Ensure we do lock the file while putting. )->willReturn(strlen($fileContents)); (new FileStore($files, __DIR__))->put('O--L / key', 'gold', 0); @@ -72,8 +72,8 @@ public function testPutWillConsiderBigValuesAsEternalTime() $fileContents = $ten9s.serialize('gold'); $files->expects($this->once())->method('put')->with( - $this->equalTo($filePath), - $this->equalTo($fileContents), + $filePath, + $fileContents, ); (new FileStore($files, __DIR__))->put('O--L / key', 'gold', (int) $ten9s + 1); @@ -103,11 +103,11 @@ public function testStoreItemProperlyStoresValues() { $files = $this->mockFilesystem(); $store = $this->getMockBuilder(FileStore::class)->onlyMethods(['expiration'])->setConstructorArgs([$files, __DIR__])->getMock(); - $store->expects($this->once())->method('expiration')->with($this->equalTo(10))->willReturn(1111111111); + $store->expects($this->once())->method('expiration')->with(10)->willReturn(1111111111); $contents = '1111111111'.serialize('Hello World'); $hash = sha1('foo'); $cache_dir = substr($hash, 0, 2).'/'.substr($hash, 2, 2); - $files->expects($this->once())->method('put')->with($this->equalTo(__DIR__.'/'.$cache_dir.'/'.$hash), $this->equalTo($contents))->willReturn(strlen($contents)); + $files->expects($this->once())->method('put')->with(__DIR__.'/'.$cache_dir.'/'.$hash, $contents)->willReturn(strlen($contents)); $result = $store->put('foo', 'Hello World', 10); $this->assertTrue($result); } @@ -127,7 +127,7 @@ public function testTouchExtendsTtl(): void $store->expects($this->once()) ->method('expiration') - ->with($this->equalTo($ttl)) + ->with($ttl) ->willReturn($now->clone()->addSeconds($ttl)->getTimestamp()); $store->expects($this->once()) ->method('getPayload') @@ -136,9 +136,9 @@ public function testTouchExtendsTtl(): void $files->expects($this->once()) ->method('put') ->with( - $this->equalTo($path), - $this->equalTo($now->clone()->addSeconds($ttl)->getTimestamp().serialize($content)), - $this->equalTo(true) + $path, + $now->clone()->addSeconds($ttl)->getTimestamp().serialize($content), + true ) ->willReturn(1); @@ -195,7 +195,7 @@ public function testForeversAreStoredWithHighTimestamp() $contents = '9999999999'.serialize('Hello World'); $hash = sha1('foo'); $cache_dir = substr($hash, 0, 2).'/'.substr($hash, 2, 2); - $files->expects($this->once())->method('put')->with($this->equalTo(__DIR__.'/'.$cache_dir.'/'.$hash), $this->equalTo($contents))->willReturn(strlen($contents)); + $files->expects($this->once())->method('put')->with(__DIR__.'/'.$cache_dir.'/'.$hash, $contents)->willReturn(strlen($contents)); $store = new FileStore($files, __DIR__); $result = $store->forever('foo', 'Hello World', 10); $this->assertTrue($result); @@ -223,8 +223,8 @@ public function testIncrementExpiredKeys() $valueAfterIncrement = '9999999999'.serialize(3); $store = new FileStore($files, __DIR__); - $files->expects($this->once())->method('get')->with($this->equalTo($filePath), $this->equalTo(true))->willReturn($initialValue); - $files->expects($this->once())->method('put')->with($this->equalTo($filePath), $this->equalTo($valueAfterIncrement)); + $files->expects($this->once())->method('get')->with($filePath, true)->willReturn($initialValue); + $files->expects($this->once())->method('put')->with($filePath, $valueAfterIncrement); $result = $store->increment('foo', 3); } @@ -237,8 +237,8 @@ public function testIncrementCanAtomicallyJump() $valueAfterIncrement = '9999999999'.serialize(4); $store = new FileStore($files, __DIR__); - $files->expects($this->once())->method('get')->with($this->equalTo($filePath), $this->equalTo(true))->willReturn($initialValue); - $files->expects($this->once())->method('put')->with($this->equalTo($filePath), $this->equalTo($valueAfterIncrement)); + $files->expects($this->once())->method('get')->with($filePath, true)->willReturn($initialValue); + $files->expects($this->once())->method('put')->with($filePath, $valueAfterIncrement); $result = $store->increment('foo', 3); $this->assertEquals(4, $result); @@ -253,8 +253,8 @@ public function testDecrementCanAtomicallyJump() $valueAfterIncrement = '9999999999'.serialize(0); $store = new FileStore($files, __DIR__); - $files->expects($this->once())->method('get')->with($this->equalTo($filePath), $this->equalTo(true))->willReturn($initialValue); - $files->expects($this->once())->method('put')->with($this->equalTo($filePath), $this->equalTo($valueAfterIncrement)); + $files->expects($this->once())->method('get')->with($filePath, true)->willReturn($initialValue); + $files->expects($this->once())->method('put')->with($filePath, $valueAfterIncrement); $result = $store->decrement('foo', 2); $this->assertEquals(0, $result); @@ -268,8 +268,8 @@ public function testIncrementNonNumericValues() $initialValue = '1999999909'.serialize('foo'); $valueAfterIncrement = '1999999909'.serialize(1); $store = new FileStore($files, __DIR__); - $files->expects($this->once())->method('get')->with($this->equalTo($filePath), $this->equalTo(true))->willReturn($initialValue); - $files->expects($this->once())->method('put')->with($this->equalTo($filePath), $this->equalTo($valueAfterIncrement)); + $files->expects($this->once())->method('get')->with($filePath, true)->willReturn($initialValue); + $files->expects($this->once())->method('put')->with($filePath, $valueAfterIncrement); $result = $store->increment('foo'); $this->assertEquals(1, $result); @@ -283,8 +283,8 @@ public function testIncrementNonExistentKeys() $valueAfterIncrement = '9999999999'.serialize(1); $store = new FileStore($files, __DIR__); // simulates a missing item in file store by the exception - $files->expects($this->once())->method('get')->with($this->equalTo($filePath), $this->equalTo(true))->willThrowException(new Exception); - $files->expects($this->once())->method('put')->with($this->equalTo($filePath), $this->equalTo($valueAfterIncrement)); + $files->expects($this->once())->method('get')->with($filePath, true)->willThrowException(new Exception); + $files->expects($this->once())->method('put')->with($filePath, $valueAfterIncrement); $result = $store->increment('foo'); $this->assertIsInt($result); $this->assertEquals(1, $result); @@ -302,7 +302,7 @@ public function testIncrementDoesNotExtendCacheLife() $files->expects($this->once())->method('get')->willReturn($initialValue); $hash = sha1('foo'); $cache_dir = substr($hash, 0, 2).'/'.substr($hash, 2, 2); - $files->expects($this->once())->method('put')->with($this->equalTo(__DIR__.'/'.$cache_dir.'/'.$hash), $this->equalTo($valueAfterIncrement)); + $files->expects($this->once())->method('put')->with(__DIR__.'/'.$cache_dir.'/'.$hash, $valueAfterIncrement); $store->increment('foo'); } @@ -311,7 +311,7 @@ public function testRemoveDeletesFileDoesntExist() $files = $this->mockFilesystem(); $hash = sha1('foobull'); $cache_dir = substr($hash, 0, 2).'/'.substr($hash, 2, 2); - $files->expects($this->once())->method('exists')->with($this->equalTo(__DIR__.'/'.$cache_dir.'/'.$hash))->willReturn(false); + $files->expects($this->once())->method('exists')->with(__DIR__.'/'.$cache_dir.'/'.$hash)->willReturn(false); $store = new FileStore($files, __DIR__); $store->forget('foobull'); } @@ -332,9 +332,9 @@ public function testRemoveDeletesFile() public function testFlushCleansDirectory() { $files = $this->mockFilesystem(); - $files->expects($this->once())->method('isDirectory')->with($this->equalTo(__DIR__))->willReturn(true); - $files->expects($this->once())->method('directories')->with($this->equalTo(__DIR__))->willReturn(['foo']); - $files->expects($this->once())->method('deleteDirectory')->with($this->equalTo('foo'))->willReturn(true); + $files->expects($this->once())->method('isDirectory')->with(__DIR__)->willReturn(true); + $files->expects($this->once())->method('directories')->with(__DIR__)->willReturn(['foo']); + $files->expects($this->once())->method('deleteDirectory')->with('foo')->willReturn(true); $store = new FileStore($files, __DIR__); $result = $store->flush(); @@ -344,9 +344,9 @@ public function testFlushCleansDirectory() public function testFlushFailsDirectoryClean() { $files = $this->mockFilesystem(); - $files->expects($this->once())->method('isDirectory')->with($this->equalTo(__DIR__))->willReturn(true); - $files->expects($this->once())->method('directories')->with($this->equalTo(__DIR__))->willReturn(['foo']); - $files->expects($this->once())->method('deleteDirectory')->with($this->equalTo('foo'))->willReturn(false); + $files->expects($this->once())->method('isDirectory')->with(__DIR__)->willReturn(true); + $files->expects($this->once())->method('directories')->with(__DIR__)->willReturn(['foo']); + $files->expects($this->once())->method('deleteDirectory')->with('foo')->willReturn(false); $store = new FileStore($files, __DIR__); $result = $store->flush(); @@ -356,7 +356,7 @@ public function testFlushFailsDirectoryClean() public function testFlushIgnoreNonExistingDirectory() { $files = $this->mockFilesystem(); - $files->expects($this->once())->method('isDirectory')->with($this->equalTo(__DIR__.'--wrong'))->willReturn(false); + $files->expects($this->once())->method('isDirectory')->with(__DIR__.'--wrong')->willReturn(false); $store = new FileStore($files, __DIR__.'--wrong'); $result = $store->flush(); @@ -367,9 +367,9 @@ public function testFlushingLocksCleansDirectory() { $lockDir = __DIR__.'/locks'; $files = $this->mockFilesystem(); - $files->expects($this->once())->method('isDirectory')->with($this->equalTo($lockDir))->willReturn(true); - $files->expects($this->once())->method('directories')->with($this->equalTo($lockDir))->willReturn(['foo']); - $files->expects($this->once())->method('deleteDirectory')->with($this->equalTo('foo'))->willReturn(true); + $files->expects($this->once())->method('isDirectory')->with($lockDir)->willReturn(true); + $files->expects($this->once())->method('directories')->with($lockDir)->willReturn(['foo']); + $files->expects($this->once())->method('deleteDirectory')->with('foo')->willReturn(true); $store = new FileStore($files, __DIR__); $store->setLockDirectory($lockDir); @@ -381,9 +381,9 @@ public function testFlushingLocksFailsDirectoryClean() { $lockDir = __DIR__.'/locks'; $files = $this->mockFilesystem(); - $files->expects($this->once())->method('isDirectory')->with($this->equalTo($lockDir))->willReturn(true); - $files->expects($this->once())->method('directories')->with($this->equalTo($lockDir))->willReturn(['foo']); - $files->expects($this->once())->method('deleteDirectory')->with($this->equalTo('foo'))->willReturn(false); + $files->expects($this->once())->method('isDirectory')->with($lockDir)->willReturn(true); + $files->expects($this->once())->method('directories')->with($lockDir)->willReturn(['foo']); + $files->expects($this->once())->method('deleteDirectory')->with('foo')->willReturn(false); $store = new FileStore($files, __DIR__); $store->setLockDirectory($lockDir); @@ -395,7 +395,7 @@ public function testFlushingLocksIgnoreNonExistingDirectory() { $lockDir = __DIR__.'/locks'; $files = $this->mockFilesystem(); - $files->expects($this->once())->method('isDirectory')->with($this->equalTo($lockDir))->willReturn(false); + $files->expects($this->once())->method('isDirectory')->with($lockDir)->willReturn(false); $store = new FileStore($files, __DIR__); $store->setLockDirectory($lockDir); diff --git a/tests/Cache/CacheMemcachedStoreTest.php b/tests/Cache/CacheMemcachedStoreTest.php index c3046087170b..0afcfcc0f23f 100755 --- a/tests/Cache/CacheMemcachedStoreTest.php +++ b/tests/Cache/CacheMemcachedStoreTest.php @@ -15,7 +15,7 @@ class CacheMemcachedStoreTest extends TestCase public function testGetReturnsNullWhenNotFound() { $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['get', 'getResultCode'])->getMock(); - $memcache->expects($this->once())->method('get')->with($this->equalTo('foo:bar'))->willReturn(null); + $memcache->expects($this->once())->method('get')->with('foo:bar')->willReturn(null); $memcache->expects($this->once())->method('getResultCode')->willReturn(1); $store = new MemcachedStore($memcache, 'foo:'); $this->assertNull($store->get('bar')); @@ -53,7 +53,7 @@ public function testSetMethodProperlyCallsMemcache() { Carbon::setTestNow($now = Carbon::now()); $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['set'])->getMock(); - $memcache->expects($this->once())->method('set')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo($now->timestamp + 60))->willReturn(true); + $memcache->expects($this->once())->method('set')->with('foo', 'bar', $now->timestamp + 60)->willReturn(true); $store = new MemcachedStore($memcache); $result = $store->put('foo', 'bar', 60); $this->assertTrue($result); @@ -69,7 +69,7 @@ public function testTouchMethodProperlyCallsMemcache(): void $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['touch'])->getMock(); - $memcache->expects($this->once())->method('touch')->with($this->equalTo($key), $this->equalTo($now->addSeconds($ttl)->getTimestamp()))->willReturn(true); + $memcache->expects($this->once())->method('touch')->with($key, $now->addSeconds($ttl)->getTimestamp())->willReturn(true); $this->assertTrue((new MemcachedStore($memcache))->touch($key, $ttl)); } @@ -95,7 +95,7 @@ public function testDecrementMethodProperlyCallsMemcache() public function testStoreItemForeverProperlyCallsMemcached() { $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['set'])->getMock(); - $memcache->expects($this->once())->method('set')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo(0))->willReturn(true); + $memcache->expects($this->once())->method('set')->with('foo', 'bar', 0)->willReturn(true); $store = new MemcachedStore($memcache); $result = $store->forever('foo', 'bar'); $this->assertTrue($result); @@ -104,7 +104,7 @@ public function testStoreItemForeverProperlyCallsMemcached() public function testForgetMethodProperlyCallsMemcache() { $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['delete'])->getMock(); - $memcache->expects($this->once())->method('delete')->with($this->equalTo('foo')); + $memcache->expects($this->once())->method('delete')->with('foo'); $store = new MemcachedStore($memcache); $store->forget('foo'); } diff --git a/tests/Cache/CacheSessionStoreTest.php b/tests/Cache/CacheSessionStoreTest.php index 9d26ff790680..89f1c230bc68 100755 --- a/tests/Cache/CacheSessionStoreTest.php +++ b/tests/Cache/CacheSessionStoreTest.php @@ -98,7 +98,7 @@ public function testStoreItemForeverProperlyStoresInArray() ->onlyMethods(['put']) ->getMock(); $mock->expects($this->once()) - ->method('put')->with($this->equalTo('foo'), $this->equalTo('bar'), $this->equalTo(0)) + ->method('put')->with('foo', 'bar', 0) ->willReturn(true); $result = $mock->forever('foo', 'bar'); $this->assertTrue($result); diff --git a/tests/Console/ConsoleApplicationTest.php b/tests/Console/ConsoleApplicationTest.php index f9fdd13aef84..5a82b48a23ea 100755 --- a/tests/Console/ConsoleApplicationTest.php +++ b/tests/Console/ConsoleApplicationTest.php @@ -42,7 +42,7 @@ public function testAddSetsLaravelInstance() $artisan = $this->getMockConsole(['addToParent']); $command = m::mock(Command::class); $command->shouldReceive('setLaravel')->once()->with(m::type(ApplicationContract::class)); - $artisan->expects($this->once())->method('addToParent')->with($this->equalTo($command))->willReturn($command); + $artisan->expects($this->once())->method('addToParent')->with($command)->willReturn($command); $result = $artisan->add($command); $this->assertSame($command, $result); @@ -53,7 +53,7 @@ public function testLaravelNotSetOnSymfonyCommands() $artisan = $this->getMockConsole(['addToParent']); $command = m::mock(SymfonyCommand::class); $command->shouldReceive('setLaravel')->never(); - $artisan->expects($this->once())->method('addToParent')->with($this->equalTo($command))->willReturn($command); + $artisan->expects($this->once())->method('addToParent')->with($command)->willReturn($command); $result = $artisan->add($command); $this->assertSame($command, $result); @@ -64,7 +64,7 @@ public function testResolveAddsCommandViaApplicationResolution() $artisan = $this->getMockConsole(['addToParent']); $command = m::mock(SymfonyCommand::class); $artisan->getLaravel()->shouldReceive('make')->once()->with('foo')->andReturn(m::mock(SymfonyCommand::class)); - $artisan->expects($this->once())->method('addToParent')->with($this->equalTo($command))->willReturn($command); + $artisan->expects($this->once())->method('addToParent')->with($command)->willReturn($command); $result = $artisan->resolve('foo'); $this->assertSame($command, $result); diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index a939aaefee29..3354974023db 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -11,7 +11,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Tests\Console\Fixtures\JobToTestWithSchedule; use Mockery as m; -use Mockery\MockInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -20,8 +19,6 @@ final class ScheduleTest extends TestCase { private Container $container; - private EventMutex&MockInterface $eventMutex; - private SchedulingMutex&MockInterface $schedulingMutex; protected function setUp(): void { @@ -29,10 +26,10 @@ protected function setUp(): void $this->container = new Container; Container::setInstance($this->container); - $this->eventMutex = m::mock(EventMutex::class); - $this->container->instance(EventMutex::class, $this->eventMutex); - $this->schedulingMutex = m::mock(SchedulingMutex::class); - $this->container->instance(SchedulingMutex::class, $this->schedulingMutex); + $eventMutex = m::mock(EventMutex::class); + $this->container->instance(EventMutex::class, $eventMutex); + $schedulingMutex = m::mock(SchedulingMutex::class); + $this->container->instance(SchedulingMutex::class, $schedulingMutex); } #[DataProvider('jobHonoursDisplayNameIfMethodExistsProvider')] @@ -40,8 +37,8 @@ public function testJobHonoursDisplayNameIfMethodExists(object $job, string $job { $schedule = new Schedule(); $scheduledJob = $schedule->job($job); - self::assertSame($jobName, $scheduledJob->description); - self::assertFalse($this->container->resolved(JobToTestWithSchedule::class)); + $this->assertSame($jobName, $scheduledJob->description); + $this->assertFalse($this->container->resolved(JobToTestWithSchedule::class)); } public static function jobHonoursDisplayNameIfMethodExistsProvider(): array @@ -64,7 +61,7 @@ public function testJobIsNotInstantiatedIfSuppliedAsClassname(): void { $schedule = new Schedule(); $scheduledJob = $schedule->job(JobToTestWithSchedule::class); - self::assertSame(JobToTestWithSchedule::class, $scheduledJob->description); - self::assertFalse($this->container->resolved(JobToTestWithSchedule::class)); + $this->assertSame(JobToTestWithSchedule::class, $scheduledJob->description); + $this->assertFalse($this->container->resolved(JobToTestWithSchedule::class)); } } diff --git a/tests/Container/ContainerExtendTest.php b/tests/Container/ContainerExtendTest.php index 0f89a54fd902..ff69113ee56a 100644 --- a/tests/Container/ContainerExtendTest.php +++ b/tests/Container/ContainerExtendTest.php @@ -196,13 +196,13 @@ public function testExtendContextualBinding() ->give(fn () => new ContainerExtendInterfaceImplementationStub('foo')); $container->extend(ContainerExtendInterfaceStub::class, function ($instance) { - self::assertInstanceOf(ContainerExtendInterfaceImplementationStub::class, $instance); - self::assertSame('foo', $instance->value); + $this->assertInstanceOf(ContainerExtendInterfaceImplementationStub::class, $instance); + $this->assertSame('foo', $instance->value); return new ContainerExtendInterfaceImplementationStub('bar'); }); - self::assertSame('bar', $container->make(ContainerExtendConsumesInterfaceStub::class)->stub->value); + $this->assertSame('bar', $container->make(ContainerExtendConsumesInterfaceStub::class)->stub->value); } // https://github.com/laravel/framework/issues/53501 @@ -216,13 +216,13 @@ public function testExtendContextualBindingAfterResolution() $container->make(ContainerExtendConsumesInterfaceStub::class); $container->extend(ContainerExtendInterfaceStub::class, function ($instance) { - self::assertInstanceOf(ContainerExtendInterfaceImplementationStub::class, $instance); - self::assertSame('foo', $instance->value); + $this->assertInstanceOf(ContainerExtendInterfaceImplementationStub::class, $instance); + $this->assertSame('foo', $instance->value); return new ContainerExtendInterfaceImplementationStub('bar'); }); - self::assertSame('bar', $container->make(ContainerExtendConsumesInterfaceStub::class)->stub->value); + $this->assertSame('bar', $container->make(ContainerExtendConsumesInterfaceStub::class)->stub->value); } } diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index d0bbb09b9e47..b13f3011fa6d 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -90,7 +90,7 @@ public function testSelectProperlyCallsPDO() $pdo->expects($this->once())->method('prepare')->with('foo')->willReturn($statement); $mock = $this->getMockConnection(['prepareBindings'], $writePdo); $mock->setReadPdo($pdo); - $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['foo' => 'bar']))->willReturn(['foo' => 'bar']); + $mock->expects($this->once())->method('prepareBindings')->with(['foo' => 'bar'])->willReturn(['foo' => 'bar']); $results = $mock->select('foo', ['foo' => 'bar']); $this->assertEquals(['boom'], $results); $log = $mock->getQueryLog(); @@ -119,7 +119,7 @@ public function testSelectResultsetsReturnsMultipleRowset() $pdo->expects($this->once())->method('prepare')->with('CALL a_procedure(?)')->willReturn($statement); $mock = $this->getMockConnection(['prepareBindings'], $writePdo); $mock->setReadPdo($pdo); - $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['foo']))->willReturn(['foo']); + $mock->expects($this->once())->method('prepareBindings')->with(['foo'])->willReturn(['foo']); $results = $mock->selectResultsets('CALL a_procedure(?)', ['foo']); $this->assertEquals([['boom'], ['boom']], $results); $log = $mock->getQueryLog(); @@ -131,7 +131,7 @@ public function testSelectResultsetsReturnsMultipleRowset() public function testInsertCallsTheStatementMethod() { $connection = $this->getMockConnection(['statement']); - $connection->expects($this->once())->method('statement')->with($this->equalTo('foo'), $this->equalTo(['bar']))->willReturn('baz'); + $connection->expects($this->once())->method('statement')->with('foo', ['bar'])->willReturn('baz'); $results = $connection->insert('foo', ['bar']); $this->assertSame('baz', $results); } @@ -139,7 +139,7 @@ public function testInsertCallsTheStatementMethod() public function testUpdateCallsTheAffectingStatementMethod() { $connection = $this->getMockConnection(['affectingStatement']); - $connection->expects($this->once())->method('affectingStatement')->with($this->equalTo('foo'), $this->equalTo(['bar']))->willReturn('baz'); + $connection->expects($this->once())->method('affectingStatement')->with('foo', ['bar'])->willReturn('baz'); $results = $connection->update('foo', ['bar']); $this->assertSame('baz', $results); } @@ -147,7 +147,7 @@ public function testUpdateCallsTheAffectingStatementMethod() public function testDeleteCallsTheAffectingStatementMethod() { $connection = $this->getMockConnection(['affectingStatement']); - $connection->expects($this->once())->method('affectingStatement')->with($this->equalTo('foo'), $this->equalTo(['bar']))->willReturn(true); + $connection->expects($this->once())->method('affectingStatement')->with('foo', ['bar'])->willReturn(true); $results = $connection->delete('foo', ['bar']); $this->assertTrue($results); } @@ -158,9 +158,9 @@ public function testStatementProperlyCallsPDO() $statement = $this->getMockBuilder('PDOStatement')->onlyMethods(['execute', 'bindValue'])->getMock(); $statement->expects($this->once())->method('bindValue')->with(1, 'bar', 2); $statement->expects($this->once())->method('execute')->willReturn(true); - $pdo->expects($this->once())->method('prepare')->with($this->equalTo('foo'))->willReturn($statement); + $pdo->expects($this->once())->method('prepare')->with('foo')->willReturn($statement); $mock = $this->getMockConnection(['prepareBindings'], $pdo); - $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['bar']))->willReturn(['bar']); + $mock->expects($this->once())->method('prepareBindings')->with(['bar'])->willReturn(['bar']); $results = $mock->statement('foo', ['bar']); $this->assertTrue($results); $log = $mock->getQueryLog(); @@ -178,7 +178,7 @@ public function testAffectingStatementProperlyCallsPDO() $statement->expects($this->once())->method('rowCount')->willReturn(42); $pdo->expects($this->once())->method('prepare')->with('foo')->willReturn($statement); $mock = $this->getMockConnection(['prepareBindings'], $pdo); - $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['foo' => 'bar']))->willReturn(['foo' => 'bar']); + $mock->expects($this->once())->method('prepareBindings')->with(['foo' => 'bar'])->willReturn(['foo' => 'bar']); $results = $mock->update('foo', ['foo' => 'bar']); $this->assertSame(42, $results); $log = $mock->getQueryLog(); @@ -190,7 +190,7 @@ public function testAffectingStatementProperlyCallsPDO() public function testTransactionLevelNotIncrementedOnTransactionException() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); - $pdo->expects($this->once())->method('beginTransaction')->will($this->throwException(new Exception)); + $pdo->expects($this->once())->method('beginTransaction')->willThrowException(new Exception); $connection = $this->getMockConnection([], $pdo); try { $connection->beginTransaction(); @@ -226,7 +226,7 @@ public function testBeginTransactionMethodNeverRetriesIfWithinTransaction() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); $pdo->expects($this->once())->method('beginTransaction'); - $pdo->expects($this->once())->method('exec')->will($this->throwException(new Exception)); + $pdo->expects($this->once())->method('exec')->willThrowException(new Exception); $connection = $this->getMockConnection(['reconnect'], $pdo); $queryGrammar = $this->createMock(Grammar::class); $queryGrammar->expects($this->once())->method('compileSavepoint')->willReturn('trans1'); @@ -256,7 +256,7 @@ public function testBeganTransactionFiresEventsIfSet() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); $connection = $this->getMockConnection(['getName'], $pdo); - $connection->expects($this->any())->method('getName')->willReturn('name'); + $connection->method('getName')->willReturn('name'); $connection->setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with(m::type(TransactionBeginning::class)); $connection->beginTransaction(); @@ -266,7 +266,7 @@ public function testCommittedFiresEventsIfSet() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); $connection = $this->getMockConnection(['getName'], $pdo); - $connection->expects($this->any())->method('getName')->willReturn('name'); + $connection->method('getName')->willReturn('name'); $connection->setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with(m::type(TransactionCommitted::class)); $connection->commit(); @@ -276,8 +276,8 @@ public function testCommittingFiresEventsIfSet() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); $connection = $this->getMockConnection(['getName', 'transactionLevel'], $pdo); - $connection->expects($this->any())->method('getName')->willReturn('name'); - $connection->expects($this->any())->method('transactionLevel')->willReturn(1); + $connection->method('getName')->willReturn('name'); + $connection->method('transactionLevel')->willReturn(1); $connection->setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with(m::type(TransactionCommitting::class)); $events->shouldReceive('dispatch')->once()->with(m::type(TransactionCommitted::class)); @@ -288,7 +288,7 @@ public function testRollBackedFiresEventsIfSet() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); $connection = $this->getMockConnection(['getName'], $pdo); - $connection->expects($this->any())->method('getName')->willReturn('name'); + $connection->method('getName')->willReturn('name'); $connection->beginTransaction(); $connection->setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with(m::type(TransactionRolledBack::class)); @@ -299,7 +299,7 @@ public function testRedundantRollBackFiresNoEvent() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); $connection = $this->getMockConnection(['getName'], $pdo); - $connection->expects($this->any())->method('getName')->willReturn('name'); + $connection->method('getName')->willReturn('name'); $connection->setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldNotReceive('dispatch'); $connection->rollBack(); @@ -344,7 +344,7 @@ public function testTransactionRetriesOnSerializationFailure() $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['inTransaction', 'beginTransaction', 'commit', 'rollBack'])->getMock(); $mock = $this->getMockConnection([], $pdo); - $pdo->expects($this->exactly(3))->method('commit')->will($this->throwException(new DatabaseConnectionTestMockPDOException('Serialization failure', '40001'))); + $pdo->expects($this->exactly(3))->method('commit')->willThrowException(new DatabaseConnectionTestMockPDOException('Serialization failure', '40001')); $pdo->expects($this->exactly(3))->method('beginTransaction'); $pdo->method('inTransaction')->willReturn(true); $pdo->expects($this->exactly(2))->method('rollBack'); diff --git a/tests/Database/DatabaseConnectorTest.php b/tests/Database/DatabaseConnectorTest.php index 013e9e58486e..fd9ded20901a 100755 --- a/tests/Database/DatabaseConnectorTest.php +++ b/tests/Database/DatabaseConnectorTest.php @@ -29,8 +29,8 @@ public function testMySqlConnectCallsCreateConnectionWithProperArguments($dsn, $ { $connector = $this->getMockBuilder(MySqlConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(PDO::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $connection->shouldReceive('exec')->once()->with('use `bar`;')->andReturn(true); $connection->shouldReceive('exec')->once()->with("SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';")->andReturn(true); $result = $connector->connect($config); @@ -54,8 +54,8 @@ public function testMySqlConnectCallsCreateConnectionWithIsolationLevel() $connector = $this->getMockBuilder(MySqlConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(PDO::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $connection->shouldReceive('exec')->once()->with('use `bar`;')->andReturn(true); $connection->shouldReceive('exec')->once()->with('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;')->andReturn(true); $connection->shouldReceive('exec')->once()->with("SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';")->andReturn(true); @@ -70,8 +70,8 @@ public function testPostgresConnectCallsCreateConnectionWithProperArguments() $config = ['host' => 'foo', 'database' => 'bar', 'port' => 111, 'charset' => 'utf8']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->zeroOrMoreTimes()->andReturn($statement); $statement->shouldReceive('execute')->zeroOrMoreTimes(); @@ -91,8 +91,8 @@ public function testPostgresSearchPathIsSet($searchPath, $expectedSql) $config = ['host' => 'foo', 'database' => 'bar', 'search_path' => $searchPath, 'charset' => 'utf8']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->once()->with($expectedSql)->andReturn($statement); $statement->shouldReceive('execute')->once(); @@ -177,8 +177,8 @@ public function testPostgresSearchPathFallbackToConfigKeySchema() $config = ['host' => 'foo', 'database' => 'bar', 'schema' => ['public', '"user"'], 'charset' => 'utf8']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->once()->with('set search_path to "public", "user"')->andReturn($statement); $statement->shouldReceive('execute')->once(); @@ -193,8 +193,8 @@ public function testPostgresApplicationNameIsSet() $config = ['host' => 'foo', 'database' => 'bar', 'charset' => 'utf8', 'application_name' => 'Laravel App']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->zeroOrMoreTimes()->andReturn($statement); $statement->shouldReceive('execute')->zeroOrMoreTimes(); @@ -209,8 +209,8 @@ public function testPostgresApplicationUseAlternativeDatabaseName() $config = ['database' => 'bar', 'connect_via_database' => 'baz']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->zeroOrMoreTimes()->andReturn($statement); $statement->shouldReceive('execute')->zeroOrMoreTimes(); @@ -225,8 +225,8 @@ public function testPostgresApplicationUseAlternativeDatabaseNameAndPort() $config = ['database' => 'bar', 'connect_via_database' => 'baz', 'port' => 5432, 'connect_via_port' => 2345]; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->zeroOrMoreTimes()->andReturn($statement); $statement->shouldReceive('execute')->zeroOrMoreTimes(); @@ -241,8 +241,8 @@ public function testPostgresConnectorReadsIsolationLevelFromConfig() $config = ['host' => 'foo', 'database' => 'bar', 'port' => 111, 'isolation_level' => 'SERIALIZABLE']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(PDO::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $statement = m::mock(PDOStatement::class); $connection->shouldReceive('prepare')->once()->with('set session characteristics as transaction isolation level SERIALIZABLE')->andReturn($statement); $statement->shouldReceive('execute')->zeroOrMoreTimes(); @@ -258,8 +258,8 @@ public function testSQLiteMemoryDatabasesMayBeConnectedTo() $config = ['database' => ':memory:']; $connector = $this->getMockBuilder(SQLiteConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -271,8 +271,8 @@ public function testSQLiteNamedMemoryDatabasesMayBeConnectedTo() $config = ['database' => 'file:mydb?mode=memory&cache=shared']; $connector = $this->getMockBuilder(SQLiteConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -284,8 +284,8 @@ public function testSQLiteFileDatabasesMayBeConnectedTo() $config = ['database' => __DIR__]; $connector = $this->getMockBuilder(SQLiteConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -297,8 +297,8 @@ public function testSqlServerConnectCallsCreateConnectionWithProperArguments() $dsn = $this->getDsn($config); $connector = $this->getMockBuilder(SqlServerConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -310,8 +310,8 @@ public function testSqlServerConnectCallsCreateConnectionWithOptionalArguments() $dsn = $this->getDsn($config); $connector = $this->getMockBuilder(SqlServerConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -324,8 +324,8 @@ public function testSqlServerConnectCallsCreateConnectionWithPreferredODBC() $dsn = $this->getDsn($config); $connector = $this->getMockBuilder(SqlServerConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); - $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); - $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); + $connector->expects($this->once())->method('getOptions')->with($config)->willReturn(['options']); + $connector->expects($this->once())->method('createConnection')->with($dsn, $config, ['options'])->willReturn($connection); $result = $connector->connect($config); $this->assertSame($result, $connection); diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 4aa3342fbac8..693c26e1a615 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -902,7 +902,7 @@ public function testUpdateProcessWithoutTimestamps() $query->shouldReceive('update')->once()->with(['name' => 'taylor'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->never())->method('updateTimestamps'); - $model->expects($this->any())->method('fireModelEvent')->willReturn(true); + $model->method('fireModelEvent')->willReturn(true); $model->id = 1; $model->syncOriginal(); @@ -937,7 +937,7 @@ public function testUpdateUsesOldPrimaryKey() public function testTimestampsAreReturnedAsObjects() { $model = $this->getMockBuilder(EloquentDateModelStub::class)->onlyMethods(['getDateFormat'])->getMock(); - $model->expects($this->any())->method('getDateFormat')->willReturn('Y-m-d'); + $model->method('getDateFormat')->willReturn('Y-m-d'); $model->setRawAttributes([ 'created_at' => '2012-12-04', 'updated_at' => '2012-12-05', @@ -950,7 +950,7 @@ public function testTimestampsAreReturnedAsObjects() public function testTimestampsAreReturnedAsObjectsFromPlainDatesAndTimestamps() { $model = $this->getMockBuilder(EloquentDateModelStub::class)->onlyMethods(['getDateFormat'])->getMock(); - $model->expects($this->any())->method('getDateFormat')->willReturn('Y-m-d H:i:s'); + $model->method('getDateFormat')->willReturn('Y-m-d H:i:s'); $model->setRawAttributes([ 'created_at' => '2012-12-04', 'updated_at' => $this->currentTime(), @@ -1047,7 +1047,7 @@ public function testFromDateTime() public function testFromDateTimeMilliseconds() { $model = $this->getMockBuilder('Illuminate\Tests\Database\EloquentDateModelStub')->onlyMethods(['getDateFormat'])->getMock(); - $model->expects($this->any())->method('getDateFormat')->willReturn('Y-m-d H:s.vi'); + $model->method('getDateFormat')->willReturn('Y-m-d H:s.vi'); $model->setRawAttributes([ 'created_at' => '2012-12-04 22:59.32130', ]); @@ -1861,7 +1861,7 @@ public function testGuardedWithFillableConfig(): void $model->fillable(['name']); $model->fill(['name' => 'Leto Atreides', 'age' => 51]); - self::assertSame( + $this->assertSame( ['name' => 'Leto Atreides', 'age' => 51], $model->getAttributes(), ); diff --git a/tests/Database/DatabaseMariaDbSchemaStateTest.php b/tests/Database/DatabaseMariaDbSchemaStateTest.php index 96b992b93218..e0dddf55f82a 100644 --- a/tests/Database/DatabaseMariaDbSchemaStateTest.php +++ b/tests/Database/DatabaseMariaDbSchemaStateTest.php @@ -25,13 +25,13 @@ public function testConnectionString(string $expectedConnectionString, array $ex $method = new ReflectionMethod(get_class($schemaState), 'connectionString'); $connString = $method->invoke($schemaState, $versionInfo); - self::assertEquals($expectedConnectionString, $connString); + $this->assertEquals($expectedConnectionString, $connString); // test baseVariables $method = new ReflectionMethod(get_class($schemaState), 'baseVariables'); $variables = $method->invoke($schemaState, $dbConfig); - self::assertEquals($expectedVariables, $variables); + $this->assertEquals($expectedVariables, $variables); } public static function provider(): Generator diff --git a/tests/Database/DatabaseMigrationCreatorTest.php b/tests/Database/DatabaseMigrationCreatorTest.php index a558110953c3..bf9a20420b9a 100755 --- a/tests/Database/DatabaseMigrationCreatorTest.php +++ b/tests/Database/DatabaseMigrationCreatorTest.php @@ -14,7 +14,7 @@ public function testBasicCreateMethodStoresMigrationFile() { $creator = $this->getCreator(); - $creator->expects($this->any())->method('getDatePrefix')->willReturn('foo'); + $creator->method('getDatePrefix')->willReturn('foo'); $creator->getFilesystem()->shouldReceive('exists')->once()->with('stubs/migration.stub')->andReturn(false); $creator->getFilesystem()->shouldReceive('get')->once()->with($creator->stubPath().'/migration.stub')->andReturn('return new class'); $creator->getFilesystem()->shouldReceive('ensureDirectoryExists')->once()->with('foo'); @@ -36,7 +36,7 @@ public function testBasicCreateMethodCallsPostCreateHooks() $_SERVER['__migration.creator.path'] = $path; }); - $creator->expects($this->any())->method('getDatePrefix')->willReturn('foo'); + $creator->method('getDatePrefix')->willReturn('foo'); $creator->getFilesystem()->shouldReceive('exists')->once()->with('stubs/migration.update.stub')->andReturn(false); $creator->getFilesystem()->shouldReceive('get')->once()->with($creator->stubPath().'/migration.update.stub')->andReturn('return new class DummyTable'); $creator->getFilesystem()->shouldReceive('ensureDirectoryExists')->once()->with('foo'); @@ -55,7 +55,7 @@ public function testBasicCreateMethodCallsPostCreateHooks() public function testTableUpdateMigrationStoresMigrationFile() { $creator = $this->getCreator(); - $creator->expects($this->any())->method('getDatePrefix')->willReturn('foo'); + $creator->method('getDatePrefix')->willReturn('foo'); $creator->getFilesystem()->shouldReceive('exists')->once()->with('stubs/migration.update.stub')->andReturn(false); $creator->getFilesystem()->shouldReceive('get')->once()->with($creator->stubPath().'/migration.update.stub')->andReturn('return new class DummyTable'); $creator->getFilesystem()->shouldReceive('ensureDirectoryExists')->once()->with('foo'); @@ -69,7 +69,7 @@ public function testTableUpdateMigrationStoresMigrationFile() public function testTableCreationMigrationStoresMigrationFile() { $creator = $this->getCreator(); - $creator->expects($this->any())->method('getDatePrefix')->willReturn('foo'); + $creator->method('getDatePrefix')->willReturn('foo'); $creator->getFilesystem()->shouldReceive('exists')->once()->with('stubs/migration.create.stub')->andReturn(false); $creator->getFilesystem()->shouldReceive('get')->once()->with($creator->stubPath().'/migration.create.stub')->andReturn('return new class DummyTable'); $creator->getFilesystem()->shouldReceive('ensureDirectoryExists')->once()->with('foo'); diff --git a/tests/Database/DatabaseMigrationMigrateCommandTest.php b/tests/Database/DatabaseMigrationMigrateCommandTest.php index a508488f4f5c..13289d6d0741 100755 --- a/tests/Database/DatabaseMigrationMigrateCommandTest.php +++ b/tests/Database/DatabaseMigrationMigrateCommandTest.php @@ -76,7 +76,7 @@ public function testMigrationRepositoryCreatedWhenNecessary() $migrator->shouldReceive('setOutput')->once()->andReturn($migrator); $migrator->shouldReceive('run')->once()->with([__DIR__.DIRECTORY_SEPARATOR.'migrations'], ['pretend' => false, 'step' => false]); $migrator->shouldReceive('repositoryExists')->once()->andReturn(false); - $command->expects($this->once())->method('callSilent')->with($this->equalTo('migrate:install'), $this->equalTo([])); + $command->expects($this->once())->method('callSilent')->with('migrate:install', []); $this->runCommand($command); } diff --git a/tests/Database/DatabaseMySqlSchemaStateTest.php b/tests/Database/DatabaseMySqlSchemaStateTest.php index 30aedb9fa250..cf07e8cb6a96 100644 --- a/tests/Database/DatabaseMySqlSchemaStateTest.php +++ b/tests/Database/DatabaseMySqlSchemaStateTest.php @@ -27,13 +27,13 @@ public function testConnectionString(string $expectedConnectionString, array $ex $method = new ReflectionMethod(get_class($schemaState), 'connectionString'); $connString = $method->invoke($schemaState, $versionInfo); - self::assertEquals($expectedConnectionString, $connString); + $this->assertEquals($expectedConnectionString, $connString); // test baseVariables $method = new ReflectionMethod(get_class($schemaState), 'baseVariables'); $variables = $method->invoke($schemaState, $dbConfig); - self::assertEquals($expectedVariables, $variables); + $this->assertEquals($expectedVariables, $variables); } public static function provider(): Generator @@ -141,8 +141,8 @@ public function testExecuteDumpProcessForDepth() { $mockProcess = $this->createMock(Process::class); $mockProcess->method('setTimeout')->willReturnSelf(); - $mockProcess->method('mustRun')->will( - $this->throwException(new Exception('column-statistics')) + $mockProcess->method('mustRun')->willThrowException( + new Exception('column-statistics') ); $mockOutput = $this->createMock(\stdClass::class); diff --git a/tests/Database/DatabaseProcessorTest.php b/tests/Database/DatabaseProcessorTest.php index b7fa22f13eca..9048caf0d34d 100755 --- a/tests/Database/DatabaseProcessorTest.php +++ b/tests/Database/DatabaseProcessorTest.php @@ -14,7 +14,7 @@ class DatabaseProcessorTest extends TestCase public function testInsertGetIdProcessing() { $pdo = $this->createMock(ProcessorTestPDOStub::class); - $pdo->expects($this->once())->method('lastInsertId')->with($this->equalTo('id'))->willReturn('1'); + $pdo->expects($this->once())->method('lastInsertId')->with('id')->willReturn('1'); $connection = m::mock(Connection::class); $connection->shouldReceive('insert')->once()->with('sql', ['foo']); $connection->shouldReceive('getPdo')->once()->andReturn($pdo); diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 1b0bcb892543..141474aa56bc 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -557,7 +557,7 @@ public function testReportExceptionsForGet() $exceptionHandler->shouldReceive('report') ->once() ->andReturnUsing(function (UnableToReadFile $e) { - self::assertStringContainsString( + $this->assertStringContainsString( 'Unable to read file from location: foo.txt.', $e->getMessage(), ); @@ -585,7 +585,7 @@ public function testReportExceptionsForReadStream() $exceptionHandler->shouldReceive('report') ->once() ->andReturnUsing(function (UnableToReadFile $e) { - self::assertStringContainsString( + $this->assertStringContainsString( 'Unable to read file from location: foo.txt.', $e->getMessage(), ); @@ -613,7 +613,7 @@ public function testReportExceptionsForPut() $exceptionHandler->shouldReceive('report') ->once() ->andReturnUsing(function (UnableToWriteFile $e) { - self::assertStringContainsString( + $this->assertStringContainsString( 'Unable to write file at location: foo.txt.', $e->getMessage(), ); @@ -647,7 +647,7 @@ public function testReportExceptionsForMimeType() $exceptionHandler->shouldReceive('report') ->once() ->andReturnUsing(function (UnableToRetrieveMetadata $e) { - self::assertStringContainsString( + $this->assertStringContainsString( 'Unable to retrieve the mime_type for file at location: unknown.mime-type.', $e->getMessage(), ); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 409a186188cc..675e594cff66 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -480,8 +480,8 @@ public function testSendRequestBodyAsJsonByDefault() $body = '{"test":"phpunit"}'; $fakeRequest = function (Request $request) use ($body) { - self::assertSame($body, $request->body()); - self::assertContains('application/json', $request->header('Content-Type')); + $this->assertSame($body, $request->body()); + $this->assertContains('application/json', $request->header('Content-Type')); return ['my' => 'response']; }; @@ -496,8 +496,8 @@ public function testSendRequestBodyWithManyAmpersands() $body = str_repeat('A thousand &. ', 1000); $fakeRequest = function (Request $request) use ($body) { - self::assertSame($body, $request->body()); - self::assertContains('text/plain', $request->header('Content-Type')); + $this->assertSame($body, $request->body()); + $this->assertContains('text/plain', $request->header('Content-Type')); return ['my' => 'response']; }; @@ -516,8 +516,8 @@ public function testSendStreamRequestBody() $body = Utils::streamFor($resource); $fakeRequest = function (Request $request) use ($string) { - self::assertSame($string, $request->body()); - self::assertContains('text/plain', $request->header('Content-Type')); + $this->assertSame($string, $request->body()); + $this->assertContains('text/plain', $request->header('Content-Type')); return ['my' => 'response']; }; @@ -2582,8 +2582,8 @@ public function testHandleRequestExeptionWithNoResponseInPoolConsideredConnectio ]; }); - self::assertInstanceOf(ConnectionException::class, $responses[0]); - self::assertSame($requestException, $responses[0]->getPrevious()); + $this->assertInstanceOf(ConnectionException::class, $responses[0]); + $this->assertSame($requestException, $responses[0]->getPrevious()); } public function testExceptionThrownInRetryCallbackIsReturnedWithoutRetryingInPool() diff --git a/tests/Integration/Database/AfterQueryTest.php b/tests/Integration/Database/AfterQueryTest.php index 62ee78df5fc6..d63e910bf1d0 100644 --- a/tests/Integration/Database/AfterQueryTest.php +++ b/tests/Integration/Database/AfterQueryTest.php @@ -44,9 +44,7 @@ public function testAfterQueryOnEloquentBuilder() ->afterQuery(function (Collection $users) use ($afterQueryIds) { $afterQueryIds->push(...$users->pluck('id')->all()); - foreach ($users as $user) { - $this->assertInstanceOf(AfterQueryUser::class, $user); - } + $this->assertContainsOnlyInstancesOf(AfterQueryUser::class, $users); }) ->get(); @@ -87,9 +85,7 @@ public function testAfterQueryOnEloquentCursor() ->afterQuery(function (Collection $users) use ($afterQueryIds) { $afterQueryIds->push(...$users->pluck('id')->all()); - foreach ($users as $user) { - $this->assertInstanceOf(AfterQueryUser::class, $user); - } + $this->assertContainsOnlyInstancesOf(AfterQueryUser::class, $users); }) ->cursor(); @@ -177,9 +173,7 @@ public function testAfterQueryHookOnBelongsToManyRelationship() ->afterQuery(function (Collection $posts) use ($afterQueryIds) { $afterQueryIds->push(...$posts->pluck('id')->all()); - foreach ($posts as $post) { - $this->assertInstanceOf(AfterQueryPost::class, $post); - } + $this->assertContainsOnlyInstancesOf(AfterQueryPost::class, $posts); }) ->get(); @@ -215,9 +209,7 @@ public function testAfterQueryHookOnHasManyThroughRelationship() ->afterQuery(function (Collection $teamMates) use ($afterQueryIds) { $afterQueryIds->push(...$teamMates->pluck('id')->all()); - foreach ($teamMates as $teamMate) { - $this->assertInstanceOf(AfterQueryUser::class, $teamMate); - } + $this->assertContainsOnlyInstancesOf(AfterQueryUser::class, $teamMates); }) ->get(); diff --git a/tests/Integration/Database/DatabaseConnectionsTest.php b/tests/Integration/Database/DatabaseConnectionsTest.php index 3d703cdf48a2..ba806847a3ec 100644 --- a/tests/Integration/Database/DatabaseConnectionsTest.php +++ b/tests/Integration/Database/DatabaseConnectionsTest.php @@ -47,7 +47,7 @@ public function testEstablishDatabaseConnection() $result = $connection->selectOne('SELECT COUNT(*) as total FROM test_1'); - self::assertSame(1, $result->total); + $this->assertSame(1, $result->total); } public function testThrowExceptionIfConnectionAlreadyExists() @@ -92,9 +92,9 @@ public function testOverrideExistingConnection() // longer be available. It's a new and fresh database $resultAfterOverride = $connection->select("SELECT name FROM sqlite_master WHERE type='table';"); - self::assertSame('test_1', $resultBeforeOverride[0]->name); + $this->assertSame('test_1', $resultBeforeOverride[0]->name); - self::assertEmpty($resultAfterOverride); + $this->assertEmpty($resultAfterOverride); } public function testEstablishingAConnectionWillDispatchAnEvent() @@ -116,13 +116,13 @@ public function testEstablishingAConnectionWillDispatchAnEvent() 'database' => ':memory:', ]); - self::assertInstanceOf( + $this->assertInstanceOf( ConnectionEstablished::class, $event, 'Expected the ConnectionEstablished event to be dispatched when establishing a connection.' ); - self::assertSame('my-phpunit-connection', $event->connectionName); + $this->assertSame('my-phpunit-connection', $event->connectionName); } public function testTablePrefix() diff --git a/tests/Mail/MailableQueuedTest.php b/tests/Mail/MailableQueuedTest.php index da5b621b9baa..eb6f3a59abe9 100644 --- a/tests/Mail/MailableQueuedTest.php +++ b/tests/Mail/MailableQueuedTest.php @@ -6,7 +6,6 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\View\Factory; -use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\FilesystemManager; use Illuminate\Foundation\Application; use Illuminate\Mail\Mailable; @@ -58,8 +57,6 @@ public function testQueuedMailableWithAttachmentFromDiskSent(): void { $app = new Application; $container = Container::getInstance(); - $this->getMockBuilder(Filesystem::class) - ->getMock(); $filesystemFactory = $this->getMockBuilder(FilesystemManager::class) ->setConstructorArgs([$app]) ->getMock(); diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 9b0a791fd46f..71f7222149ef 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -26,7 +26,7 @@ public function testPushProperlyPushesJobOntoDatabase($uuid, $job, $displayNameS }); $queue = $this->getMockBuilder(DatabaseQueue::class)->onlyMethods(['currentTime'])->setConstructorArgs([$database = m::mock(Connection::class), 'table', 'default'])->getMock(); - $queue->expects($this->any())->method('currentTime')->willReturn('time'); + $queue->method('currentTime')->willReturn('time'); $queue->setContainer($container = m::spy(Container::class)); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) use ($uuid, $displayNameStartsWith, $jobStartsWith) { @@ -74,7 +74,7 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() ->onlyMethods(['currentTime']) ->setConstructorArgs([$database = m::mock(Connection::class), 'table', 'default']) ->getMock(); - $queue->expects($this->any())->method('currentTime')->willReturn('time'); + $queue->method('currentTime')->willReturn('time'); $queue->setContainer($container = m::spy(Container::class)); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) use ($uuid, $time) { @@ -104,7 +104,7 @@ public function testPushIncludesBatchIdInPayloadForBatchableJob() $job = (new MyBatchableJob)->withBatchId('test-batch-id'); $queue = $this->getMockBuilder(DatabaseQueue::class)->onlyMethods(['currentTime'])->setConstructorArgs([$database = m::mock(Connection::class), 'table', 'default'])->getMock(); - $queue->expects($this->any())->method('currentTime')->willReturn('time'); + $queue->method('currentTime')->willReturn('time'); $queue->setContainer($container = m::spy(Container::class)); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) { @@ -163,8 +163,8 @@ public function testBulkBatchPushesOntoDatabase() $database = m::mock(Connection::class); $queue = $this->getMockBuilder(DatabaseQueue::class)->onlyMethods(['currentTime', 'availableAt'])->setConstructorArgs([$database, 'table', 'default'])->getMock(); - $queue->expects($this->any())->method('currentTime')->willReturn('created'); - $queue->expects($this->any())->method('availableAt')->willReturn('available'); + $queue->method('currentTime')->willReturn('created'); + $queue->method('availableAt')->willReturn('available'); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('insert')->once()->andReturnUsing(function ($records) use ($uuid, $time) { $this->assertEquals([[ diff --git a/tests/Support/DateFacadeTest.php b/tests/Support/DateFacadeTest.php index 46c391430b4e..b3717cc1d02b 100644 --- a/tests/Support/DateFacadeTest.php +++ b/tests/Support/DateFacadeTest.php @@ -23,7 +23,7 @@ protected function tearDown(): void protected static function assertBetweenStartAndNow($start, $actual) { - static::assertThat( + self::assertThat( $actual, static::logicalAnd( static::greaterThanOrEqual($start), @@ -36,24 +36,24 @@ public function testUseClosure() { $start = Carbon::now()->getTimestamp(); $this->assertSame(Carbon::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); + self::assertBetweenStartAndNow($start, Date::now()->getTimestamp()); DateFactory::use(function (Carbon $date) { return new DateTime($date->format('Y-m-d H:i:s.u'), $date->getTimezone()); }); $start = Carbon::now()->getTimestamp(); $this->assertSame(DateTime::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); + self::assertBetweenStartAndNow($start, Date::now()->getTimestamp()); } public function testUseClassName() { $start = Carbon::now()->getTimestamp(); $this->assertSame(Carbon::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); + self::assertBetweenStartAndNow($start, Date::now()->getTimestamp()); DateFactory::use(DateTime::class); $start = Carbon::now()->getTimestamp(); $this->assertSame(DateTime::class, get_class(Date::now())); - $this->assertBetweenStartAndNow($start, Date::now()->getTimestamp()); + self::assertBetweenStartAndNow($start, Date::now()->getTimestamp()); } public function testCarbonImmutable() diff --git a/tests/Testing/AssertRedirectToActionTest.php b/tests/Testing/AssertRedirectToActionTest.php index 8ef82de9be3c..8d2b0534162d 100644 --- a/tests/Testing/AssertRedirectToActionTest.php +++ b/tests/Testing/AssertRedirectToActionTest.php @@ -11,11 +11,6 @@ class AssertRedirectToActionTest extends TestCase { - /** - * @var \Illuminate\Contracts\Routing\Registrar - */ - private $router; - /** * @var \Illuminate\Routing\UrlGenerator */ @@ -25,16 +20,16 @@ protected function setUp(): void { parent::setUp(); - $this->router = $this->app->make(Registrar::class); + $router = $this->app->make(Registrar::class); - $this->router->get('controller/index', [TestActionController::class, 'index']); - $this->router->get('controller/show/{id}', [TestActionController::class, 'show']); + $router->get('controller/index', [TestActionController::class, 'index']); + $router->get('controller/show/{id}', [TestActionController::class, 'show']); - $this->router->get('redirect-to-index', function () { + $router->get('redirect-to-index', function () { return new RedirectResponse($this->urlGenerator->action([TestActionController::class, 'index'])); }); - $this->router->get('redirect-to-show', function () { + $router->get('redirect-to-show', function () { return new RedirectResponse($this->urlGenerator->action([TestActionController::class, 'show'], ['id' => 123])); }); diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index ba9653b5ccfc..0da214569104 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1688,7 +1688,7 @@ public function testAssertExactJsonStructure(): void try { $response->assertExactJsonStructure(['foo']); $failed = false; - } catch (AssertionFailedError $e) { + } catch (AssertionFailedError) { $failed = true; } @@ -1700,7 +1700,7 @@ public function testAssertExactJsonStructure(): void try { $response->assertExactJsonStructure(['foobar' => ['foobar_foo'], 'foo', 0, 'bars', 'baz', 'barfoo', 'numeric_keys']); $failed = false; - } catch (AssertionFailedError $e) { + } catch (AssertionFailedError) { $failed = true; } @@ -1712,7 +1712,7 @@ public function testAssertExactJsonStructure(): void try { $response->assertExactJsonStructure(['bars' => ['*' => ['bar']], 'foo', 'foobar', 0, 'baz', 'barfoo', 'numeric_keys']); $failed = false; - } catch (AssertionFailedError $e) { + } catch (AssertionFailedError) { $failed = true; } @@ -1724,7 +1724,7 @@ public function testAssertExactJsonStructure(): void try { $response->assertExactJsonStructure(['numeric_keys' => ['*' => ['bar']], 'foo', 'foobar', 0, 'bars', 'baz', 'barfoo']); $failed = false; - } catch (AssertionFailedError $e) { + } catch (AssertionFailedError) { $failed = true; } @@ -1736,7 +1736,7 @@ public function testAssertExactJsonStructure(): void try { $response->assertExactJsonStructure(['baz' => ['*' => ['foo', 'bar' => ['foo']]], 'foo', 'foobar', 0, 'bars', 'barfoo', 'numeric_keys']); $failed = false; - } catch (AssertionFailedError $e) { + } catch (AssertionFailedError) { $failed = true; } diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php index 5c6527514eab..5ac17aad8ca8 100755 --- a/tests/Translation/TranslationTranslatorTest.php +++ b/tests/Translation/TranslationTranslatorTest.php @@ -18,19 +18,19 @@ class TranslationTranslatorTest extends TestCase public function testHasMethodReturnsFalseWhenReturnedTranslationIsNull() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'))->willReturn('foo'); + $t->expects($this->once())->method('get')->with('foo', [], 'bar')->willReturn('foo'); $this->assertFalse($t->has('foo', 'bar')); $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en', 'sp'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'))->willReturn('bar'); + $t->expects($this->once())->method('get')->with('foo', [], 'bar')->willReturn('bar'); $this->assertTrue($t->has('foo', 'bar')); $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'), false)->willReturn('bar'); + $t->expects($this->once())->method('get')->with('foo', [], 'bar', false)->willReturn('bar'); $this->assertTrue($t->hasForLocale('foo', 'bar')); $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'), false)->willReturn('foo'); + $t->expects($this->once())->method('get')->with('foo', [], 'bar', false)->willReturn('foo'); $this->assertFalse($t->hasForLocale('foo', 'bar')); $t = new Translator($this->getLoader(), 'en'); @@ -135,8 +135,8 @@ public function testGetMethodProperlyLoadsAndRetrievesItemForGlobalNamespace() public function testChoiceMethodProperlyLoadsAndRetrievesItemForAnInt() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); - $t->expects($this->once())->method('localeForChoice')->with($this->equalTo('foo'), $this->equalTo(null))->willReturn('en'); + $t->expects($this->once())->method('get')->with('foo', [], 'en')->willReturn('line'); + $t->expects($this->once())->method('localeForChoice')->with('foo', null)->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('line', 10, 'en')->andReturn('choiced'); @@ -146,8 +146,8 @@ public function testChoiceMethodProperlyLoadsAndRetrievesItemForAnInt() public function testChoiceMethodProperlyLoadsAndRetrievesItemForAFloat() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); - $t->expects($this->once())->method('localeForChoice')->with($this->equalTo('foo'), $this->equalTo(null))->willReturn('en'); + $t->expects($this->once())->method('get')->with('foo', [], 'en')->willReturn('line'); + $t->expects($this->once())->method('localeForChoice')->with('foo', null)->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('line', 1.2, 'en')->andReturn('choiced'); @@ -157,8 +157,8 @@ public function testChoiceMethodProperlyLoadsAndRetrievesItemForAFloat() public function testChoiceMethodProperlyCountsCollectionsAndLoadsAndRetrievesItem() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->exactly(2))->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); - $t->expects($this->exactly(2))->method('localeForChoice')->with($this->equalTo('foo'), $this->equalTo(null))->willReturn('en'); + $t->expects($this->exactly(2))->method('get')->with('foo', [], 'en')->willReturn('line'); + $t->expects($this->exactly(2))->method('localeForChoice')->with('foo', null)->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->twice()->with('line', 3, 'en')->andReturn('choiced'); @@ -173,8 +173,8 @@ public function testChoiceMethodProperlySelectsLocaleForChoose() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'hasForLocale'])->setConstructorArgs([$this->getLoader(), 'cs'])->getMock(); $t->setFallback('en'); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); - $t->expects($this->once())->method('hasForLocale')->with($this->equalTo('foo'), $this->equalTo('cs'))->willReturn(false); + $t->expects($this->once())->method('get')->with('foo', [], 'en')->willReturn('line'); + $t->expects($this->once())->method('hasForLocale')->with('foo', 'cs')->willReturn(false); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('line', 10, 'en')->andReturn('choiced'); @@ -184,8 +184,8 @@ public function testChoiceMethodProperlySelectsLocaleForChoose() public function testChoiceMethodProperlyUsesCustomCountReplacement() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo(':count foos'), $this->equalTo([]), $this->equalTo('en'))->willReturn('{1} :count foos|[2,*] :count foos'); - $t->expects($this->once())->method('localeForChoice')->with($this->equalTo(':count foos'), $this->equalTo(null))->willReturn('en'); + $t->expects($this->once())->method('get')->with(':count foos', [], 'en')->willReturn('{1} :count foos|[2,*] :count foos'); + $t->expects($this->once())->method('localeForChoice')->with(':count foos', null)->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('{1} :count foos|[2,*] :count foos', 1234, 'en')->andReturn(':count foos'); diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 2e878ad805ae..a862b522f24d 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -2468,9 +2468,9 @@ public function testGreaterThan() $this->assertTrue($v->fails()); $fileOne = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileOne->expects($this->any())->method('getSize')->willReturn(5472); + $fileOne->method('getSize')->willReturn(5472); $fileTwo = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileTwo->expects($this->any())->method('getSize')->willReturn(3151); + $fileTwo->method('getSize')->willReturn(3151); $v = new Validator($trans, ['lhs' => $fileOne, 'rhs' => $fileTwo], ['lhs' => 'gt:rhs']); $this->assertTrue($v->passes()); @@ -2563,9 +2563,9 @@ public function testLessThan() $this->assertTrue($v->passes()); $fileOne = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileOne->expects($this->any())->method('getSize')->willReturn(5472); + $fileOne->method('getSize')->willReturn(5472); $fileTwo = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileTwo->expects($this->any())->method('getSize')->willReturn(3151); + $fileTwo->method('getSize')->willReturn(3151); $v = new Validator($trans, ['lhs' => $fileOne, 'rhs' => $fileTwo], ['lhs' => 'lt:rhs']); $this->assertTrue($v->fails()); @@ -2604,9 +2604,9 @@ public function testGreaterThanOrEqual() $this->assertTrue($v->fails()); $fileOne = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileOne->expects($this->any())->method('getSize')->willReturn(5472); + $fileOne->method('getSize')->willReturn(5472); $fileTwo = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileTwo->expects($this->any())->method('getSize')->willReturn(5472); + $fileTwo->method('getSize')->willReturn(5472); $v = new Validator($trans, ['lhs' => $fileOne, 'rhs' => $fileTwo], ['lhs' => 'gte:rhs']); $this->assertTrue($v->passes()); @@ -2645,9 +2645,9 @@ public function testLessThanOrEqual() $this->assertTrue($v->passes()); $fileOne = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileOne->expects($this->any())->method('getSize')->willReturn(5472); + $fileOne->method('getSize')->willReturn(5472); $fileTwo = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $fileTwo->expects($this->any())->method('getSize')->willReturn(5472); + $fileTwo->method('getSize')->willReturn(5472); $v = new Validator($trans, ['lhs' => $fileOne, 'rhs' => $fileTwo], ['lhs' => 'lte:rhs']); $this->assertTrue($v->passes()); @@ -3825,12 +3825,12 @@ public function testValidateSize() $this->assertFalse($v->passes()); $file = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(3072); + $file->method('getSize')->willReturn(3072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Size:3']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); + $file->method('getSize')->willReturn(4072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Size:3']); $this->assertFalse($v->passes()); } @@ -3881,12 +3881,12 @@ public function testValidateBetween() $this->assertFalse($v->passes()); $file = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(3072); + $file->method('getSize')->willReturn(3072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Between:1,5']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); + $file->method('getSize')->willReturn(4072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Between:1,2']); $this->assertFalse($v->passes()); } @@ -3940,12 +3940,12 @@ public function testValidateMin() $this->assertFalse($v->passes()); $file = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(3072); + $file->method('getSize')->willReturn(3072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Min:2']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(File::class)->onlyMethods(['getSize'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); + $file->method('getSize')->willReturn(4072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Min:10']); $this->assertFalse($v->passes()); } @@ -4007,7 +4007,7 @@ public function testValidateMax() $this->assertFalse($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['isValid'])->setConstructorArgs([__FILE__, basename(__FILE__)])->getMock(); - $file->expects($this->any())->method('isValid')->willReturn(false); + $file->method('isValid')->willReturn(false); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Max:10']); $this->assertFalse($v->passes()); } @@ -4114,8 +4114,8 @@ public function testProperMessagesAreReturnedForSizes() $this->assertSame('string', $v->messages()->first('name')); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(4072); + $file->method('isValid')->willReturn(true); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Max:3']); $this->assertFalse($v->passes()); $v->messages()->setFormat(':message'); @@ -4150,11 +4150,11 @@ public function testValidateGtPlaceHolderIsReplacedProperly() $this->assertSame('minimum value', $v->messages()->first('max')); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(4072); + $file->method('isValid')->willReturn(true); $biggerFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $biggerFile->expects($this->any())->method('getSize')->willReturn(5120); - $biggerFile->expects($this->any())->method('isValid')->willReturn(true); + $biggerFile->method('getSize')->willReturn(5120); + $biggerFile->method('isValid')->willReturn(true); $v = new Validator($trans, ['photo' => $file, 'bigger' => $biggerFile], ['photo' => 'file|gt:bigger']); $this->assertFalse($v->passes()); $this->assertEquals(5, $v->messages()->first('photo')); @@ -4192,11 +4192,11 @@ public function testValidateLtPlaceHolderIsReplacedProperly() $this->assertSame('maximum value', $v->messages()->first('min')); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(4072); + $file->method('isValid')->willReturn(true); $smallerFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $smallerFile->expects($this->any())->method('getSize')->willReturn(2048); - $smallerFile->expects($this->any())->method('isValid')->willReturn(true); + $smallerFile->method('getSize')->willReturn(2048); + $smallerFile->method('isValid')->willReturn(true); $v = new Validator($trans, ['photo' => $file, 'smaller' => $smallerFile], ['photo' => 'file|lt:smaller']); $this->assertFalse($v->passes()); $this->assertEquals(2, $v->messages()->first('photo')); @@ -4234,11 +4234,11 @@ public function testValidateGtePlaceHolderIsReplacedProperly() $this->assertSame('minimum value', $v->messages()->first('max')); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(4072); + $file->method('isValid')->willReturn(true); $biggerFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $biggerFile->expects($this->any())->method('getSize')->willReturn(5120); - $biggerFile->expects($this->any())->method('isValid')->willReturn(true); + $biggerFile->method('getSize')->willReturn(5120); + $biggerFile->method('isValid')->willReturn(true); $v = new Validator($trans, ['photo' => $file, 'bigger' => $biggerFile], ['photo' => 'file|gte:bigger']); $this->assertFalse($v->passes()); $this->assertEquals(5, $v->messages()->first('photo')); @@ -4276,11 +4276,11 @@ public function testValidateLtePlaceHolderIsReplacedProperly() $this->assertSame('maximum value', $v->messages()->first('min')); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(4072); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(4072); + $file->method('isValid')->willReturn(true); $smallerFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $smallerFile->expects($this->any())->method('getSize')->willReturn(2048); - $smallerFile->expects($this->any())->method('isValid')->willReturn(true); + $smallerFile->method('getSize')->willReturn(2048); + $smallerFile->method('isValid')->willReturn(true); $v = new Validator($trans, ['photo' => $file, 'smaller' => $smallerFile], ['photo' => 'file|lte:smaller']); $this->assertFalse($v->passes()); $this->assertEquals(2, $v->messages()->first('photo')); @@ -4619,11 +4619,11 @@ public function testValidateGtMessagesAreCorrect() ], 'en'); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(8919); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(8919); + $file->method('isValid')->willReturn(true); $otherFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $otherFile->expects($this->any())->method('getSize')->willReturn(9216); - $otherFile->expects($this->any())->method('isValid')->willReturn(true); + $otherFile->method('getSize')->willReturn(9216); + $otherFile->method('isValid')->willReturn(true); $v = new Validator($trans, [ 'numeric' => 7, @@ -4659,11 +4659,11 @@ public function testValidateGteMessagesAreCorrect() ], 'en'); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(8919); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(8919); + $file->method('isValid')->willReturn(true); $otherFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $otherFile->expects($this->any())->method('getSize')->willReturn(9216); - $otherFile->expects($this->any())->method('isValid')->willReturn(true); + $otherFile->method('getSize')->willReturn(9216); + $otherFile->method('isValid')->willReturn(true); $v = new Validator($trans, [ 'numeric' => 7, @@ -4699,11 +4699,11 @@ public function testValidateLtMessagesAreCorrect() ], 'en'); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(8919); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(8919); + $file->method('isValid')->willReturn(true); $otherFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $otherFile->expects($this->any())->method('getSize')->willReturn(8192); - $otherFile->expects($this->any())->method('isValid')->willReturn(true); + $otherFile->method('getSize')->willReturn(8192); + $otherFile->method('isValid')->willReturn(true); $v = new Validator($trans, [ 'numeric' => 7, @@ -4739,11 +4739,11 @@ public function testValidateLteMessagesAreCorrect() ], 'en'); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $file->expects($this->any())->method('getSize')->willReturn(8919); - $file->expects($this->any())->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(8919); + $file->method('isValid')->willReturn(true); $otherFile = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getSize', 'isValid'])->setConstructorArgs([__FILE__, false])->getMock(); - $otherFile->expects($this->any())->method('getSize')->willReturn(8192); - $otherFile->expects($this->any())->method('isValid')->willReturn(true); + $otherFile->method('getSize')->willReturn(8192); + $otherFile->method('isValid')->willReturn(true); $v = new Validator($trans, [ 'numeric' => 7, @@ -5269,69 +5269,69 @@ public function testValidateImage() $uploadedFile = [__FILE__, '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('php'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('php'); + $file->method('guessExtension')->willReturn('php'); + $file->method('getClientOriginalExtension')->willReturn('php'); $v = new Validator($trans, ['x' => $file], ['x' => 'image']); $this->assertFalse($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file2->expects($this->any())->method('guessExtension')->willReturn('jpeg'); - $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpeg'); + $file2->method('guessExtension')->willReturn('jpeg'); + $file2->method('getClientOriginalExtension')->willReturn('jpeg'); $v = new Validator($trans, ['x' => $file2], ['x' => 'image']); $this->assertTrue($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file2->expects($this->any())->method('guessExtension')->willReturn('jpg'); - $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file2->method('guessExtension')->willReturn('jpg'); + $file2->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file2], ['x' => 'image']); $this->assertTrue($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file2->expects($this->any())->method('guessExtension')->willReturn('jpg'); - $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file2->method('guessExtension')->willReturn('jpg'); + $file2->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file2], ['x' => 'image']); $this->assertTrue($v->passes()); $file3 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file3->expects($this->any())->method('guessExtension')->willReturn('gif'); - $file3->expects($this->any())->method('getClientOriginalExtension')->willReturn('gif'); + $file3->method('guessExtension')->willReturn('gif'); + $file3->method('getClientOriginalExtension')->willReturn('gif'); $v = new Validator($trans, ['x' => $file3], ['x' => 'image']); $this->assertTrue($v->passes()); $file4 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file4->expects($this->any())->method('guessExtension')->willReturn('bmp'); - $file4->expects($this->any())->method('getClientOriginalExtension')->willReturn('bmp'); + $file4->method('guessExtension')->willReturn('bmp'); + $file4->method('getClientOriginalExtension')->willReturn('bmp'); $v = new Validator($trans, ['x' => $file4], ['x' => 'image']); $this->assertTrue($v->passes()); $file5 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file5->expects($this->any())->method('guessExtension')->willReturn('png'); - $file5->expects($this->any())->method('getClientOriginalExtension')->willReturn('png'); + $file5->method('guessExtension')->willReturn('png'); + $file5->method('getClientOriginalExtension')->willReturn('png'); $v = new Validator($trans, ['x' => $file5], ['x' => 'image']); $this->assertTrue($v->passes()); $file6 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file6->expects($this->any())->method('guessExtension')->willReturn('svg'); - $file6->expects($this->any())->method('getClientOriginalExtension')->willReturn('svg'); + $file6->method('guessExtension')->willReturn('svg'); + $file6->method('getClientOriginalExtension')->willReturn('svg'); $v = new Validator($trans, ['x' => $file6], ['x' => 'image']); $this->assertFalse($v->passes()); $file6 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file6->expects($this->any())->method('guessExtension')->willReturn('svg'); - $file6->expects($this->any())->method('getClientOriginalExtension')->willReturn('svg'); + $file6->method('guessExtension')->willReturn('svg'); + $file6->method('getClientOriginalExtension')->willReturn('svg'); $v = new Validator($trans, ['x' => $file6], ['x' => 'image:allow_svg']); $this->assertTrue($v->passes()); $file7 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file7->expects($this->any())->method('guessExtension')->willReturn('webp'); - $file7->expects($this->any())->method('getClientOriginalExtension')->willReturn('webp'); + $file7->method('guessExtension')->willReturn('webp'); + $file7->method('getClientOriginalExtension')->willReturn('webp'); $v = new Validator($trans, ['x' => $file7], ['x' => 'Image']); $this->assertTrue($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file2->expects($this->any())->method('guessExtension')->willReturn('jpg'); - $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file2->method('guessExtension')->willReturn('jpg'); + $file2->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file2], ['x' => 'Image']); $this->assertTrue($v->passes()); } @@ -5342,8 +5342,8 @@ public function testValidateImageDoesNotAllowPhpExtensionsOnImageMime() $uploadedFile = [__FILE__, '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('jpeg'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('php'); + $file->method('guessExtension')->willReturn('jpeg'); + $file->method('getClientOriginalExtension')->willReturn('php'); $v = new Validator($trans, ['x' => $file], ['x' => 'image']); $this->assertFalse($v->passes()); } @@ -5487,21 +5487,21 @@ public function testValidateMimetypes() $uploadedFile = [__DIR__.'/ValidationMacroTest.php', '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('rtf'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('rtf'); + $file->method('guessExtension')->willReturn('rtf'); + $file->method('getClientOriginalExtension')->willReturn('rtf'); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getMimeType'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getMimeType')->willReturn('text/rtf'); + $file->method('getMimeType')->willReturn('text/rtf'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimetypes:text/*']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getMimeType'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getMimeType')->willReturn('application/pdf'); + $file->method('getMimeType')->willReturn('application/pdf'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimetypes:text/rtf']); $this->assertFalse($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getMimeType'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getMimeType')->willReturn('image/jpeg'); + $file->method('getMimeType')->willReturn('image/jpeg'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimetypes:image/jpeg']); $this->assertTrue($v->passes()); } @@ -5512,26 +5512,26 @@ public function testValidateMime() $uploadedFile = [__FILE__, '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('pdf'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('pdf'); + $file->method('guessExtension')->willReturn('pdf'); + $file->method('getClientOriginalExtension')->willReturn('pdf'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:pdf']); $this->assertTrue($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'isValid'])->setConstructorArgs($uploadedFile)->getMock(); - $file2->expects($this->any())->method('guessExtension')->willReturn('pdf'); - $file2->expects($this->any())->method('isValid')->willReturn(false); + $file2->method('guessExtension')->willReturn('pdf'); + $file2->method('isValid')->willReturn(false); $v = new Validator($trans, ['x' => $file2], ['x' => 'mimes:pdf']); $this->assertFalse($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('jpg'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file->method('guessExtension')->willReturn('jpg'); + $file->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpeg']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('jpg'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpeg'); + $file->method('guessExtension')->willReturn('jpg'); + $file->method('getClientOriginalExtension')->willReturn('jpeg'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpg']); $this->assertTrue($v->passes()); } @@ -5542,28 +5542,28 @@ public function testValidateExtension() $uploadedFile = [__FILE__, '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('pdf'); + $file->method('getClientOriginalExtension')->willReturn('pdf'); $v = new Validator($trans, ['x' => $file], ['x' => 'extensions:pdf']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file], ['x' => 'extensions:jpg']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file], ['x' => 'extensions:jpeg,jpg']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $file->method('getClientOriginalExtension')->willReturn('jpg'); $v = new Validator($trans, ['x' => $file], ['x' => 'extensions:jpeg']); $this->assertFalse($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('jpg'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpeg'); + $file->method('guessExtension')->willReturn('jpg'); + $file->method('getClientOriginalExtension')->willReturn('jpeg'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpg|extensions:jpg']); $this->assertFalse($v->passes()); } @@ -5574,14 +5574,14 @@ public function testValidateMimeEnforcesPhpCheck() $uploadedFile = [__FILE__, '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file->expects($this->any())->method('guessExtension')->willReturn('pdf'); - $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('php'); + $file->method('guessExtension')->willReturn('pdf'); + $file->method('getClientOriginalExtension')->willReturn('php'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:pdf']); $this->assertFalse($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); - $file2->expects($this->any())->method('guessExtension')->willReturn('php'); - $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('php'); + $file2->method('guessExtension')->willReturn('php'); + $file2->method('getClientOriginalExtension')->willReturn('php'); $v = new Validator($trans, ['x' => $file2], ['x' => 'mimes:pdf,php']); $this->assertTrue($v->passes()); } From 1b6e77f3ecbf161c162f2237673bc267dd2330fd Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Wed, 15 Apr 2026 14:59:03 +0200 Subject: [PATCH 163/596] [13.x] Enforce static calls (#59704) * Enfoce static calls * StyleCI fix --------- Co-authored-by: Lucas Michot --- src/Illuminate/Bus/UniqueLock.php | 4 +- src/Illuminate/Console/Scheduling/Event.php | 2 +- src/Illuminate/Database/Eloquent/Builder.php | 4 +- .../Database/Eloquent/SoftDeletes.php | 2 +- .../Foundation/Console/ChannelListCommand.php | 2 +- .../Foundation/Console/RouteListCommand.php | 2 +- .../Concerns/InteractsWithContentTypes.php | 4 +- tests/Database/DatabaseEloquentModelTest.php | 38 ++--- .../DatabaseMigratorIntegrationTest.php | 106 +++++++------- .../DatabaseSchemaBuilderIntegrationTest.php | 10 +- tests/Http/HttpClientTest.php | 100 ++++++------- tests/Http/Middleware/TrustProxiesTest.php | 4 +- tests/View/Blade/BladeComponentsTest.php | 4 +- types/Database/Eloquent/Factories/Factory.php | 4 +- types/Support/Collection.php | 138 +++++++++--------- types/Support/LazyCollection.php | 132 ++++++++--------- 16 files changed, 278 insertions(+), 278 deletions(-) diff --git a/src/Illuminate/Bus/UniqueLock.php b/src/Illuminate/Bus/UniqueLock.php index cf834cf1b27b..6570afa5530f 100644 --- a/src/Illuminate/Bus/UniqueLock.php +++ b/src/Illuminate/Bus/UniqueLock.php @@ -43,7 +43,7 @@ public function acquire($job) ? ($job->uniqueVia() ?? $this->cache) : $this->cache; - return (bool) $cache->lock($this->getKey($job), $uniqueFor)->get(); + return (bool) $cache->lock(self::getKey($job), $uniqueFor)->get(); } /** @@ -58,7 +58,7 @@ public function release($job) ? ($job->uniqueVia() ?? $this->cache) : $this->cache; - $cache->lock($this->getKey($job))->forceRelease(); + $cache->lock(self::getKey($job))->forceRelease(); } /** diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 246acd2ce359..9e46d9f05b6b 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -827,7 +827,7 @@ public function mutexName() } return 'framework'.DIRECTORY_SEPARATOR.'schedule-'. - sha1($this->expression.$this->normalizeCommand($this->command ?? '')); + sha1($this->expression.self::normalizeCommand($this->command ?? '')); } /** diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 7a071ff5ac19..4de139b1cebf 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -529,7 +529,7 @@ public function fillForInsert(array $values) $values = [$values]; } - $this->model->unguarded(function () use (&$values) { + $this->model::unguarded(function () use (&$values) { foreach ($values as $key => $rowValues) { $values[$key] = tap( $this->newModelInstance($rowValues), @@ -1244,7 +1244,7 @@ public function createQuietly(array $attributes = []) */ public function forceCreate(array $attributes) { - return $this->model->unguarded(function () use ($attributes) { + return $this->model::unguarded(function () use ($attributes) { return $this->newModelInstance()->create($attributes); }); } diff --git a/src/Illuminate/Database/Eloquent/SoftDeletes.php b/src/Illuminate/Database/Eloquent/SoftDeletes.php index 06060adb83c1..2604829a6224 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletes.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletes.php @@ -104,7 +104,7 @@ public static function forceDestroy($ids) $count = 0; - foreach ($instance->withTrashed()->whereIn($key, $ids)->get() as $model) { + foreach ($instance::withTrashed()->whereIn($key, $ids)->get() as $model) { if ($model->forceDelete()) { $count++; } diff --git a/src/Illuminate/Foundation/Console/ChannelListCommand.php b/src/Illuminate/Foundation/Console/ChannelListCommand.php index 7063f1addfe1..a57aa1e02427 100644 --- a/src/Illuminate/Foundation/Console/ChannelListCommand.php +++ b/src/Illuminate/Foundation/Console/ChannelListCommand.php @@ -78,7 +78,7 @@ protected function forCli($channels) return mb_strlen($channelName); }); - $terminalWidth = $this->getTerminalWidth(); + $terminalWidth = self::getTerminalWidth(); $channelCount = $this->determineChannelCountOutput($channels, $terminalWidth); diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index a48575e6790f..2c5f07c2ed24 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -399,7 +399,7 @@ protected function forCli($routes) $maxMethod = mb_strlen($routes->max('method')); - $terminalWidth = $this->getTerminalWidth(); + $terminalWidth = self::getTerminalWidth(); $routeCount = $this->determineRouteCountOutput($routes, $terminalWidth); diff --git a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php index a921e4913e49..595824ae91fc 100644 --- a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php +++ b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php @@ -80,7 +80,7 @@ public function accepts($contentTypes) $type = strtolower($type); - if ($this->matchesType($accept, $type) || $accept === strtok($type, '/').'/*') { + if (self::matchesType($accept, $type) || $accept === strtok($type, '/').'/*') { return true; } } @@ -121,7 +121,7 @@ public function prefers($contentTypes) $type = strtolower($type); - if ($this->matchesType($type, $accept) || $accept === strtok($type, '/').'/*') { + if (self::matchesType($type, $accept) || $accept === strtok($type, '/').'/*') { return $contentType; } } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 693c26e1a615..84470778a14f 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -821,7 +821,7 @@ public function testUpdateProcess() $query->shouldReceive('update')->once()->with(['name' => 'taylor'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.updated: '.get_class($model), $model)->andReturn(true); @@ -843,7 +843,7 @@ public function testUpdateProcessDoesntOverrideTimestamps() $query->shouldReceive('where')->once()->with('id', '=', 1); $query->shouldReceive('update')->once()->with(['created_at' => 'foo', 'updated_at' => 'bar'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until'); $events->shouldReceive('dispatch'); @@ -860,7 +860,7 @@ public function testSaveIsCanceledIfSavingEventReturnsFalse() $model = $this->getMockBuilder(EloquentModelStub::class)->onlyMethods(['newModelQuery'])->getMock(); $query = m::mock(Builder::class); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(false); $model->exists = true; @@ -872,7 +872,7 @@ public function testUpdateIsCanceledIfUpdatingEventReturnsFalse() $model = $this->getMockBuilder(EloquentModelStub::class)->onlyMethods(['newModelQuery'])->getMock(); $query = m::mock(Builder::class); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(false); $model->exists = true; @@ -886,7 +886,7 @@ public function testEventsCanBeFiredWithCustomEventObjects() $model = $this->getMockBuilder(EloquentModelEventObjectStub::class)->onlyMethods(['newModelQuery'])->getMock(); $query = m::mock(Builder::class); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with(m::type(EloquentModelSavingEventStub::class))->andReturn(false); $model->exists = true; @@ -919,7 +919,7 @@ public function testUpdateUsesOldPrimaryKey() $query->shouldReceive('update')->once()->with(['id' => 2, 'foo' => 'bar'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.updated: '.get_class($model), $model)->andReturn(true); @@ -1065,7 +1065,7 @@ public function testInsertProcess() $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.created: '.get_class($model), $model); @@ -1085,7 +1085,7 @@ public function testInsertProcess() $model->expects($this->once())->method('updateTimestamps'); $model->setIncrementing(false); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.created: '.get_class($model), $model); @@ -1104,7 +1104,7 @@ public function testInsertIsCanceledIfCreatingEventReturnsFalse() $query = m::mock(Builder::class); $query->shouldReceive('getConnection')->once(); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(false); @@ -1123,7 +1123,7 @@ public function testInsertOrIgnoreProcessWithIncrementing() $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.created: '.get_class($model), $model); @@ -1148,7 +1148,7 @@ public function testInsertOrIgnoreProcessWithConflict() $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(true); @@ -1171,7 +1171,7 @@ public function testInsertOrIgnoreProcessWithNonIncrementing() $model->expects($this->once())->method('updateTimestamps'); $model->setIncrementing(false); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.created: '.get_class($model), $model); @@ -1196,7 +1196,7 @@ public function testInsertOrIgnoreProcessWithNamedUnique() $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($model), $model)->andReturn(true); @@ -2638,7 +2638,7 @@ public function testReplicatingEventIsFiredWhenReplicatingModel() { $model = new EloquentModelStub; - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with('eloquent.replicating: '.get_class($model), m::on(function ($m) use ($model) { return $model->is($m); })); @@ -2655,7 +2655,7 @@ public function testReplicateQuietlyCreatesANewModelInstanceWithSameAttributeVal $model->updated_at = new DateTime; $replicated = $model->replicateQuietly(); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->never()->with('eloquent.replicating: '.get_class($model), $model)->andReturn(true); $this->assertNull($replicated->id); @@ -2698,7 +2698,7 @@ public function testIncrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $query->shouldReceive('where')->andReturn($query); $query->shouldReceive('increment'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->never()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->never()->with('eloquent.updating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->never()->with('eloquent.updated: '.get_class($model), $model)->andReturn(true); @@ -2725,7 +2725,7 @@ public function testDecrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $query->shouldReceive('where')->andReturn($query); $query->shouldReceive('decrement'); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->never()->with('eloquent.saving: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('until')->never()->with('eloquent.updating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->never()->with('eloquent.updated: '.get_class($model), $model)->andReturn(true); @@ -2811,7 +2811,7 @@ public function testIncrementEachFiresModelEvents() $query->shouldReceive('where')->andReturn($query); $query->shouldReceive('incrementEach')->andReturn(1); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(true); $events->shouldReceive('dispatch')->once()->with('eloquent.updated: '.get_class($model), $model); @@ -2828,7 +2828,7 @@ public function testIncrementEachReturnsFalseWhenUpdatingEventCancelled() $model->shouldReceive('newQueryWithoutScopes')->never(); - $model->setEventDispatcher($events = m::mock(Dispatcher::class)); + $model::setEventDispatcher($events = m::mock(Dispatcher::class)); $events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($model), $model)->andReturn(false); $result = $model->publicIncrementEach(['foo' => 1]); diff --git a/tests/Database/DatabaseMigratorIntegrationTest.php b/tests/Database/DatabaseMigratorIntegrationTest.php index 8f2273a4ee4f..d7a59782b765 100644 --- a/tests/Database/DatabaseMigratorIntegrationTest.php +++ b/tests/Database/DatabaseMigratorIntegrationTest.php @@ -89,8 +89,8 @@ public function testBasicMigrationOfSingleFolder() { $ran = $this->migrator->run([__DIR__.'/migrations/one']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); $this->assertTrue(str_contains($ran[0], 'users')); $this->assertTrue(str_contains($ran[1], 'password_resets')); @@ -102,12 +102,12 @@ public function testMigrationsDefaultConnectionCanBeChanged() return $this->migrator->run([__DIR__.'/migrations/one'], ['database' => 'sqllite3']); }); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); - $this->assertTrue($this->db->schema('sqlite2')->hasTable('users')); - $this->assertTrue($this->db->schema('sqlite2')->hasTable('password_resets')); - $this->assertFalse($this->db->schema('sqlite3')->hasTable('users')); - $this->assertFalse($this->db->schema('sqlite3')->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema('sqlite2')->hasTable('users')); + $this->assertTrue($this->db::schema('sqlite2')->hasTable('password_resets')); + $this->assertFalse($this->db::schema('sqlite3')->hasTable('users')); + $this->assertFalse($this->db::schema('sqlite3')->hasTable('password_resets')); $this->assertTrue(Str::contains($ran[0], 'users')); $this->assertTrue(Str::contains($ran[1], 'password_resets')); @@ -117,12 +117,12 @@ public function testMigrationsCanEachDefineConnection() { $ran = $this->migrator->run([__DIR__.'/migrations/connection_configured']); - $this->assertFalse($this->db->schema()->hasTable('failed_jobs')); - $this->assertFalse($this->db->schema()->hasTable('jobs')); - $this->assertFalse($this->db->schema('sqlite2')->hasTable('failed_jobs')); - $this->assertFalse($this->db->schema('sqlite2')->hasTable('jobs')); - $this->assertTrue($this->db->schema('sqlite3')->hasTable('failed_jobs')); - $this->assertTrue($this->db->schema('sqlite3')->hasTable('jobs')); + $this->assertFalse($this->db::schema()->hasTable('failed_jobs')); + $this->assertFalse($this->db::schema()->hasTable('jobs')); + $this->assertFalse($this->db::schema('sqlite2')->hasTable('failed_jobs')); + $this->assertFalse($this->db::schema('sqlite2')->hasTable('jobs')); + $this->assertTrue($this->db::schema('sqlite3')->hasTable('failed_jobs')); + $this->assertTrue($this->db::schema('sqlite3')->hasTable('jobs')); $this->assertTrue(Str::contains($ran[0], 'failed_jobs')); $this->assertTrue(Str::contains($ran[1], 'jobs')); @@ -134,12 +134,12 @@ public function testMigratorCannotChangeDefinedMigrationConnection() return $this->migrator->run([__DIR__.'/migrations/connection_configured']); }); - $this->assertFalse($this->db->schema()->hasTable('failed_jobs')); - $this->assertFalse($this->db->schema()->hasTable('jobs')); - $this->assertFalse($this->db->schema('sqlite2')->hasTable('failed_jobs')); - $this->assertFalse($this->db->schema('sqlite2')->hasTable('jobs')); - $this->assertTrue($this->db->schema('sqlite3')->hasTable('failed_jobs')); - $this->assertTrue($this->db->schema('sqlite3')->hasTable('jobs')); + $this->assertFalse($this->db::schema()->hasTable('failed_jobs')); + $this->assertFalse($this->db::schema()->hasTable('jobs')); + $this->assertFalse($this->db::schema('sqlite2')->hasTable('failed_jobs')); + $this->assertFalse($this->db::schema('sqlite2')->hasTable('jobs')); + $this->assertTrue($this->db::schema('sqlite3')->hasTable('failed_jobs')); + $this->assertTrue($this->db::schema('sqlite3')->hasTable('jobs')); $this->assertTrue(Str::contains($ran[0], 'failed_jobs')); $this->assertTrue(Str::contains($ran[1], 'jobs')); @@ -148,11 +148,11 @@ public function testMigratorCannotChangeDefinedMigrationConnection() public function testMigrationsCanBeRolledBack() { $this->migrator->run([__DIR__.'/migrations/one']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); $rolledBack = $this->migrator->rollback([__DIR__.'/migrations/one']); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); $this->assertTrue(str_contains($rolledBack[0], 'password_resets')); $this->assertTrue(str_contains($rolledBack[1], 'users')); @@ -161,11 +161,11 @@ public function testMigrationsCanBeRolledBack() public function testMigrationsCanBeResetUsingAnString() { $this->migrator->run([__DIR__.'/migrations/one']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); $rolledBack = $this->migrator->reset(__DIR__.'/migrations/one'); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); $this->assertTrue(str_contains($rolledBack[0], 'password_resets')); $this->assertTrue(str_contains($rolledBack[1], 'users')); @@ -174,11 +174,11 @@ public function testMigrationsCanBeResetUsingAnString() public function testMigrationsCanBeResetUsingAnArray() { $this->migrator->run([__DIR__.'/migrations/one']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); $rolledBack = $this->migrator->reset([__DIR__.'/migrations/one']); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); $this->assertTrue(str_contains($rolledBack[0], 'password_resets')); $this->assertTrue(str_contains($rolledBack[1], 'users')); @@ -187,52 +187,52 @@ public function testMigrationsCanBeResetUsingAnArray() public function testNoErrorIsThrownWhenNoOutstandingMigrationsExist() { $this->migrator->run([__DIR__.'/migrations/one']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); $this->migrator->run([__DIR__.'/migrations/one']); } public function testNoErrorIsThrownWhenNothingToRollback() { $this->migrator->run([__DIR__.'/migrations/one']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); $this->migrator->rollback([__DIR__.'/migrations/one']); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); $this->migrator->rollback([__DIR__.'/migrations/one']); } public function testMigrationsCanRunAcrossMultiplePaths() { $this->migrator->run([__DIR__.'/migrations/one', __DIR__.'/migrations/two']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); - $this->assertTrue($this->db->schema()->hasTable('flights')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('flights')); } public function testMigrationsCanBeRolledBackAcrossMultiplePaths() { $this->migrator->run([__DIR__.'/migrations/one', __DIR__.'/migrations/two']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); - $this->assertTrue($this->db->schema()->hasTable('flights')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('flights')); $this->migrator->rollback([__DIR__.'/migrations/one', __DIR__.'/migrations/two']); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); - $this->assertFalse($this->db->schema()->hasTable('flights')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('flights')); } public function testMigrationsCanBeResetAcrossMultiplePaths() { $this->migrator->run([__DIR__.'/migrations/one', __DIR__.'/migrations/two']); - $this->assertTrue($this->db->schema()->hasTable('users')); - $this->assertTrue($this->db->schema()->hasTable('password_resets')); - $this->assertTrue($this->db->schema()->hasTable('flights')); + $this->assertTrue($this->db::schema()->hasTable('users')); + $this->assertTrue($this->db::schema()->hasTable('password_resets')); + $this->assertTrue($this->db::schema()->hasTable('flights')); $this->migrator->reset([__DIR__.'/migrations/one', __DIR__.'/migrations/two']); - $this->assertFalse($this->db->schema()->hasTable('users')); - $this->assertFalse($this->db->schema()->hasTable('password_resets')); - $this->assertFalse($this->db->schema()->hasTable('flights')); + $this->assertFalse($this->db::schema()->hasTable('users')); + $this->assertFalse($this->db::schema()->hasTable('password_resets')); + $this->assertFalse($this->db::schema()->hasTable('flights')); } public function testMigrationsCanBeProperlySortedAcrossMultiplePaths() diff --git a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php index a9b989d3be41..d09b6ca56ab8 100644 --- a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php @@ -43,14 +43,14 @@ protected function tearDown(): void public function testHasColumnWithTablePrefix() { - $this->db->connection()->setTablePrefix('test_'); + $this->db::connection()->setTablePrefix('test_'); - $this->db->connection()->getSchemaBuilder()->create('table1', function (Blueprint $table) { + $this->db::connection()->getSchemaBuilder()->create('table1', function (Blueprint $table) { $table->integer('id'); $table->string('name'); }); - $this->assertTrue($this->db->connection()->getSchemaBuilder()->hasColumn('table1', 'name')); + $this->assertTrue($this->db::connection()->getSchemaBuilder()->hasColumn('table1', 'name')); } public function testHasColumnAndIndexWithPrefixIndexDisabled() @@ -89,7 +89,7 @@ public function testHasColumnAndIndexWithPrefixIndexEnabled() public function testDropColumnWithTablePrefix() { - $this->db->connection()->setTablePrefix('test_'); + $this->db::connection()->setTablePrefix('test_'); $this->schemaBuilder()->create('pandemic_table', function (Blueprint $table) { $table->integer('id'); @@ -112,6 +112,6 @@ public function testDropColumnWithTablePrefix() private function schemaBuilder() { - return $this->db->connection()->getSchemaBuilder(); + return $this->db::connection()->getSchemaBuilder(); } } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 675e594cff66..d8963e1f1155 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -1410,7 +1410,7 @@ public function testRequestLevelTruncationLevelOnRequestException() RequestException::truncateAt(60); $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -1436,7 +1436,7 @@ public function testNoTruncationOnRequestLevel() RequestException::truncateAt(60); $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -1459,7 +1459,7 @@ public function testRequestExceptionDoesNotTruncateButRequestDoes() RequestException::dontTruncate(); $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -1480,7 +1480,7 @@ public function testAsyncRequestExceptionsRespectRequestTruncation() { RequestException::dontTruncate(); $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = $this->factory->async()->throw()->truncateExceptionsAt(4)->get('http://foo.com/json')->wait(); @@ -2008,7 +2008,7 @@ public function testMultipleRequestsAreSentInThePoolWithKeys() public function testMiddlewareRunsInPool() { $this->factory->fake(function (Request $request) { - return $this->factory->response('Fake'); + return $this->factory::response('Fake'); }); $history = []; @@ -2094,7 +2094,7 @@ public function testTheRequestSendingAndResponseReceivedEventsAreFiredForEveryRe $factory = new Factory($events); $factory->fake([ - '*' => $factory->response(['error'], 403), + '*' => $factory::response(['error'], 403), ]); $response = $factory->retry(2, 1000, null, false)->get('http://foo.com/get'); @@ -2118,7 +2118,7 @@ public function testTheTransferStatsAreCalledSafelyWhenFakingTheRequest() public function testTransferStatsArePresentWhenFakingTheRequestUsingAPromiseResponse() { - $this->factory->fake(['https://example.com' => $this->factory->response()]); + $this->factory->fake(['https://example.com' => $this->factory::response()]); $effectiveUri = $this->factory->get('https://example.com')->effectiveUri(); $this->assertSame('https://example.com', (string) $effectiveUri); @@ -2131,7 +2131,7 @@ public function testClonedClientsWorkSuccessfullyWithTheRequestObject() $events->shouldReceive('dispatch')->once()->with(m::type(ResponseReceived::class)); $factory = new Factory($events); - $factory->fake(['example.com' => $factory->response('foo', 200)]); + $factory->fake(['example.com' => $factory::response('foo', 200)]); $client = $factory->timeout(10); $clonedClient = clone $client; @@ -2148,7 +2148,7 @@ public function testRequestIsMacroable() $this->factory->fake(function (Request $request) { $this->assertSame('yes!', $request->customMethod()); - return $this->factory->response(); + return $this->factory::response(); }); $this->factory->get('https://example.com'); @@ -2157,7 +2157,7 @@ public function testRequestIsMacroable() public function testRequestExceptionIsThrownWhenRetriesExhausted() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -2179,7 +2179,7 @@ public function testRequestExceptionIsThrownWhenRetriesExhausted() public function testRequestExceptionIsThrownWhenRetriesExhaustedWithBackoffArray() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -2201,7 +2201,7 @@ public function testRequestExceptionIsThrownWhenRetriesExhaustedWithBackoffArray public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $exception = null; @@ -2230,7 +2230,7 @@ public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessary() public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessaryWithBackoffArray() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $exception = null; @@ -2259,7 +2259,7 @@ public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessaryWit public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhausted() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory @@ -2274,7 +2274,7 @@ public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhausted() public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhaustedWithBackoffArray() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory @@ -2289,7 +2289,7 @@ public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhaustedWi public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = 0; @@ -2312,7 +2312,7 @@ public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessary public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessaryWithBackoffArray() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = 0; @@ -2385,7 +2385,7 @@ public function testRequestCanBeModifiedInRetryCallbackWithBackoffArray() public function testExceptionThrownInRetryCallbackWithoutRetrying() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $exception = null; @@ -2410,7 +2410,7 @@ public function testExceptionThrownInRetryCallbackWithoutRetrying() public function testExceptionThrownInRetryCallbackWithoutRetryingWithBackoffArray() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $exception = null; @@ -2465,7 +2465,7 @@ public function testRequestsWillBeWaitingSleepMillisecondsReceivedBeforeRetry() public function testRequestExceptionReturnedWhenRetriesExhaustedInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ @@ -2481,7 +2481,7 @@ public function testRequestExceptionReturnedWhenRetriesExhaustedInPool() public function testRequestExceptionIsReturnedWithoutRetriesIfRetryNotNecessaryInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = collect(); @@ -2505,7 +2505,7 @@ public function testRequestExceptionIsReturnedWithoutRetriesIfRetryNotNecessaryI public function testRequestExceptionIsNotReturnedWhenDisabledAndRetriesExhaustedInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); [$response] = $this->factory->pool(fn ($pool) => [ @@ -2522,7 +2522,7 @@ public function testRequestExceptionIsNotReturnedWhenDisabledAndRetriesExhausted public function testRequestExceptionIsNotReturnedWithoutRetriesIfRetryNotNecessaryInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = collect(); @@ -2589,7 +2589,7 @@ public function testHandleRequestExeptionWithNoResponseInPoolConsideredConnectio public function testExceptionThrownInRetryCallbackIsReturnedWithoutRetryingInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 500), + '*' => $this->factory::response(['error'], 500), ]); [$exception] = $this->factory->pool(fn ($pool) => [ @@ -2614,7 +2614,7 @@ public function testExceptionThrowInMiddlewareAllowsRetry() $this->expectException(RuntimeException::class); $this->factory->fake(function (Request $request) { - return $this->factory->response('Fake'); + return $this->factory::response('Fake'); })->withMiddleware($middleware) ->retry(3, 1, function (Exception $exception, PendingRequest $request) { return true; @@ -2651,7 +2651,7 @@ public function testRequestsWillBeWaitingSleepMillisecondsReceivedInBackoffArray public function testFailedRequest() { - $requestException = $this->factory->failedRequest(['code' => 'not_found'], 404, ['X-RateLimit-Remaining' => 199]); + $requestException = $this->factory::failedRequest(['code' => 'not_found'], 404, ['X-RateLimit-Remaining' => 199]); $this->assertInstanceOf(RequestException::class, $requestException); $this->assertEqualsCanonicalizing(['code' => 'not_found'], $requestException->response->json()); @@ -2661,7 +2661,7 @@ public function testFailedRequest() public function testFakeConnectionException() { - $this->factory->fake($this->factory->failedConnection('Fake')); + $this->factory->fake($this->factory::failedConnection('Fake')); $exception = null; @@ -2683,7 +2683,7 @@ public function testFakeConnectionException() public function testFakeConnectionExceptionWithinFakeClosure() { - $this->factory->fake(fn () => $this->factory->failedConnection('Fake')); + $this->factory->fake(fn () => $this->factory::failedConnection('Fake')); $exception = null; @@ -2702,7 +2702,7 @@ public function testFakeConnectionExceptionWithinFakeClosure() public function testFakeConnectionExceptionWithinArray() { - $this->factory->fake(['*' => $this->factory->failedConnection('Fake')]); + $this->factory->fake(['*' => $this->factory::failedConnection('Fake')]); $exception = null; @@ -2747,7 +2747,7 @@ public function testFakeConnectionExceptionWithinSequence() public function testMiddlewareRunsWhenFaked() { $this->factory->fake(function (Request $request) { - return $this->factory->response('Fake'); + return $this->factory::response('Fake'); }); $history = []; @@ -2770,7 +2770,7 @@ public function testMiddlewareRunsWhenFaked() public function testMiddlewareRunsAndCanChangeRequestOnAssertSent() { $this->factory->fake(function (Request $request) { - return $this->factory->response('Fake'); + return $this->factory::response('Fake'); }); $pendingRequest = $this->factory->withMiddleware( @@ -2894,9 +2894,9 @@ public function testTooManyRedirectsExceptionConvertedToConnectionException() public function testTooManyRedirectsWithFakedRedirectChain() { $this->factory->fake([ - '1.example.com' => $this->factory->response(null, 301, ['Location' => 'https://2.example.com']), - '2.example.com' => $this->factory->response(null, 301, ['Location' => 'https://3.example.com']), - '3.example.com' => $this->factory->response('', 200), + '1.example.com' => $this->factory::response(null, 301, ['Location' => 'https://2.example.com']), + '2.example.com' => $this->factory::response(null, 301, ['Location' => 'https://3.example.com']), + '3.example.com' => $this->factory::response('', 200), ]); $this->expectException(ConnectionException::class); @@ -2907,7 +2907,7 @@ public function testTooManyRedirectsWithFakedRedirectChain() public function testRequestExceptionIsNotThrownIfThePendingRequestIsSetToThrowOnFailureButTheResponseIsSuccessful() { $this->factory->fake([ - '*' => $this->factory->response(['success'], 200), + '*' => $this->factory::response(['success'], 200), ]); $response = $this->factory @@ -2920,7 +2920,7 @@ public function testRequestExceptionIsNotThrownIfThePendingRequestIsSetToThrowOn public function testRequestExceptionIsThrownIfThePendingRequestIsSetToThrowOnFailure() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -2940,7 +2940,7 @@ public function testRequestExceptionIsThrownIfThePendingRequestIsSetToThrowOnFai public function testRequestExceptionIsThrownIfTheThrowIfOnThePendingRequestIsSetToTrueOnFailure() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -2960,7 +2960,7 @@ public function testRequestExceptionIsThrownIfTheThrowIfOnThePendingRequestIsSet public function testRequestExceptionIsNotThrownIfTheThrowIfOnThePendingRequestIsSetToFalseOnFailure() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory @@ -2973,7 +2973,7 @@ public function testRequestExceptionIsNotThrownIfTheThrowIfOnThePendingRequestIs public function testRequestExceptionIsThrownIfTheThrowIfClosureOnThePendingRequestReturnsTrue() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -3007,7 +3007,7 @@ public function testRequestExceptionIsThrownIfTheThrowIfClosureOnThePendingReque public function testRequestExceptionIsNotThrownIfTheThrowIfClosureOnThePendingRequestReturnsFalse() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $hitThrowCallback = false; @@ -3030,7 +3030,7 @@ public function testRequestExceptionIsNotThrownIfTheThrowIfClosureOnThePendingRe public function testRequestExceptionIsThrownWithCallbackIfThePendingRequestIsSetToThrowOnFailure() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $exception = null; @@ -3109,7 +3109,7 @@ public function testRequestExceptionIsNotThrownIfTheRequestDoesNotFail() public function testRequestExceptionIsNotReturnedIfThePendingRequestIsSetToThrowOnFailureButTheResponseIsSuccessfulInPool() { $this->factory->fake([ - '*' => $this->factory->response(['success'], 200), + '*' => $this->factory::response(['success'], 200), ]); [$response] = $this->factory->pool(fn ($pool) => [ @@ -3123,7 +3123,7 @@ public function testRequestExceptionIsNotReturnedIfThePendingRequestIsSetToThrow public function testRequestExceptionIsReturnedIfThePendingRequestIsSetToThrowOnFailureInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ @@ -3137,7 +3137,7 @@ public function testRequestExceptionIsReturnedIfThePendingRequestIsSetToThrowOnF public function testRequestExceptionIsReturnedIfTheThrowIfOnThePendingRequestIsSetToTrueOnFailureInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ @@ -3151,7 +3151,7 @@ public function testRequestExceptionIsReturnedIfTheThrowIfOnThePendingRequestIsS public function testRequestExceptionIsNotReturnedIfTheThrowIfOnThePendingRequestIsSetToFalseOnFailureInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); [$response] = $this->factory->pool(fn ($pool) => [ @@ -3165,7 +3165,7 @@ public function testRequestExceptionIsNotReturnedIfTheThrowIfOnThePendingRequest public function testRequestExceptionIsReturnedIfTheThrowIfClosureOnThePendingRequestReturnsTrueInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $hitThrowCallback = collect(); @@ -3194,7 +3194,7 @@ public function testRequestExceptionIsReturnedIfTheThrowIfClosureOnThePendingReq public function testRequestExceptionIsNotReturnedIfTheThrowIfClosureOnThePendingRequestReturnsFalseInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $hitThrowCallback = collect(); @@ -3217,7 +3217,7 @@ public function testRequestExceptionIsNotReturnedIfTheThrowIfClosureOnThePending public function testRequestExceptionIsReturnedWithCallbackIfThePendingRequestIsSetToThrowOnFailureInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); $flag = collect(); @@ -3237,7 +3237,7 @@ public function testRequestExceptionIsReturnedWithCallbackIfThePendingRequestIsS public function testRequestExceptionIsReturnedAfterLastRetryInPool() { $this->factory->fake([ - '*' => $this->factory->response(['error'], 403), + '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ @@ -3987,7 +3987,7 @@ public function testItCanHaveGlobalDefaultValues() $allowRedirects = $options['allow_redirects']; $headers = $request->headers(); - return $factory->response(''); + return $factory::response(''); }); $factory->get('https://laravel.com'); diff --git a/tests/Http/Middleware/TrustProxiesTest.php b/tests/Http/Middleware/TrustProxiesTest.php index 54f636f76755..eeb4dac7d5be 100644 --- a/tests/Http/Middleware/TrustProxiesTest.php +++ b/tests/Http/Middleware/TrustProxiesTest.php @@ -42,7 +42,7 @@ public function test_request_does_not_trust() public function test_does_trust_trusted_proxy() { $req = $this->createProxiedRequest(); - $req->setTrustedProxies(['192.168.10.10'], $this->headerAll); + $req::setTrustedProxies(['192.168.10.10'], $this->headerAll); $this->assertSame('173.174.200.38', $req->getClientIp(), 'Assert trusted proxy x-forwarded-for header used'); $this->assertSame('https', $req->getScheme(), 'Assert trusted proxy x-forwarded-proto header used'); @@ -396,7 +396,7 @@ protected function createProxiedRequest($serverOverrides = []) // which is likely something like this: $request = Request::create('http://localhost:8888/tag/proxy', 'GET', [], [], [], $serverOverrides, null); // Need to make sure these haven't already been set - $request->setTrustedProxies([], $this->headerAll); + $request::setTrustedProxies([], $this->headerAll); return $request; } diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 92d89e6ef5b9..8920d1820317 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -26,14 +26,14 @@ public function testClassComponentsAreCompiled() public function testEndComponentsAreCompiled() { - $this->compiler->newComponentHash('foo'); + $this->compiler::newComponentHash('foo'); $this->assertSame('renderComponent(); ?>', $this->compiler->compileString('@endcomponent')); } public function testEndComponentClassesAreCompiled() { - $this->compiler->newComponentHash('foo'); + $this->compiler::newComponentHash('foo'); $this->assertSame(str_replace("\r\n", "\n", 'renderComponent(); ?> diff --git a/types/Database/Eloquent/Factories/Factory.php b/types/Database/Eloquent/Factories/Factory.php index be0fccf6bdfc..486dc541ed75 100644 --- a/types/Database/Eloquent/Factories/Factory.php +++ b/types/Database/Eloquent/Factories/Factory.php @@ -165,10 +165,10 @@ public function definition(): array }; }); -$factory->useNamespace('string'); +$factory::useNamespace('string'); assertType('Illuminate\Database\Eloquent\Factories\Factory', $factory::factoryForModel(User::class)); -assertType('class-string>', $factory->resolveFactoryName(User::class)); +assertType('class-string>', $factory::resolveFactoryName(User::class)); Factory::guessFactoryNamesUsing(function (string $modelName) { return match ($modelName) { diff --git a/types/Support/Collection.php b/types/Support/Collection.php index 99bba538c55f..09227ea84cc8 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -64,16 +64,16 @@ public function __invoke(): string assertType('User', $user); })); -assertType('Illuminate\Support\Collection', $collection->range(1, 100)); +assertType('Illuminate\Support\Collection', $collection::range(1, 100)); -assertType('Illuminate\Support\Collection<(int|string), string>', $collection->wrap('string')); -assertType('Illuminate\Support\Collection<(int|string), User>', $collection->wrap(new User)); +assertType('Illuminate\Support\Collection<(int|string), string>', $collection::wrap('string')); +assertType('Illuminate\Support\Collection<(int|string), User>', $collection::wrap(new User)); -assertType('Illuminate\Support\Collection<(int|string), string>', $collection->wrap(['string'])); -assertType('Illuminate\Support\Collection<(int|string), User>', $collection->wrap(['string' => new User])); +assertType('Illuminate\Support\Collection<(int|string), string>', $collection::wrap(['string'])); +assertType('Illuminate\Support\Collection<(int|string), User>', $collection::wrap(['string' => new User])); -assertType("array<0, 'string'>", $collection->unwrap(['string'])); -assertType('array', $collection->unwrap( +assertType("array<0, 'string'>", $collection::unwrap(['string'])); +assertType('array', $collection::unwrap( $collection )); @@ -643,42 +643,42 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection->mapInto(User::class)); -assertType('Illuminate\Support\Collection', $collection->make([1])->merge([2])); -assertType('Illuminate\Support\Collection', $collection->make(['string'])->merge(['string'])); +assertType('Illuminate\Support\Collection', $collection::make([1])->merge([2])); +assertType('Illuminate\Support\Collection', $collection::make(['string'])->merge(['string'])); -assertType('Illuminate\Support\Collection', $collection->make([1])->merge(['string'])); -assertType('Illuminate\Support\Collection', $collection->make(['string'])->merge([1])); +assertType('Illuminate\Support\Collection', $collection::make([1])->merge(['string'])); +assertType('Illuminate\Support\Collection', $collection::make(['string'])->merge([1])); -assertType('Illuminate\Support\Collection', $collection->make([1])->mergeRecursive([2 => 'string'])); -assertType('Illuminate\Support\Collection', $collection->make(['string'])->mergeRecursive(['string'])); +assertType('Illuminate\Support\Collection', $collection::make([1])->mergeRecursive([2 => 'string'])); +assertType('Illuminate\Support\Collection', $collection::make(['string'])->mergeRecursive(['string'])); -assertType('Illuminate\Support\Collection', $collection->make(['string' => 'string'])->combine([2])); -assertType('Illuminate\Support\Collection', $collection->make([1])->combine([1])); -assertType('Illuminate\Support\Collection', $collection->make(['string'])->combine(['string'])); +assertType('Illuminate\Support\Collection', $collection::make(['string' => 'string'])->combine([2])); +assertType('Illuminate\Support\Collection', $collection::make([1])->combine([1])); +assertType('Illuminate\Support\Collection', $collection::make(['string'])->combine(['string'])); -assertType('Illuminate\Support\Collection', $collection->make([1])->union([1])); -assertType('Illuminate\Support\Collection', $collection->make(['string' => 'string'])->union(['string' => 'string'])); +assertType('Illuminate\Support\Collection', $collection::make([1])->union([1])); +assertType('Illuminate\Support\Collection', $collection::make(['string' => 'string'])->union(['string' => 'string'])); -assertType('mixed', $collection->make()->min()); -assertType('mixed', $collection->make([1])->min()); -assertType('mixed', $collection->make([1])->min('string')); -assertType('mixed', $collection->make(['string' => 1])->min('string')); -assertType('mixed', $collection->make([1])->min(function ($int) { +assertType('mixed', $collection::make()->min()); +assertType('mixed', $collection::make([1])->min()); +assertType('mixed', $collection::make([1])->min('string')); +assertType('mixed', $collection::make(['string' => 1])->min('string')); +assertType('mixed', $collection::make([1])->min(function ($int) { assertType('int', $int); return 1; })); -assertType('mixed', $collection->make([new User])->min('id')); +assertType('mixed', $collection::make([new User])->min('id')); -assertType('mixed', $collection->make()->max()); -assertType('mixed', $collection->make([1])->max()); -assertType('mixed', $collection->make([1])->max('string')); -assertType('mixed', $collection->make([1])->max(function ($int) { +assertType('mixed', $collection::make()->max()); +assertType('mixed', $collection::make([1])->max()); +assertType('mixed', $collection::make([1])->max('string')); +assertType('mixed', $collection::make([1])->max(function ($int) { assertType('int', $int); return 1; })); -assertType('mixed', $collection->make([new User])->max('id')); +assertType('mixed', $collection::make([new User])->max('id')); assertType('Illuminate\Support\Collection', $collection->nth(1, 2)); @@ -699,9 +699,9 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection, Illuminate\Support\Collection>', $collection::make(['string'])->partition('string', 'string')); assertType('Illuminate\Support\Collection, Illuminate\Support\Collection>', $collection::make(['string'])->partition('string')); -assertType('Illuminate\Support\Collection', $collection->make([1])->concat([2])); -assertType('Illuminate\Support\Collection', $collection->make(['string'])->concat(['string'])); -assertType('Illuminate\Support\Collection', $collection->make([1])->concat(['string'])); +assertType('Illuminate\Support\Collection', $collection::make([1])->concat([2])); +assertType('Illuminate\Support\Collection', $collection::make(['string'])->concat(['string'])); +assertType('Illuminate\Support\Collection', $collection::make([1])->concat(['string'])); assertType('Illuminate\Support\Collection', $collection::make([1])->random(2)); assertType('string', $collection::make(['string'])->random()); @@ -760,8 +760,8 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection->reverse()); -// assertType('int|bool', $collection->make([1])->search(2)); -// assertType('string|bool', $collection->make(['string' => 'string'])->search('string')); +// assertType('int|bool', $collection::make([1])->search(2)); +// assertType('string|bool', $collection::make(['string' => 'string'])->search('string')); // assertType('int|bool', $collection->search(function ($user, $int) { // assertType('User', $user); // assertType('int', $int); @@ -769,13 +769,13 @@ function ($collection, $count) { // return true; // })); -assertType('Illuminate\Support\Collection', $collection->make([1])->shuffle()); +assertType('Illuminate\Support\Collection', $collection::make([1])->shuffle()); assertType('Illuminate\Support\Collection', $collection->shuffle()); -assertType('Illuminate\Support\Collection', $collection->make([1])->skip(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->skip(1)); assertType('Illuminate\Support\Collection', $collection->skip(1)); -assertType('Illuminate\Support\Collection', $collection->make([1])->skipUntil(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->skipUntil(1)); assertType('Illuminate\Support\Collection', $collection->skipUntil(new User)); assertType('Illuminate\Support\Collection', $collection->skipUntil(function ($user, $int) { assertType('User', $user); @@ -784,7 +784,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection', $collection->make([1])->skipWhile(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->skipWhile(1)); assertType('Illuminate\Support\Collection', $collection->skipWhile(new User)); assertType('Illuminate\Support\Collection', $collection->skipWhile(function ($user, $int) { assertType('User', $user); @@ -793,14 +793,14 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection', $collection->make([1])->slice(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->slice(1)); assertType('Illuminate\Support\Collection', $collection->slice(1, 2)); assertType('Illuminate\Support\Collection>', $collection->split(3)); -assertType('Illuminate\Support\Collection>', $collection->make([1])->split(3)); +assertType('Illuminate\Support\Collection>', $collection::make([1])->split(3)); -assertType('string', $collection->make(['string' => 'string'])->sole('string', 'string')); -assertType('string', $collection->make(['string' => 'string'])->sole('string', '=', 'string')); +assertType('string', $collection::make(['string' => 'string'])->sole('string', 'string')); +assertType('string', $collection::make(['string' => 'string'])->sole('string', '=', 'string')); assertType('User', $collection->sole(function ($user, $int) { assertType('User', $user); assertType('int', $int); @@ -878,23 +878,23 @@ function ($collection, $count) { return 1; }])); -assertType('Illuminate\Support\Collection', $collection->make([1])->sortKeys()); -assertType('Illuminate\Support\Collection', $collection->make(['string' => 'string'])->sortKeys(1, true)); +assertType('Illuminate\Support\Collection', $collection::make([1])->sortKeys()); +assertType('Illuminate\Support\Collection', $collection::make(['string' => 'string'])->sortKeys(1, true)); -assertType('Illuminate\Support\Collection', $collection->make([1])->sortKeysDesc()); -assertType('Illuminate\Support\Collection', $collection->make(['string' => 'string'])->sortKeysDesc(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->sortKeysDesc()); +assertType('Illuminate\Support\Collection', $collection::make(['string' => 'string'])->sortKeysDesc(1)); -assertType('mixed', $collection->make([1])->sum('string')); -assertType('int<1, 2>', $collection->make(['string'])->sum(function ($string) { +assertType('mixed', $collection::make([1])->sum('string')); +assertType('int<1, 2>', $collection::make(['string'])->sum(function ($string) { assertType('string', $string); return rand(1, 2); })); -assertType('Illuminate\Support\Collection', $collection->make([1])->take(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->take(1)); assertType('Illuminate\Support\Collection', $collection->take(1)); -assertType('Illuminate\Support\Collection', $collection->make([1])->takeUntil(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->takeUntil(1)); assertType('Illuminate\Support\Collection', $collection->takeUntil(new User)); assertType('Illuminate\Support\Collection', $collection->takeUntil(function ($user, $int) { assertType('User', $user); @@ -903,7 +903,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection', $collection->make([1])->takeWhile(1)); +assertType('Illuminate\Support\Collection', $collection::make([1])->takeWhile(1)); assertType('Illuminate\Support\Collection', $collection->takeWhile(new User)); assertType('Illuminate\Support\Collection', $collection->takeWhile(function ($user, $int) { assertType('User', $user); @@ -921,7 +921,7 @@ function ($collection, $count) { return collect([1]); })); -assertType('1', $collection->make([1])->pipe(function ($collection) { +assertType('1', $collection::make([1])->pipe(function ($collection) { assertType('Illuminate\Support\Collection', $collection); return 1; @@ -929,8 +929,8 @@ function ($collection, $count) { assertType('User', $collection->pipeInto(User::class)); -assertType('Illuminate\Support\Collection<(int|string), mixed>', $collection->make(['string' => 'string'])->pluck('string')); -assertType('Illuminate\Support\Collection<(int|string), mixed>', $collection->make(['string' => 'string'])->pluck('string', 'string')); +assertType('Illuminate\Support\Collection<(int|string), mixed>', $collection::make(['string' => 'string'])->pluck('string')); +assertType('Illuminate\Support\Collection<(int|string), mixed>', $collection::make(['string' => 'string'])->pluck('string', 'string')); assertType('Illuminate\Support\Collection', $collection->reject()); assertType('Illuminate\Support\Collection', $collection->reject(new User)); @@ -953,7 +953,7 @@ function ($collection, $count) { return $user->getTable(); })); -assertType('Illuminate\Support\Collection', $collection->make(['string' => 'string'])->unique(function ($stringA, $stringB) { +assertType('Illuminate\Support\Collection', $collection::make(['string' => 'string'])->unique(function ($stringA, $stringB) { assertType('string', $stringA); assertType('string', $stringB); @@ -972,18 +972,18 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection::make(['string', 'string'])->values()); assertType('Illuminate\Support\Collection', $collection::make(['string', 1])->values()); -assertType('Illuminate\Support\Collection', $collection->make([1])->pad(2, 0)); -assertType('Illuminate\Support\Collection', $collection->make([1])->pad(2, 'string')); +assertType('Illuminate\Support\Collection', $collection::make([1])->pad(2, 0)); +assertType('Illuminate\Support\Collection', $collection::make([1])->pad(2, 'string')); assertType('Illuminate\Support\Collection', $collection->pad(2, 0)); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make([1])->countBy()); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make(['string' => 'string'])->countBy('string')); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make([new User])->countBy('email')); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => 'email')); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => 0)); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => Digit::One)); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => NamedDigit::One)); -assertType('Illuminate\Support\Collection<(int|string), int>', $collection->make(['string'])->countBy(function ($string, $int) { +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make([1])->countBy()); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make(['string' => 'string'])->countBy('string')); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make([new User])->countBy('email')); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => 'email')); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => 0)); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => Digit::One)); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => NamedDigit::One)); +assertType('Illuminate\Support\Collection<(int|string), int>', $collection::make(['string'])->countBy(function ($string, $int) { assertType('string', $string); assertType('int', $int); @@ -995,9 +995,9 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection>', $collection::make(['string' => 'string'])->zip(['string'])); assertType('Illuminate\Support\Collection', $collection->collect()); -assertType('Illuminate\Support\Collection', $collection->make([1])->collect()); +assertType('Illuminate\Support\Collection', $collection::make([1])->collect()); -assertType('Illuminate\Support\Collection', $collection->make([1])->push(2)); +assertType('Illuminate\Support\Collection', $collection::make([1])->push(2)); assertType('array', $collection->all()); @@ -1021,10 +1021,10 @@ function ($collection, $count) { 'string-key-2' => 'string-value-2', ])->pop(2)); -assertType('Illuminate\Support\Collection', $collection->make([1])->prepend(2)); +assertType('Illuminate\Support\Collection', $collection::make([1])->prepend(2)); assertType('Illuminate\Support\Collection', $collection->prepend(new User, 2)); -assertType('Illuminate\Support\Collection', $collection->make([1])->push(2)); +assertType('Illuminate\Support\Collection', $collection::make([1])->push(2)); assertType('Illuminate\Support\Collection', $collection->push(new User, new User)); assertType('User|null', $collection->pull(1)); diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index f04a81314dd1..30f30eeea905 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -57,16 +57,16 @@ public function toArray(): array assertType('User', $user); })); -assertType('Illuminate\Support\LazyCollection', $collection->range(1, 100)); +assertType('Illuminate\Support\LazyCollection', $collection::range(1, 100)); -assertType('Illuminate\Support\LazyCollection<(int|string), string>', $collection->wrap('string')); -assertType('Illuminate\Support\LazyCollection<(int|string), User>', $collection->wrap(new User)); +assertType('Illuminate\Support\LazyCollection<(int|string), string>', $collection::wrap('string')); +assertType('Illuminate\Support\LazyCollection<(int|string), User>', $collection::wrap(new User)); -assertType('Illuminate\Support\LazyCollection<(int|string), string>', $collection->wrap(['string'])); -assertType('Illuminate\Support\LazyCollection<(int|string), User>', $collection->wrap(['string' => new User])); +assertType('Illuminate\Support\LazyCollection<(int|string), string>', $collection::wrap(['string'])); +assertType('Illuminate\Support\LazyCollection<(int|string), User>', $collection::wrap(['string' => new User])); -assertType("array<0, 'string'>", $collection->unwrap(['string'])); -assertType('array', $collection->unwrap( +assertType("array<0, 'string'>", $collection::unwrap(['string'])); +assertType('array', $collection::unwrap( $collection )); @@ -536,40 +536,40 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection', $collection->mapInto(User::class)); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->merge([2])); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string'])->merge(['string'])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->merge([2])); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string'])->merge(['string'])); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->merge(['string'])); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string'])->merge([1])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->merge(['string'])); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string'])->merge([1])); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->mergeRecursive([2])); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string'])->mergeRecursive(['string'])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->mergeRecursive([2])); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string'])->mergeRecursive(['string'])); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string' => 'string'])->combine([2])); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->combine([1])); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string' => 'string'])->combine([2])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->combine([1])); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->union([1])); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string' => 'string'])->union(['string' => 'string'])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->union([1])); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string' => 'string'])->union(['string' => 'string'])); -assertType('mixed', $collection->make()->min()); -assertType('mixed', $collection->make([1])->min()); -assertType('mixed', $collection->make([1])->min('string')); -assertType('mixed', $collection->make([1])->min(function ($int) { +assertType('mixed', $collection::make()->min()); +assertType('mixed', $collection::make([1])->min()); +assertType('mixed', $collection::make([1])->min('string')); +assertType('mixed', $collection::make([1])->min(function ($int) { assertType('int', $int); return 1; })); -assertType('mixed', $collection->make([new User])->min('id')); +assertType('mixed', $collection::make([new User])->min('id')); -assertType('mixed', $collection->make()->max()); -assertType('mixed', $collection->make([1])->max()); -assertType('mixed', $collection->make([1])->max('string')); -assertType('mixed', $collection->make([1])->max(function ($int) { +assertType('mixed', $collection::make()->max()); +assertType('mixed', $collection::make([1])->max()); +assertType('mixed', $collection::make([1])->max('string')); +assertType('mixed', $collection::make([1])->max(function ($int) { assertType('int', $int); return 1; })); -assertType('mixed', $collection->make([new User])->max('id')); +assertType('mixed', $collection::make([new User])->max('id')); assertType('Illuminate\Support\LazyCollection', $collection->nth(1, 2)); @@ -590,12 +590,12 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection, Illuminate\Support\LazyCollection>', $collection::make(['string'])->partition('string', 'string')); assertType('Illuminate\Support\LazyCollection, Illuminate\Support\LazyCollection>', $collection::make(['string'])->partition('string')); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->concat([2])); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string'])->concat(['string'])); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->concat(['string'])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->concat([2])); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string'])->concat(['string'])); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->concat(['string'])); -assertType('Illuminate\Support\LazyCollection|int', $collection->make([1])->random(2)); -assertType('Illuminate\Support\LazyCollection|string', $collection->make(['string'])->random()); +assertType('Illuminate\Support\LazyCollection|int', $collection::make([1])->random(2)); +assertType('Illuminate\Support\LazyCollection|string', $collection::make(['string'])->random()); assertType('1', $collection ->reduce(function ($null, $user) { @@ -620,8 +620,8 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection', $collection->reverse()); -// assertType('int|bool', $collection->make([1])->search(2)); -// assertType('string|bool', $collection->make(['string' => 'string'])->search('string')); +// assertType('int|bool', $collection::make([1])->search(2)); +// assertType('string|bool', $collection::make(['string' => 'string'])->search('string')); // assertType('int|bool', $collection->search(function ($user, $int) { // assertType('User', $user); // assertType('int', $int); @@ -629,13 +629,13 @@ public function toArray(): array // return true; // })); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->shuffle()); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->shuffle()); assertType('Illuminate\Support\LazyCollection', $collection->shuffle()); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->skip(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->skip(1)); assertType('Illuminate\Support\LazyCollection', $collection->skip(1)); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->skipUntil(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->skipUntil(1)); assertType('Illuminate\Support\LazyCollection', $collection->skipUntil(new User)); assertType('Illuminate\Support\LazyCollection', $collection->skipUntil(function ($user, $int) { assertType('User', $user); @@ -644,7 +644,7 @@ public function toArray(): array return true; })); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->skipWhile(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->skipWhile(1)); assertType('Illuminate\Support\LazyCollection', $collection->skipWhile(new User)); assertType('Illuminate\Support\LazyCollection', $collection->skipWhile(function ($user, $int) { assertType('User', $user); @@ -653,14 +653,14 @@ public function toArray(): array return true; })); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->slice(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->slice(1)); assertType('Illuminate\Support\LazyCollection', $collection->slice(1, 2)); assertType('Illuminate\Support\LazyCollection>', $collection->split(3)); -assertType('Illuminate\Support\LazyCollection>', $collection->make([1])->split(3)); +assertType('Illuminate\Support\LazyCollection>', $collection::make([1])->split(3)); -assertType('string', $collection->make(['string' => 'string'])->sole('string', 'string')); -assertType('string', $collection->make(['string' => 'string'])->sole('string', '=', 'string')); +assertType('string', $collection::make(['string' => 'string'])->sole('string', 'string')); +assertType('string', $collection::make(['string' => 'string'])->sole('string', '=', 'string')); assertType('User', $collection->sole(function ($user, $int) { assertType('User', $user); assertType('int', $int); @@ -738,23 +738,23 @@ public function toArray(): array return 1; }])); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->sortKeys()); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string' => 'string'])->sortKeys(1, true)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->sortKeys()); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string' => 'string'])->sortKeys(1, true)); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->sortKeysDesc()); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string' => 'string'])->sortKeysDesc(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->sortKeysDesc()); +assertType('Illuminate\Support\LazyCollection', $collection::make(['string' => 'string'])->sortKeysDesc(1)); -assertType('mixed', $collection->make([1])->sum('string')); -assertType('int<1, 2>', $collection->make(['string'])->sum(function ($string) { +assertType('mixed', $collection::make([1])->sum('string')); +assertType('int<1, 2>', $collection::make(['string'])->sum(function ($string) { assertType('string', $string); return rand(1, 2); })); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->take(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->take(1)); assertType('Illuminate\Support\LazyCollection', $collection->take(1)); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->takeUntil(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->takeUntil(1)); assertType('Illuminate\Support\LazyCollection', $collection->takeUntil(new User)); assertType('Illuminate\Support\LazyCollection', $collection->takeUntil(function ($user, $int) { assertType('User', $user); @@ -769,7 +769,7 @@ public function toArray(): array // assertType('int|null', $int); })); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->takeWhile(1)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->takeWhile(1)); assertType('Illuminate\Support\LazyCollection', $collection->takeWhile(new User)); assertType('Illuminate\Support\LazyCollection', $collection->takeWhile(function ($user, $int) { assertType('User', $user); @@ -787,7 +787,7 @@ public function toArray(): array return new LazyCollection([1]); })); -assertType('1', $collection->make([1])->pipe(function ($collection) { +assertType('1', $collection::make([1])->pipe(function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); return 1; @@ -795,8 +795,8 @@ public function toArray(): array assertType('User', $collection->pipeInto(User::class)); -assertType('Illuminate\Support\LazyCollection<(int|string), mixed>', $collection->make(['string' => 'string'])->pluck('string')); -assertType('Illuminate\Support\LazyCollection<(int|string), mixed>', $collection->make(['string' => 'string'])->pluck('string', 'string')); +assertType('Illuminate\Support\LazyCollection<(int|string), mixed>', $collection::make(['string' => 'string'])->pluck('string')); +assertType('Illuminate\Support\LazyCollection<(int|string), mixed>', $collection::make(['string' => 'string'])->pluck('string', 'string')); assertType('Illuminate\Support\LazyCollection', $collection->reject()); assertType('Illuminate\Support\LazyCollection', $collection->reject(function ($user) { @@ -819,7 +819,7 @@ public function toArray(): array return $user->getTable(); })); -assertType('Illuminate\Support\LazyCollection', $collection->make(['string' => 'string'])->unique(function ($stringA, $stringB) { +assertType('Illuminate\Support\LazyCollection', $collection::make(['string' => 'string'])->unique(function ($stringA, $stringB) { assertType('string', $stringA); assertType('string', $stringB); @@ -838,18 +838,18 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection', $collection::make(['string', 'string'])->values()); assertType('Illuminate\Support\LazyCollection', $collection::make(['string', 1])->values()); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->pad(2, 0)); -assertType('Illuminate\Support\LazyCollection', $collection->make([1])->pad(2, 'string')); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->pad(2, 0)); +assertType('Illuminate\Support\LazyCollection', $collection::make([1])->pad(2, 'string')); assertType('Illuminate\Support\LazyCollection', $collection->pad(2, 0)); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make([1])->countBy()); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make(['string' => 'string'])->countBy('string')); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make([new User])->countBy('email')); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => 'email')); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => 0)); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => Digit::One)); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make([new User])->countBy(static fn ($user) => NamedDigit::One)); -assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection->make(['string'])->countBy(function ($string, $int) { +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make([1])->countBy()); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make(['string' => 'string'])->countBy('string')); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make([new User])->countBy('email')); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => 'email')); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => 0)); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => Digit::One)); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make([new User])->countBy(static fn ($user) => NamedDigit::One)); +assertType('Illuminate\Support\LazyCollection<(int|string), int>', $collection::make(['string'])->countBy(function ($string, $int) { assertType('string', $string); assertType('int', $int); @@ -861,7 +861,7 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection>', $collection::make(['string' => 'string'])->zip(['string'])); assertType('Illuminate\Support\Collection', $collection->collect()); -assertType('Illuminate\Support\Collection', $collection->make([1])->collect()); +assertType('Illuminate\Support\Collection', $collection::make([1])->collect()); assertType('array', $collection->all()); From 99f4ce250ff42f7889afb41f1ae6ebca4ea7ba9e Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 15 Apr 2026 13:59:23 +0100 Subject: [PATCH 164/596] 13.x-table-attr-child-override (#59701) explicit readability pls dont add a space im only human... after all test up Update DatabaseEloquentModelAttributesTest.php --- src/Illuminate/Database/Eloquent/Model.php | 12 ++++- .../DatabaseEloquentModelAttributesTest.php | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index b851d15377c9..78a05ce62ae0 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -435,7 +435,17 @@ public function initializeModelAttributes() { $table = static::resolveClassAttribute(Table::class); - $this->table ??= $table->name ?? null; + $reflection = new ReflectionClass(static::class); + + $declaresTable = $reflection->hasProperty('table') + && $reflection->getProperty('table')->getDeclaringClass()->getName() === static::class; + + if (! $declaresTable && $reflection->getAttributes(Table::class) !== []) { + $this->table = $table->name ?? null; + } else { + $this->table ??= $table->name ?? null; + } + $this->connection ??= static::resolveClassAttribute(Connection::class, 'name'); if ($this->primaryKey === 'id' && $table && $table->key !== null) { diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index 8279ac4dc5ad..f3988e817b06 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -54,6 +54,27 @@ public function test_table_property_takes_precedence(): void $this->assertSame('property_table', $model->getTable()); } + public function test_child_table_attribute_overrides_inherited_table_property(): void + { + $model = new ChildModelWithTableAttribute; + + $this->assertSame('child_attr', $model->getTable()); + } + + public function test_child_inherits_parent_table_attribute(): void + { + $model = new ChildModelWithNoTable; + + $this->assertSame('parent_attr', $model->getTable()); + } + + public function test_child_table_property_overrides_parent_table_attribute(): void + { + $model = new ChildModelWithTableProperty; + + $this->assertSame('child_prop', $model->getTable()); + } + public function test_primary_key_attribute(): void { $model = new ModelWithPrimaryKeyAttribute; @@ -406,6 +427,33 @@ class ModelWithTableAttributeAndProperty extends Model protected $table = 'property_table'; } +class ParentModelWithTableProperty extends Model +{ + protected $table = 'parent_prop'; +} + +#[Table(name: 'child_attr')] +class ChildModelWithTableAttribute extends ParentModelWithTableProperty +{ + // +} + +#[Table(name: 'parent_attr')] +class ParentModelWithTableAttribute extends Model +{ + // +} + +class ChildModelWithNoTable extends ParentModelWithTableAttribute +{ + // +} + +class ChildModelWithTableProperty extends ParentModelWithTableAttribute +{ + protected $table = 'child_prop'; +} + #[Table(key: 'custom_id')] class ModelWithPrimaryKeyAttribute extends Model { From 5937afcfaad7867afeb2b9e321b17eeefcb8cd94 Mon Sep 17 00:00:00 2001 From: Bipin Kareparambil Date: Wed, 15 Apr 2026 17:01:57 +0400 Subject: [PATCH 165/596] [13.x] Return null from Cursor::fromEncoded for malformed payloads (#59699) --- src/Illuminate/Pagination/Cursor.php | 4 ++++ tests/Pagination/CursorTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Illuminate/Pagination/Cursor.php b/src/Illuminate/Pagination/Cursor.php index 433e33e0ae1c..579c741a7f4e 100644 --- a/src/Illuminate/Pagination/Cursor.php +++ b/src/Illuminate/Pagination/Cursor.php @@ -125,6 +125,10 @@ public static function fromEncoded($encodedString) return null; } + if (! is_array($parameters) || ! array_key_exists('_pointsToNextItems', $parameters)) { + return null; + } + $pointsToNextItems = $parameters['_pointsToNextItems']; unset($parameters['_pointsToNextItems']); diff --git a/tests/Pagination/CursorTest.php b/tests/Pagination/CursorTest.php index 78f7cfc2e4f9..1d40025b09c7 100644 --- a/tests/Pagination/CursorTest.php +++ b/tests/Pagination/CursorTest.php @@ -18,6 +18,30 @@ public function testCanEncodeAndDecodeSuccessfully() $this->assertEquals($cursor, Cursor::fromEncoded($cursor->encode())); } + public function testFromEncodedReturnsNullForNonStringInput() + { + $this->assertNull(Cursor::fromEncoded(null)); + $this->assertNull(Cursor::fromEncoded(123)); + } + + public function testFromEncodedReturnsNullForInvalidJson() + { + $this->assertNull(Cursor::fromEncoded(base64_encode('not-json'))); + } + + public function testFromEncodedReturnsNullWhenDecodedPayloadIsNotAnArray() + { + $this->assertNull(Cursor::fromEncoded(base64_encode(json_encode('scalar')))); + $this->assertNull(Cursor::fromEncoded(base64_encode(json_encode(null)))); + } + + public function testFromEncodedReturnsNullWhenPointsToNextItemsKeyIsMissing() + { + $payload = base64_encode(json_encode(['id' => 422])); + + $this->assertNull(Cursor::fromEncoded($payload)); + } + public function testCanGetParams() { $cursor = new Cursor([ From 68b2b02ee5027f1626f28ee95f2c6bac70756fb0 Mon Sep 17 00:00:00 2001 From: "Paul A." Date: Wed, 15 Apr 2026 16:05:40 +0300 Subject: [PATCH 166/596] [12.x] Fix infinite rate limiter TTL on custom increments (#59693) * wip * add test * fix test --- src/Illuminate/Cache/RateLimiter.php | 4 ++-- tests/Cache/CacheRateLimiterTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index aef6cec0c96b..20d23cb05cdd 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -171,9 +171,9 @@ public function increment($key, $decaySeconds = 60, $amount = 1) $hits = (int) $this->cache->increment($key, $amount); - if (! $added && $hits == 1) { + if (! $added && $hits == $amount) { $this->withoutSerializationOrCompression( - fn () => $this->cache->put($key, 1, $decaySeconds) + fn () => $this->cache->put($key, $amount, $decaySeconds) ); } diff --git a/tests/Cache/CacheRateLimiterTest.php b/tests/Cache/CacheRateLimiterTest.php index dea5e0841f37..9b2e4f422165 100644 --- a/tests/Cache/CacheRateLimiterTest.php +++ b/tests/Cache/CacheRateLimiterTest.php @@ -71,6 +71,19 @@ public function testHitHasNoMemoryLeak() $rateLimiter->hit('key', 1); } + public function testIncrementWithCustomAmountHasNoMemoryLeak() + { + $cache = m::mock(Cache::class); + $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 60)->andReturn(true); + $cache->shouldReceive('add')->once()->with('key', 0, 60)->andReturn(false); + $cache->shouldReceive('increment')->once()->with('key', 2)->andReturn(2); + $cache->shouldReceive('put')->once()->with('key', 2, 60); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); + $rateLimiter = new RateLimiter($cache); + + $rateLimiter->increment('key', 60, 2); + } + public function testRemainingIsNotNegative(): void { $cache = m::mock(Cache::class); From 9c0e9e8b1e9a5f6ae6afc873d8e30e53da8ebc72 Mon Sep 17 00:00:00 2001 From: "Paul A." Date: Wed, 15 Apr 2026 16:43:05 +0300 Subject: [PATCH 167/596] [13.x] Port forward rate limiter fix (#59706) * wip * rollback * ci * ci --- src/Illuminate/Cache/RateLimiter.php | 4 ++-- tests/Cache/CacheRateLimiterTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index aef6cec0c96b..20d23cb05cdd 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -171,9 +171,9 @@ public function increment($key, $decaySeconds = 60, $amount = 1) $hits = (int) $this->cache->increment($key, $amount); - if (! $added && $hits == 1) { + if (! $added && $hits == $amount) { $this->withoutSerializationOrCompression( - fn () => $this->cache->put($key, 1, $decaySeconds) + fn () => $this->cache->put($key, $amount, $decaySeconds) ); } diff --git a/tests/Cache/CacheRateLimiterTest.php b/tests/Cache/CacheRateLimiterTest.php index e8c221626a60..58496b737b10 100644 --- a/tests/Cache/CacheRateLimiterTest.php +++ b/tests/Cache/CacheRateLimiterTest.php @@ -72,6 +72,19 @@ public function testHitHasNoMemoryLeak() $rateLimiter->hit('key', 1); } + public function testIncrementWithCustomAmountHasNoMemoryLeak() + { + $cache = m::mock(Cache::class); + $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 60)->andReturn(true); + $cache->shouldReceive('add')->once()->with('key', 0, 60)->andReturn(false); + $cache->shouldReceive('increment')->once()->with('key', 2)->andReturn(2); + $cache->shouldReceive('put')->once()->with('key', 2, 60); + $cache->shouldReceive('getStore')->andReturn(new ArrayStore); + $rateLimiter = new RateLimiter($cache); + + $rateLimiter->increment('key', 60, 2); + } + public function testRemainingIsNotNegative(): void { $cache = m::mock(Cache::class); From 09cb37d5334b428c11ff5771ace97bc3ed1cbdc7 Mon Sep 17 00:00:00 2001 From: Matthew Nessworthy Date: Wed, 15 Apr 2026 16:49:45 +0200 Subject: [PATCH 168/596] [13.x] Add debounceable queued jobs (#59507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add debounceable queued jobs Adds last-dispatch-wins semantics for queued jobs via ShouldBeDebounced interface. When multiple dispatches occur for the same debounce identity, only the most recent executes. New files: - ShouldBeDebounced marker interface - DebounceLock cache-based lock manager - DebounceFor PHP 8 attribute - JobDebounced event - Debounced standalone middleware Framework integration: - PendingDispatch acquires debounce lock at dispatch time - CallQueuedHandler checks ownership at execution time - Queue/SyncQueue register transaction rollback handlers Co-Authored-By: Claude Opus 4.6 (1M context) * chore: styleci * fix: resolve debounce CI test failures - Guard ensureDebounceLockIsReleased() call with instanceof ShouldBeDebounced to prevent extra isReleased() calls breaking ThrottlesExceptionsTest mocks - Extend debounce lock TTL to debounceFor*2 so lock remains valid when the delayed job becomes available for processing - Travel past debounce window in DebouncedJobTest before running queue worker so delayed jobs are available on async queue drivers Co-Authored-By: Claude Opus 4.6 (1M context) * Fix debounce lock lifecycle correctness issues - Make release() owner-aware to prevent wiping a newer dispatch's lock - Fail-open when lock is missing (cache eviction/TTL expiry) instead of silently deleting the job - Continue chain/batch dispatch when a debounced job is superseded - Release debounce lock via context on model-not-found exceptions - Fire JobDebounced event from Debounced middleware for parity with the ShouldBeDebounced interface path Co-Authored-By: Claude Opus 4.6 (1M context) * Fix lock TTL to survive past job delay for supersession check The lock TTL must exceed the debounce delay so the ownership check can distinguish superseded jobs from expired locks at execution time. Co-Authored-By: Claude Opus 4.6 (1M context) * Replace lock-based debounce with plain cache key Locks are the wrong abstraction for debounce — we need a "latest owner" marker, not mutual exclusion. This replaces forceRelease/get/restoreLock with simple cache put/get/forget operations. The TTL is now generous (10x debounceFor, min 300s) for garbage collection only — correctness no longer depends on it. A 15-minute debounce window no longer requires a 30-minute lock. Co-Authored-By: Claude Opus 4.6 (1M context) * Add comment explaining generous cache TTL in DebounceLock Co-Authored-By: Claude Opus 4.6 (1M context) * Fix debounce test failures on Beanstalkd and Redis drivers - Skip delayed-job tests on Beanstalkd (travelTo cannot control an external server's delay timer) - Replace Event::fake with Event::listen for the superseded event test to avoid subtle interaction issues with queue:work artisan command Co-Authored-By: Claude Opus 4.6 (1M context) * Don't release debounce token after execution Releasing the token after the current owner executes creates a race: if the current job processes before a superseded one, removing the token causes the superseded job to see an empty cache and execute via fail-open. The token's only purpose is supersession detection. Let the generous GC TTL (min 300s) handle cleanup. Transaction rollback callbacks still release the token when a dispatch is abandoned. Co-Authored-By: Claude Opus 4.6 (1M context) * Remove Debounced middleware and ShouldBeDebounced interface Simplify to attribute-only approach per maintainer feedback. The #[DebounceFor] attribute is now the sole mechanism for enabling job debouncing. Co-Authored-By: Claude Opus 4.6 (1M context) * Clean up debounce implementation after review Move debounceOwner property to Queueable trait to avoid PHP 8.2+ dynamic property deprecations, extract getDebounceDelay() on DebounceLock to eliminate duplicated resolution logic in PendingDispatch, and fix stale comment about token lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) * Add test for debounceFor() method override path Co-Authored-By: Claude Opus 4.6 (1M context) * Skip debounce lock resolution for non-debounceable jobs Avoid resolving Cache from the container when the job has no debounce configuration, fixing BindingResolutionException in test environments without a cache store bound. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: styleci * Add call-site debounceFor, max-wait cap, fix superseded chain/batch behavior - dispatch(new Job)->debounceFor(30) enables debouncing at the call site - #[DebounceFor(30, maxWait: 120)] caps how long a job can be deferred - Superseded jobs no longer trigger chain dispatch or batch success recording - Add test coverage for debounceVia() custom cache store * formatting * formatting * formatting * formatting --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/DebounceLock.php | 188 ++++++++ src/Illuminate/Bus/Queueable.php | 7 + .../Foundation/Bus/PendingDispatch.php | 40 +- .../Queue/Attributes/DebounceFor.php | 17 + src/Illuminate/Queue/CallQueuedHandler.php | 48 ++ src/Illuminate/Queue/Events/JobDebounced.php | 20 + src/Illuminate/Queue/Queue.php | 9 + src/Illuminate/Queue/SyncQueue.php | 9 + tests/Integration/Queue/DebouncedJobTest.php | 447 ++++++++++++++++++ 9 files changed, 784 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Bus/DebounceLock.php create mode 100644 src/Illuminate/Queue/Attributes/DebounceFor.php create mode 100644 src/Illuminate/Queue/Events/JobDebounced.php create mode 100644 tests/Integration/Queue/DebouncedJobTest.php diff --git a/src/Illuminate/Bus/DebounceLock.php b/src/Illuminate/Bus/DebounceLock.php new file mode 100644 index 000000000000..ed32aa9eb162 --- /dev/null +++ b/src/Illuminate/Bus/DebounceLock.php @@ -0,0 +1,188 @@ +cache = $cache; + } + + /** + * Store a debounce owner token for the given job. + * + * Overwrites any existing token, implementing last-writer-wins semantics. + * + * @param mixed $job + * @param int|null $debounceFor + * @param int|null $maxWait + * @return array{owner: string, maxWaitExceeded: bool} + */ + public function acquire($job, $debounceFor = null, $maxWait = null) + { + $cache = $this->resolveCache($job); + + $ttl = max(($debounceFor ?? $this->getDebounceDelay($job)) * 10, 300); + + $cache->put($key = static::getKey($job), $owner = Str::random(40), $ttl); + + return [ + 'owner' => $owner, + 'maxWaitExceeded' => $this->maxWaitExceeded( + $cache, $key, $ttl, $maxWait ?? $this->getMaxDebounceWait($job) + ) + ]; + } + + /** + * Determine if the maximum debounce wait time has been exceeded. + */ + protected function maxWaitExceeded(Cache $cache, string $key, int $ttl, ?int $maxWait): bool + { + if (is_null($maxWait)) { + return false; + } + + $timestampKey = $key.':first_dispatched_at'; + + if (! $cache->has($timestampKey)) { + $cache->put($timestampKey, Carbon::now()->timestamp, $ttl); + + return false; + } + + $elapsed = Carbon::now()->timestamp - $cache->get($timestampKey); + + if ($elapsed >= $maxWait) { + $cache->forget($timestampKey); + + return true; + } + + return false; + } + + /** + * Determine if the given owner is the current owner for this debounce key. + * + * @param mixed $job + * @param string $owner + * @return bool + */ + public function isCurrentOwner($job, string $owner) + { + return $this->resolveCache($job)->get(static::getKey($job)) === $owner; + } + + /** + * Determine if a debounce token exists for the given job. + * + * @param mixed $job + * @return bool + */ + public function lockExists($job) + { + return ! is_null($this->resolveCache($job)->get(static::getKey($job))); + } + + /** + * Remove the debounce token for the given job. + * + * @param mixed $job + * @param string $owner + * @return void + */ + public function release($job, string $owner = '') + { + $key = static::getKey($job); + + $cache = $this->resolveCache($job); + + if (! empty($owner) && $cache->get($key) !== $owner) { + return; + } + + $cache->forget($key); + $cache->forget($key.':first_dispatched_at'); + } + + /** + * Get the debounce delay for the given job. + * + * @param mixed $job + * @return int|null + */ + public function getDebounceDelay($job) + { + return $this->getAttributeValue($job, DebounceFor::class, 'debounceFor'); + } + + /** + * Get the maximum debounce wait time for the given job. + * + * @param mixed $job + * @return int|null + */ + public function getMaxDebounceWait($job) + { + $attributes = (new ReflectionClass($job))->getAttributes(DebounceFor::class); + + return count($attributes) > 0 + ? $attributes[0]->newInstance()->maxWait + : null; + } + + /** + * Generate the cache key for the given job. + * + * @param mixed $job + * @return string + */ + public static function getKey($job) + { + $debounceId = method_exists($job, 'debounceId') + ? $job->debounceId() + : ($job->debounceId ?? ''); + + $jobName = method_exists($job, 'displayName') + ? hash('xxh128', $job->displayName()) + : get_class($job); + + return 'laravel_debounced_job:'.$jobName.':'.$debounceId; + } + + /** + * Resolve the cache store for the given job. + * + * @param mixed $job + * @return \Illuminate\Contracts\Cache\Repository + */ + protected function resolveCache($job) + { + return method_exists($job, 'debounceVia') + ? ($job->debounceVia() ?? $this->cache) + : $this->cache; + } +} diff --git a/src/Illuminate/Bus/Queueable.php b/src/Illuminate/Bus/Queueable.php index f7750feecac0..ef95e2890e99 100644 --- a/src/Illuminate/Bus/Queueable.php +++ b/src/Illuminate/Bus/Queueable.php @@ -42,6 +42,13 @@ trait Queueable */ public $deduplicator; + /** + * The lock owner token for debounce supersession checks. + * + * @var string + */ + public $debounceOwner = ''; + /** * The number of seconds before the job should be made available. * diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index 1dffdb06b18c..92e2baa104dc 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -2,12 +2,16 @@ namespace Illuminate\Foundation\Bus; +use Illuminate\Bus\DebounceLock; use Illuminate\Bus\UniqueLock; use Illuminate\Container\Container; use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Foundation\Queue\InteractsWithUniqueJobs; +use Illuminate\Queue\Attributes\DebounceFor; +use LogicException; +use ReflectionClass; class PendingDispatch { @@ -209,6 +213,36 @@ protected function shouldDispatch() ->acquire($this->job); } + /** + * Acquire a debounce lock for the job and set its delay. + * + * @return void + * + * @throws \LogicException + */ + protected function acquireDebounceLock() + { + if (empty((new ReflectionClass($this->job))->getAttributes(DebounceFor::class))) { + return; + } + + if ($this->job instanceof ShouldBeUnique) { + throw new LogicException('A debounced job cannot also implement ShouldBeUnique.'); + } + + $lock = new DebounceLock(Container::getInstance()->make(Cache::class)); + + $result = $lock->acquire( + $this->job, $debounceFor = $lock->getDebounceDelay($this->job) + ); + + $this->job->debounceOwner = $result['owner']; + + if (is_null($this->job->delay)) { + $this->job->delay = $result['maxWaitExceeded'] ? 0 : $debounceFor; + } + } + /** * Get the underlying job instance. * @@ -246,7 +280,11 @@ public function __destruct() $this->removeUniqueJobInformationFromContext($this->job); return; - } elseif ($this->afterResponse) { + } + + $this->acquireDebounceLock(); + + if ($this->afterResponse) { app(Dispatcher::class)->dispatchAfterResponse($this->job); } else { app(Dispatcher::class)->dispatch($this->job); diff --git a/src/Illuminate/Queue/Attributes/DebounceFor.php b/src/Illuminate/Queue/Attributes/DebounceFor.php new file mode 100644 index 000000000000..dbb04f816b08 --- /dev/null +++ b/src/Illuminate/Queue/Attributes/DebounceFor.php @@ -0,0 +1,17 @@ +handleModelNotFound($job, $e); } + if ($this->commandShouldBeDebounced($command)) { + return $this->deleteDebouncedJob($job, $command); + } + $this->dispatchThroughMiddleware($job, $command); if (! $job->isReleased() && ! $this->commandShouldBeUniqueUntilProcessing($command)) { @@ -216,6 +222,48 @@ protected function ensureUniqueJobLockIsReleased($command) } } + /** + * Determine if the debounced command was superseded by a newer dispatch. + * + * @param mixed $command + * @return bool + */ + protected function commandShouldBeDebounced($command) + { + $owner = $command->debounceOwner ?? ''; + + if (empty($owner)) { + return false; + } + + $lock = new DebounceLock($this->container->make(Cache::class)); + + // Fail-open: if the lock no longer exists (cache eviction, TTL expiry), let the job execute... + if (! $lock->lockExists($command)) { + return false; + } + + return ! $lock->isCurrentOwner($command, $owner); + } + + /** + * Handle a debounced (superseded) job by firing an event and deleting it. + * + * @param \Illuminate\Contracts\Queue\Job $job + * @param mixed $command + * @return void + */ + protected function deleteDebouncedJob($job, $command) + { + if ($this->container->bound('events')) { + $this->container->make('events')->dispatch( + new JobDebounced($job->getConnectionName(), $job, $command) + ); + } + + $job->delete(); + } + /** * Determine if the given command should be unique. */ diff --git a/src/Illuminate/Queue/Events/JobDebounced.php b/src/Illuminate/Queue/Events/JobDebounced.php new file mode 100644 index 000000000000..27b52604736a --- /dev/null +++ b/src/Illuminate/Queue/Events/JobDebounced.php @@ -0,0 +1,20 @@ +debounceOwner ?? '')) { + $this->container->make('db.transactions')->addCallbackForRollback( + function () use ($job) { + (new DebounceLock($this->container->make(Cache::class)))->release($job, $job->debounceOwner ?? ''); + } + ); + } + return $this->container->make('db.transactions')->addCallback( function () use ($queue, $job, $payload, $delay, $callback) { $this->raiseJobQueueingEvent($queue, $job, $payload, $delay); diff --git a/src/Illuminate/Queue/SyncQueue.php b/src/Illuminate/Queue/SyncQueue.php index 900324d84c3c..b2fff207eb15 100755 --- a/src/Illuminate/Queue/SyncQueue.php +++ b/src/Illuminate/Queue/SyncQueue.php @@ -2,6 +2,7 @@ namespace Illuminate\Queue; +use Illuminate\Bus\DebounceLock; use Illuminate\Bus\UniqueLock; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\Job; @@ -137,6 +138,14 @@ function () use ($job) { ); } + if (! empty($job->debounceOwner ?? '')) { + $this->container->make('db.transactions')->addCallbackForRollback( + function () use ($job) { + (new DebounceLock($this->container->make(Cache::class)))->release($job, $job->debounceOwner ?? ''); + } + ); + } + return $this->container->make('db.transactions')->addCallback( fn () => $this->executeJob($job, $data, $queue) ); diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php new file mode 100644 index 000000000000..b3d380b7e969 --- /dev/null +++ b/tests/Integration/Queue/DebouncedJobTest.php @@ -0,0 +1,447 @@ +set('cache.default', 'database'); + } + + public function testDebouncedJobDispatchesAndExecutes() + { + $this->markTestSkippedWhenUsingQueueDrivers(['beanstalkd']); + + DebouncedTestJob::$handled = false; + + dispatch(new DebouncedTestJob('entity-1')); + $this->travelTo(now()->addSeconds(31)); + $this->runQueueWorkerCommand(['--once' => true]); + + $this->assertTrue(DebouncedTestJob::$handled); + } + + public function testSupersededDebouncedJobIsSkipped() + { + $this->markTestSkippedWhenUsingQueueDrivers(['sync', 'beanstalkd']); + + DebouncedTestJob::$handleCount = 0; + + // Dispatch two jobs with the same debounce identity. + // The second dispatch supersedes the first. + dispatch(new DebouncedTestJob('entity-1')); + dispatch(new DebouncedTestJob('entity-1')); + + // Advance time past the debounce window so jobs become available. + $this->travelTo(now()->addSeconds(31)); + + // Process both jobs from the queue. + $this->runQueueWorkerCommand(['--once' => true], 2); + + // Only the second (latest) dispatch should have executed. + $this->assertEquals(1, DebouncedTestJob::$handleCount); + } + + public function testTokenPersistsAfterSuccessfulExecution() + { + $this->markTestSkippedWhenUsingQueueDrivers(['beanstalkd']); + + DebouncedTestJob::$handled = false; + + dispatch($job = new DebouncedTestJob('entity-1')); + $this->travelTo(now()->addSeconds(31)); + $this->runQueueWorkerCommand(['--once' => true]); + + $this->assertTrue($job::$handled); + + // Debounce token persists after execution (cleaned up by GC TTL) + // to prevent a race where a superseded job sees an empty cache + // and executes via fail-open. + $this->assertNotNull( + $this->app->get(Cache::class)->get(DebounceLock::getKey($job)) + ); + } + + public function testFailedDebouncedJobStillCallsHandler() + { + DebouncedTestFailJob::$handled = false; + + $this->expectException(Exception::class); + + try { + dispatch_sync(new DebouncedTestFailJob('entity-1')); + } finally { + $this->assertTrue(DebouncedTestFailJob::$handled); + } + } + + public function testJobDebouncedEventFiresForSupersededJob() + { + $this->markTestSkippedWhenUsingQueueDrivers(['sync', 'beanstalkd']); + + $firedCount = 0; + + Event::listen(JobDebounced::class, function () use (&$firedCount) { + $firedCount++; + }); + + dispatch(new DebouncedTestJob('entity-1')); + dispatch(new DebouncedTestJob('entity-1')); + + $this->travelTo(now()->addSeconds(31)); + $this->runQueueWorkerCommand(['--once' => true], 2); + + $this->assertEquals(1, $firedCount); + } + + public function testDebouncedAndUniqueThrowsLogicException() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('debounced job cannot also implement ShouldBeUnique'); + + DebouncedAndUniqueTestJob::dispatch('entity-1'); + } + + public function testDebounceOwnerSurvivesSerialization() + { + $job = new DebouncedTestJob('entity-1'); + $job->debounceOwner = 'test-owner-token-123'; + + $restored = unserialize(serialize($job)); + + $this->assertEquals('test-owner-token-123', $restored->debounceOwner); + } + + public function testDifferentDebounceIdsDoNotInterfere() + { + $this->markTestSkippedWhenUsingQueueDrivers(['sync', 'beanstalkd']); + + DebouncedTestJob::$handleCount = 0; + + dispatch(new DebouncedTestJob('entity-1')); + dispatch(new DebouncedTestJob('entity-2')); + + $this->travelTo(now()->addSeconds(31)); + $this->runQueueWorkerCommand(['--once' => true], 2); + + // Both should execute — different identities. + $this->assertEquals(2, DebouncedTestJob::$handleCount); + } + + public function testDebounceLockKeyFormat() + { + $job = new DebouncedTestJob('entity-1'); + + $key = DebounceLock::getKey($job); + + $this->assertStringStartsWith('laravel_debounced_job:', $key); + $this->assertStringEndsWith(':entity-1', $key); + } + + public function testQueueFakeCapturesDebouncedJob() + { + Queue::fake(); + + DebouncedTestJob::dispatch('entity-1'); + + Queue::assertPushed(DebouncedTestJob::class); + } + + public function testJobExecutesWhenCacheTokenIsEvicted() + { + $this->markTestSkippedWhenUsingQueueDrivers(['beanstalkd']); + + DebouncedTestJob::$handled = false; + + dispatch($job = new DebouncedTestJob('entity-1')); + + // Simulate cache eviction by manually removing the debounce token. + $this->app->get(Cache::class)->forget(DebounceLock::getKey($job)); + + $this->travelTo(now()->addSeconds(31)); + $this->runQueueWorkerCommand(['--once' => true]); + + // Job should execute (fail-open) even though token was evicted. + $this->assertTrue(DebouncedTestJob::$handled); + } + + public function testOwnerAwareReleaseDoesNotWipeNewerLock() + { + $cache = $this->app->get(Cache::class); + $lock = new DebounceLock($cache); + + $jobA = new DebouncedTestJob('entity-1'); + $jobB = new DebouncedTestJob('entity-1'); + + $ownerA = $lock->acquire($jobA)['owner']; + $ownerB = $lock->acquire($jobB)['owner']; + + // Releasing with A's owner should not wipe B's token. + $lock->release($jobA, $ownerA); + + // B should still be the current owner. + $this->assertTrue($lock->isCurrentOwner($jobB, $ownerB)); + } + + public function testReleaseClearsMaxWaitTimestamp() + { + $cache = $this->app->get(Cache::class); + $lock = new DebounceLock($cache); + $job = new DebouncedWithMaxWaitJob('entity-1'); + + $first = $lock->acquire($job); + + $this->assertFalse($first['maxWaitExceeded']); + + // Simulate rollback cleanup. + $lock->release($job, $first['owner']); + + $this->assertNull($cache->get(DebounceLock::getKey($job).':first_dispatched_at')); + + // If timestamp cleanup worked, max wait should not appear exceeded. + $this->travelTo(now()->addSeconds(61)); + + $second = $lock->acquire($job); + + $this->assertFalse($second['maxWaitExceeded']); + } + + public function testSupersededDebouncedJobDoesNotDispatchChain() + { + $this->markTestSkippedWhenUsingQueueDrivers(['sync', 'beanstalkd']); + + DebouncedTestJob::$handleCount = 0; + ChainReceiverJob::$handled = false; + + // First dispatch with a chain — will be superseded. + dispatch(new DebouncedTestJob('entity-1'))->chain([new ChainReceiverJob]); + + // Second dispatch supersedes the first (no chain). + dispatch(new DebouncedTestJob('entity-1')); + + $this->travelTo(now()->addSeconds(31)); + $this->runQueueWorkerCommand(['--once' => true], 3); + + // Only the second dispatch should have executed. + $this->assertEquals(1, DebouncedTestJob::$handleCount); + + // Chain from superseded job should NOT have been dispatched. + $this->assertFalse(ChainReceiverJob::$handled); + } + + public function testDebounceViaUsesCustomCacheStore() + { + $this->markTestSkippedWhenUsingQueueDrivers(['beanstalkd']); + + DebouncedWithCustomCacheJob::$handled = false; + + dispatch(new DebouncedWithCustomCacheJob('entity-1')); + + $key = DebounceLock::getKey(new DebouncedWithCustomCacheJob('entity-1')); + + // Token should exist in the custom 'array' store. + $this->assertNotNull(CacheFacade::store('array')->get($key)); + + // Token should NOT exist in the default 'database' store. + $this->assertNull(CacheFacade::store('database')->get($key)); + } + + public function testMaxDebounceWaitForcesImmediateExecution() + { + $this->markTestSkippedWhenUsingQueueDrivers(['beanstalkd']); + + DebouncedWithMaxWaitJob::$handleCount = 0; + + // First dispatch at t=0. + dispatch(new DebouncedWithMaxWaitJob('entity-1')); + + // Second dispatch at t=50 (within maxWait of 60s). + $this->travelTo(now()->addSeconds(50)); + dispatch(new DebouncedWithMaxWaitJob('entity-1')); + + // Third dispatch at t=61 — exceeds maxWait. + $this->travelTo(now()->addSeconds(11)); + $job = new DebouncedWithMaxWaitJob('entity-1'); + $pending = dispatch($job); + unset($pending); + + // The job should be queued with delay=0 since max wait was exceeded. + $this->assertEquals(0, $job->delay); + } + + public function testDebounceWithoutMaxWaitAllowsIndefiniteDelay() + { + $this->markTestSkippedWhenUsingQueueDrivers(['beanstalkd']); + + // Regular debounced job (no maxWait) — delay should always be the debounce value. + $job1 = new DebouncedTestJob('entity-1'); + $pending = dispatch($job1); + unset($pending); + + $this->assertEquals(30, $job1->delay); + + // Dispatch again much later — still gets the full delay. + $this->travelTo(now()->addSeconds(600)); + $job2 = new DebouncedTestJob('entity-1'); + $pending2 = dispatch($job2); + unset($pending2); + + $this->assertEquals(30, $job2->delay); + } + +} + +#[DebounceFor(30)] +class DebouncedTestJob implements ShouldQueue +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public static $handled = false; + + public static $handleCount = 0; + + public function __construct(public string $entityId) + { + } + + public function debounceId(): string + { + return $this->entityId; + } + + public function handle() + { + static::$handled = true; + static::$handleCount++; + } +} + +#[DebounceFor(30)] +class DebouncedTestFailJob implements ShouldQueue +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public $tries = 1; + + public static $handled = false; + + public function __construct(public string $entityId) + { + } + + public function debounceId(): string + { + return $this->entityId; + } + + public function handle() + { + static::$handled = true; + + throw new Exception; + } +} + +#[DebounceFor(30)] +class DebouncedAndUniqueTestJob implements ShouldQueue, ShouldBeUnique +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public function __construct(public string $entityId) + { + } + + public function debounceId(): string + { + return $this->entityId; + } + + public function handle() + { + } +} + +class ChainReceiverJob implements ShouldQueue +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + } +} + +#[DebounceFor(30)] +class DebouncedWithCustomCacheJob implements ShouldQueue +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public static $handled = false; + + public function __construct(public string $entityId) + { + } + + public function debounceId(): string + { + return $this->entityId; + } + + public function debounceVia(): \Illuminate\Contracts\Cache\Repository + { + return \Illuminate\Container\Container::getInstance() + ->make(\Illuminate\Contracts\Cache\Factory::class) + ->store('array'); + } + + public function handle() + { + static::$handled = true; + } +} + +#[DebounceFor(30, maxWait: 60)] +class DebouncedWithMaxWaitJob implements ShouldQueue +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public static $handleCount = 0; + + public function __construct(public string $entityId) + { + } + + public function debounceId(): string + { + return $this->entityId; + } + + public function handle() + { + static::$handleCount++; + } +} From e47ad2ca758a0d60207f25dc958deb28304a6bdd Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 15 Apr 2026 14:50:23 +0000 Subject: [PATCH 169/596] Apply fixes from StyleCI --- src/Illuminate/Bus/DebounceLock.php | 2 +- tests/Integration/Queue/DebouncedJobTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Illuminate/Bus/DebounceLock.php b/src/Illuminate/Bus/DebounceLock.php index ed32aa9eb162..31489d0bb6cb 100644 --- a/src/Illuminate/Bus/DebounceLock.php +++ b/src/Illuminate/Bus/DebounceLock.php @@ -52,7 +52,7 @@ public function acquire($job, $debounceFor = null, $maxWait = null) 'owner' => $owner, 'maxWaitExceeded' => $this->maxWaitExceeded( $cache, $key, $ttl, $maxWait ?? $this->getMaxDebounceWait($job) - ) + ), ]; } diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php index b3d380b7e969..0c36149613c3 100644 --- a/tests/Integration/Queue/DebouncedJobTest.php +++ b/tests/Integration/Queue/DebouncedJobTest.php @@ -310,7 +310,6 @@ public function testDebounceWithoutMaxWaitAllowsIndefiniteDelay() $this->assertEquals(30, $job2->delay); } - } #[DebounceFor(30)] From 839b70d6c858cb63a656d74e000a82915fb79091 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 15 Apr 2026 10:21:18 -0500 Subject: [PATCH 170/596] check for managed queues --- src/Illuminate/Foundation/Cloud.php | 12 ++++++++++++ tests/Integration/Foundation/CloudTest.php | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index bdc1afc38ded..31a51c03cdd0 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -5,6 +5,7 @@ use Illuminate\Database\Migrations\Migrator; use Illuminate\Foundation\Bootstrap\HandleExceptions; use Illuminate\Foundation\Bootstrap\LoadConfiguration; +use Illuminate\Queue\Worker; use Monolog\Formatter\JsonFormatter; use Monolog\Handler\SocketHandler; use PDO; @@ -29,6 +30,7 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst static::configureDisks($app); static::configureUnpooledPostgresConnection($app); static::ensureMigrationsUseUnpooledConnection($app); + static::configureManagedQueues(); }, HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); @@ -112,6 +114,16 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): }); } + /** + * Configure managed queues if applicable. + */ + public static function configureManagedQueues(): void + { + if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { + Worker::$restartable = false; + } + } + /** * Configure the Laravel Cloud log channels. */ diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index a20f64a5aa81..ad6e35b55752 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Foundation; use Illuminate\Foundation\Cloud; +use Illuminate\Queue\Worker; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; @@ -53,6 +54,21 @@ public function test_it_can_configure_disks() unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); } + public function test_it_disables_queue_restart_polling_for_managed_queues() + { + Worker::$restartable = true; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + try { + Cloud::configureManagedQueues(); + + $this->assertFalse(Worker::$restartable); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + Worker::$restartable = true; + } + } + public function test_it_respects_log_levels() { if (isset($_SERVER['LOG_LEVEL'])) { From e284bb8a97d3adc28922c418938eab979811aa13 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 15 Apr 2026 10:24:19 -0500 Subject: [PATCH 171/596] backport --- src/Illuminate/Foundation/Cloud.php | 12 ++++++++++++ tests/Integration/Foundation/CloudTest.php | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 63cfe86a07c8..07a7317c9f85 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -5,6 +5,7 @@ use Illuminate\Database\Migrations\Migrator; use Illuminate\Foundation\Bootstrap\HandleExceptions; use Illuminate\Foundation\Bootstrap\LoadConfiguration; +use Illuminate\Queue\Worker; use Monolog\Formatter\JsonFormatter; use Monolog\Handler\SocketHandler; use PDO; @@ -29,6 +30,7 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst static::configureDisks($app); static::configureUnpooledPostgresConnection($app); static::ensureMigrationsUseUnpooledConnection($app); + static::configureManagedQueues(); }, HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); @@ -112,6 +114,16 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): }); } + /** + * Configure managed queues if applicable. + */ + public static function configureManagedQueues(): void + { + if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { + Worker::$restartable = false; + } + } + /** * Configure the Laravel Cloud log channels. */ diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index a20f64a5aa81..ad6e35b55752 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Foundation; use Illuminate\Foundation\Cloud; +use Illuminate\Queue\Worker; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; @@ -53,6 +54,21 @@ public function test_it_can_configure_disks() unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); } + public function test_it_disables_queue_restart_polling_for_managed_queues() + { + Worker::$restartable = true; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + try { + Cloud::configureManagedQueues(); + + $this->assertFalse(Worker::$restartable); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + Worker::$restartable = true; + } + } + public function test_it_respects_log_levels() { if (isset($_SERVER['LOG_LEVEL'])) { From 59e374600cc952eb8bcf8410aff87e5a104b6a82 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Wed, 15 Apr 2026 22:07:35 +0100 Subject: [PATCH 172/596] [13.x] Support JSON responses for the built-in health route (#59710) * Support JSON responses for the built-in health route * Update ApplicationBuilder.php * Update RouteServiceProviderHealthTest.php --------- Co-authored-by: Taylor Otwell --- .../Configuration/ApplicationBuilder.php | 13 ++++++- .../RouteServiceProviderHealthTest.php | 37 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index c4af2e6bfc30..b67b52f72878 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -13,6 +13,7 @@ use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; use Illuminate\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider; +use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Event; @@ -218,7 +219,7 @@ protected function buildRoutingCallback(array|string|null $web, } if (is_string($health)) { - Route::get($health, function () { + Route::get($health, function (Request $request) { $exception = null; try { @@ -233,9 +234,17 @@ protected function buildRoutingCallback(array|string|null $web, $exception = $e->getMessage(); } + $status = $exception ? 500 : 200; + + if ($request->expectsJson()) { + return response()->json([ + 'status' => $exception ? 'down' : 'up', + ], $status); + } + return response(View::file(__DIR__.'/../resources/health-up.blade.php', [ 'exception' => $exception, - ]), status: $exception ? 500 : 200); + ]), status: $status); }); } diff --git a/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php b/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php index 6a331999d88f..ee331e3ab4a3 100644 --- a/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php +++ b/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php @@ -3,10 +3,14 @@ namespace Illuminate\Tests\Integration\Foundation\Support\Providers; use Illuminate\Foundation\Application; +use Illuminate\Foundation\Events\DiagnosingHealth; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +use RuntimeException; +#[WithConfig('app.debug', false)] #[WithConfig('filesystems.disks.local.serve', false)] class RouteServiceProviderHealthTest extends TestCase { @@ -31,6 +35,37 @@ protected function defineEnvironment($app) public function test_it_can_load_health_page() { - $this->get('/up')->assertOk(); + $this->get('/up') + ->assertOk() + ->assertSee('Application up'); + } + + public function test_it_returns_json_when_request_expects_json() + { + $this->getJson('/up') + ->assertOk() + ->assertExactJson(['status' => 'up']); + } + + public function test_it_returns_json_failure_status_when_diagnosis_reports_a_problem() + { + Event::listen(DiagnosingHealth::class, function () { + throw new RuntimeException('Database connection refused.'); + }); + + $this->getJson('/up') + ->assertStatus(500) + ->assertExactJson(['status' => 'down']); + } + + public function test_it_renders_html_failure_page_when_diagnosis_reports_a_problem() + { + Event::listen(DiagnosingHealth::class, function () { + throw new RuntimeException('Database connection refused.'); + }); + + $this->get('/up') + ->assertStatus(500) + ->assertSee('experiencing problems'); } } From 0fa420f961b890af0c1dae46c0cd63a83864384c Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 15 Apr 2026 22:14:58 +0100 Subject: [PATCH 173/596] wip (#59711) --- src/Illuminate/Queue/QueueRoutes.php | 2 +- tests/Queue/QueueRoutesTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/QueueRoutes.php b/src/Illuminate/Queue/QueueRoutes.php index ae6eb9fd0117..8c73fed0f8e5 100644 --- a/src/Illuminate/Queue/QueueRoutes.php +++ b/src/Illuminate/Queue/QueueRoutes.php @@ -26,7 +26,7 @@ public function getConnection($queueable) } return is_string($route) - ? $route + ? null : $route[0]; } diff --git a/tests/Queue/QueueRoutesTest.php b/tests/Queue/QueueRoutesTest.php index 46de4cdf8f61..30aeda1a8fb4 100644 --- a/tests/Queue/QueueRoutesTest.php +++ b/tests/Queue/QueueRoutesTest.php @@ -68,6 +68,16 @@ public function testGetConnection() $this->assertSame('job-connection', $defaults->getConnection(new SomeJob)); $this->assertNull($defaults->getConnection(new Payment)); } + + public function testStringRouteDefaultsToQueueNotConnection() + { + $defaults = new QueueRoutes(); + + $defaults->set([BaseNotification::class => 'notifications']); + + $this->assertSame('notifications', $defaults->getQueue(new FinanceNotification)); + $this->assertNull($defaults->getConnection(new FinanceNotification)); + } } trait CustomTrait From 80a472e1d0ef2c496a77d252b2cc4be721a6c886 Mon Sep 17 00:00:00 2001 From: "Cy(rod) John" Date: Thu, 16 Apr 2026 21:41:32 +0800 Subject: [PATCH 174/596] [13.x] Fix failOnUnknownFields query parameter handling (#59728) * fix(http): ignore query params in strict requests Allow *_confirmation only when the base field uses confirmed. Refs #59694 * formatting --------- Co-authored-by: Taylor Otwell --- .../Foundation/Http/FormRequest.php | 10 +- .../Foundation/FoundationFormRequestTest.php | 169 ++++++++++++++++-- 2 files changed, 163 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 72faf5b0e2ab..3cde9bfe0a4b 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -16,6 +16,7 @@ use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use Illuminate\Support\Arr; +use Illuminate\Validation\ValidationRuleParser; use Illuminate\Validation\ValidatesWhenResolvedTrait; use ReflectionClass; @@ -231,7 +232,9 @@ protected function validateNoUnknownFields(Validator $validator): void { $allowedKeys = array_keys($this->validationRules()); - foreach (array_keys(Arr::dot($this->all())) as $inputKey) { + $input = $this->isJson() ? $this->json()->all() : $this->request->all(); + + foreach (array_keys(Arr::dot($input)) as $inputKey) { if (! $this->isKnownField($inputKey, $allowedKeys)) { $validator->errors()->add($inputKey, trans('validation.prohibited', [ 'attribute' => str_replace('_', ' ', $inputKey), @@ -254,6 +257,11 @@ protected function isKnownField(string $inputKey, array $allowedKeys): bool return true; } + if (str_ends_with($inputKey, '_confirmation') && + $ruleKey === substr($inputKey, 0, -13)) { + return true; + } + if (str_contains($ruleKey, '*')) { $pattern = '/^'.str_replace('\*', '[^.]+', preg_quote($ruleKey, '/')).'$/'; diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index 43d4251ce866..e96de23a5200 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -252,7 +252,8 @@ public function testFailOnUnknownFieldsRejectsExtraInputWhenEnabledOnRequest() { $request = $this->createRequest( ['name' => 'Taylor', 'unexpected' => 'value'], - FoundationTestFormRequestFailOnUnknownFieldsStub::class + FoundationTestFormRequestFailOnUnknownFieldsStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -266,7 +267,8 @@ public function testFailOnUnknownFieldsAllowsExtraInputWhenExplicitlyDisabledOnR { $request = $this->createRequest( ['name' => 'Taylor', 'with' => 'extras'], - FoundationTestFormRequestSkipUnknownFieldsFailureStub::class + FoundationTestFormRequestSkipUnknownFieldsFailureStub::class, + 'POST' ); $request->validateResolved(); @@ -280,7 +282,8 @@ public function testFailOnUnknownFieldsEnabledViaFailOnUnknownFieldsStaticMethod $request = $this->createRequest( ['name' => 'Taylor', 'unexpected' => 'value'], - FoundationTestFormRequestStub::class + FoundationTestFormRequestStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -296,7 +299,8 @@ public function testFailOnUnknownFieldsWorksWhenRequestDoesNotDefineRulesMethod( $request = $this->createRequest( ['unexpected' => 'value'], - FoundationTestFormRequestWithoutRulesMethod::class + FoundationTestFormRequestWithoutRulesMethod::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -312,7 +316,8 @@ public function testFailOnUnknownFieldsAttributeOverridesGlobalStatic() $request = $this->createRequest( ['name' => 'Taylor', 'with' => 'extras'], - FoundationTestFormRequestSkipUnknownFieldsFailureStub::class + FoundationTestFormRequestSkipUnknownFieldsFailureStub::class, + 'POST' ); $request->validateResolved(); @@ -329,7 +334,8 @@ public function testFailOnUnknownFieldsAllowsKeysMatchingWildcardRules() ['id' => 2, 'name' => 'b'], ], ], - FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class + FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -348,7 +354,8 @@ public function testFailOnUnknownFieldsPassesForInputMatchingWildcardRulesOnly() ['id' => 2], ], ], - FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class + FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class, + 'POST' ); $request->validateResolved(); @@ -372,7 +379,8 @@ public function testFailOnUnknownFieldsWildcardMatchesSingleSegmentOnly() ['name' => 'a'], ], ], - FoundationTestFormRequestFailOnUnknownFieldsSingleSegmentWildcardStub::class + FoundationTestFormRequestFailOnUnknownFieldsSingleSegmentWildcardStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -390,7 +398,8 @@ public function testFailOnUnknownFieldsRejectsMultipleUnknownKeys() 'role' => 'admin', 'profile' => ['is_admin' => true], ], - FoundationTestFormRequestFailOnUnknownFieldsStub::class + FoundationTestFormRequestFailOnUnknownFieldsStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -405,7 +414,8 @@ public function testFailOnUnknownFieldsRejectsUnknownNestedSibling() { $request = $this->createRequest( ['user' => ['name' => 'Taylor', 'role' => 'admin']], - FoundationTestFormRequestFailOnUnknownFieldsNestedStub::class + FoundationTestFormRequestFailOnUnknownFieldsNestedStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -419,7 +429,8 @@ public function testFailOnUnknownFieldsUsesPreparedInput() { $request = $this->createRequest( ['full_name' => 'Taylor'], - FoundationTestFormRequestFailOnUnknownFieldsPrepareForValidationStub::class + FoundationTestFormRequestFailOnUnknownFieldsPrepareForValidationStub::class, + 'POST' ); $request->validateResolved(); @@ -431,7 +442,8 @@ public function testFailOnUnknownFieldsChecksRequestPayloadWhenValidationDataIsO { $request = $this->createRequest( ['name' => 'Taylor', 'unexpected' => 'value'], - FoundationTestFormRequestFailOnUnknownFieldsValidationDataOverrideStub::class + FoundationTestFormRequestFailOnUnknownFieldsValidationDataOverrideStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -445,7 +457,8 @@ public function testFailOnUnknownFieldsStillRunsWithStopOnFirstFailureAttribute( { $request = $this->createRequest( ['unexpected' => 'value'], - FoundationTestFormRequestFailOnUnknownFieldsStopOnFirstFailureStub::class + FoundationTestFormRequestFailOnUnknownFieldsStopOnFirstFailureStub::class, + 'POST' ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -455,6 +468,106 @@ public function testFailOnUnknownFieldsStillRunsWithStopOnFirstFailureAttribute( $this->assertTrue($exception->validator->errors()->has('unexpected')); } + public function testFailOnUnknownFieldsIgnoresQueryParametersOnGetRequests() + { + FormRequest::failOnUnknownFields(); + + $container = tap(new Container, function ($container) { + $container->instance( + ValidationFactoryContract::class, + $this->createValidationFactory($container) + ); + + $container->instance('translator', new TranslatorConcrete(new ArrayLoader([ + 'validation' => [ + 'prohibited' => 'The :attribute field is prohibited.', + ], + ]), 'en')); + }); + + Container::setInstance($container); + + $request = FoundationTestFormRequestWithoutRulesMethod::create( + '/?page=1&perPage=5&expires=1234567890&signature=abc123', + 'GET' + ); + + $request->setRedirector($this->createMockRedirector($request)) + ->setContainer($container); + + $request->validateResolved(); + + $this->assertSame([], $request->validated()); + } + + public function testFailOnUnknownFieldsAllowsConfirmationFieldsWhenBaseFieldIsConfirmed() + { + FormRequest::failOnUnknownFields(); + + $container = tap(new Container, function ($container) { + $container->instance( + ValidationFactoryContract::class, + $this->createValidationFactory($container) + ); + + $container->instance('translator', new TranslatorConcrete(new ArrayLoader([ + 'validation' => [ + 'prohibited' => 'The :attribute field is prohibited.', + ], + ]), 'en')); + }); + + Container::setInstance($container); + + $request = FoundationTestFormRequestConfirmedFieldStub::create( + '/', + 'POST', + ['password' => 'secret123', 'password_confirmation' => 'secret123'] + ); + + $request->setRedirector($this->createMockRedirector($request)) + ->setContainer($container); + + $request->validateResolved(); + + $this->assertEquals(['password' => 'secret123'], $request->validated()); + } + + // public function testFailOnUnknownFieldsRejectsConfirmationFieldsWithoutConfirmedRule() + // { + // FormRequest::failOnUnknownFields(); + + // $container = tap(new Container, function ($container) { + // $container->instance( + // ValidationFactoryContract::class, + // $this->createValidationFactory($container) + // ); + + // $container->instance('translator', new TranslatorConcrete(new ArrayLoader([ + // 'validation' => [ + // 'prohibited' => 'The :attribute field is prohibited.', + // ], + // ]), 'en')); + // }); + + // Container::setInstance($container); + + // $request = FoundationTestFormRequestUnconfirmedFieldStub::create( + // '/', + // 'POST', + // ['password' => 'secret123', 'password_confirmation' => 'secret123'] + // ); + + // $request->setRedirector($this->createMockRedirector($request)) + // ->setContainer($container); + + // $exception = $this->catchException(ValidationException::class, function () use ($request) { + // $request->validateResolved(); + // }); + + // $this->assertTrue($exception->validator->errors()->has('password_confirmation')); + // } + /** * Catch the given exception thrown from the executor, and return it. * @@ -486,7 +599,7 @@ protected function catchException($class, $executor) * @param string $class * @return \Illuminate\Foundation\Http\FormRequest */ - protected function createRequest($payload = [], $class = FoundationTestFormRequestStub::class) + protected function createRequest($payload = [], $class = FoundationTestFormRequestStub::class, $method = 'GET') { $container = tap(new Container, function ($container) { $container->instance( @@ -503,7 +616,7 @@ protected function createRequest($payload = [], $class = FoundationTestFormReque Container::setInstance($container); - $request = $class::create('/', 'GET', $payload); + $request = $class::create('/', $method, $payload); return $request->setRedirector($this->createMockRedirector($request)) ->setContainer($container); @@ -888,3 +1001,29 @@ public function authorize() return true; } } + +class FoundationTestFormRequestConfirmedFieldStub extends FormRequest +{ + public function rules() + { + return ['password' => 'required|confirmed']; + } + + public function authorize() + { + return true; + } +} + +class FoundationTestFormRequestUnconfirmedFieldStub extends FormRequest +{ + public function rules() + { + return ['password' => 'required']; + } + + public function authorize() + { + return true; + } +} From a3f860e48bbac2eededfaf7561b9888d3a761935 Mon Sep 17 00:00:00 2001 From: Bipin Kareparambil Date: Thu, 16 Apr 2026 17:41:52 +0400 Subject: [PATCH 175/596] [13.x] Fix flaky QueueWorkerTest by freezing time before computing retryUntil (#59727) --- tests/Queue/QueueWorkerTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index 9c666ed25e2a..f66766cdf2b7 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -241,6 +241,8 @@ public function testJobIsNotReleasedIfItHasExceededMaxAttempts() public function testJobIsNotReleasedIfItHasExpired() { + Carbon::setTestNow($now = Carbon::create(2026, 1, 1, 0, 0, 0)); + $e = new RuntimeException; $job = new WorkerFakeJob(function ($job) use ($e) { @@ -250,13 +252,11 @@ public function testJobIsNotReleasedIfItHasExpired() throw $e; }); - $job->retryUntil = Carbon::now()->addSeconds(1)->getTimestamp(); + $job->retryUntil = $now->copy()->addSecond()->getTimestamp(); $job->attempts = 0; - Carbon::setTestNow( - Carbon::now()->addSeconds(1) - ); + Carbon::setTestNow($now->copy()->addSecond()); $worker = $this->getWorker('default', ['queue' => [$job]]); $worker->runNextJob('default', 'queue', $this->workerOptions()); From 7670e028be90cda190f6f50ffb2b48986842db26 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Apr 2026 13:42:09 +0000 Subject: [PATCH 176/596] Apply fixes from StyleCI --- src/Illuminate/Foundation/Http/FormRequest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 3cde9bfe0a4b..b95e81be6777 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -16,7 +16,6 @@ use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use Illuminate\Support\Arr; -use Illuminate\Validation\ValidationRuleParser; use Illuminate\Validation\ValidatesWhenResolvedTrait; use ReflectionClass; From 44c74ebb597825d56ae3a1a249697e913e2b15c2 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 16 Apr 2026 14:43:54 +0100 Subject: [PATCH 177/596] [13.x] Allow array of pivot arrays to be passed to hasAttached (#59723) * 13.x-allow-has-attached-to-pass-arrays improve * Update Factory.php --------- Co-authored-by: Taylor Otwell --- .../Database/Eloquent/Factories/Factory.php | 8 ++++++++ tests/Database/DatabaseEloquentFactoryTest.php | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 603b23397a5e..a51e8e01d748 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -724,6 +724,14 @@ protected function guessRelationship(string $related) */ public function hasAttached($factory, $pivot = [], $relationship = null) { + if (is_array($pivot) && count($pivot) > 0 && array_all($pivot, fn ($p) => is_array($p))) { + $factory = $factory instanceof Factory && $factory->count === null + ? $factory->count(count($pivot)) + : $factory; + + $pivot = new Sequence(...$pivot); + } + return $this->newInstance([ 'has' => $this->has->concat([new BelongsToManyRelationship( $factory, diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index b995a58868d5..d987b8faa813 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -556,6 +556,17 @@ public function test_belongs_to_many_relationship_with_existing_model_instances_ unset($_SERVER['__test.role.creating-role']); } + public function test_belongs_to_many_relationship_with_pivot_arrays() + { + $user = FactoryTestUserFactory::new() + ->hasAttached(FactoryTestRoleFactory::new(), [['admin' => 'Y'], ['admin' => 'N']]) + ->create(); + + $this->assertCount(2, $user->factoryTestRoles); + $this->assertSame('Y', $user->factoryTestRoles[0]->pivot->admin); + $this->assertSame('N', $user->factoryTestRoles[1]->pivot->admin); + } + public function test_sequences() { $users = FactoryTestUserFactory::times(2)->sequence( From 9ce4b638e84d22eaacd1deabf748bb26c13daeea Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 16 Apr 2026 13:44:12 +0000 Subject: [PATCH 178/596] Apply fixes from StyleCI --- src/Illuminate/Database/Eloquent/Factories/Factory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index a51e8e01d748..4d2278d6af3a 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -725,8 +725,8 @@ protected function guessRelationship(string $related) public function hasAttached($factory, $pivot = [], $relationship = null) { if (is_array($pivot) && count($pivot) > 0 && array_all($pivot, fn ($p) => is_array($p))) { - $factory = $factory instanceof Factory && $factory->count === null - ? $factory->count(count($pivot)) + $factory = $factory instanceof Factory && $factory->count === null + ? $factory->count(count($pivot)) : $factory; $pivot = new Sequence(...$pivot); From 7f3aa1ad2348a210df347997075b86dfe6a55c3c Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:56:38 +0600 Subject: [PATCH 179/596] [13.x] Fix TypeError in digits_between validation rule on non-string values (#59717) --- .../Validation/Concerns/ValidatesAttributes.php | 4 ++++ tests/Validation/ValidationValidatorTest.php | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 35492c65b6e6..e80392d5712d 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -741,6 +741,10 @@ public function validateDigitsBetween($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'digits_between'); + if (! is_string($value) && ! is_numeric($value)) { + return false; + } + $length = strlen((string) $value); return ! preg_match('/[^0-9]/', $value) diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a862b522f24d..eb8450c26b5a 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -3265,6 +3265,13 @@ public function testValidateMinDigitsDoesNotThrowOnNonStringValue() $this->assertFalse($v->passes()); } + public function testValidateDigitsBetweenDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array']], ['x' => 'digits_between:1,10']); + $this->assertFalse($v->passes()); + } + public function testValidateDoesntStartWith() { $trans = $this->getIlluminateArrayTranslator(); @@ -3764,6 +3771,9 @@ public function testValidateDigits() $v = new Validator($trans, ['foo' => '+12.3'], ['foo' => 'digits_between:1,6']); $this->assertFalse($v->passes()); + $v = new Validator($trans, ['foo' => ['12345']], ['foo' => 'digits_between:1,6']); + $this->assertFalse($v->passes()); + $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['foo' => '12345'], ['foo' => 'min_digits:1']); $this->assertTrue($v->passes()); From 4e17d1c2a7147f63b2fcfd2f538a31419cd1276a Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:57:03 +0600 Subject: [PATCH 180/596] [13.x] Add enum support to PasswordBrokerManager (#59714) --- .../Auth/Passwords/PasswordBrokerManager.php | 10 +-- tests/Auth/AuthPasswordBrokerManagerTest.php | 70 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 tests/Auth/AuthPasswordBrokerManagerTest.php diff --git a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php index 6e42bba190d8..6275933db4a0 100644 --- a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php +++ b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php @@ -5,6 +5,8 @@ use Illuminate\Contracts\Auth\PasswordBrokerFactory as FactoryContract; use InvalidArgumentException; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Contracts\Auth\PasswordBroker */ @@ -37,12 +39,12 @@ public function __construct($app) /** * Attempt to get the broker from the local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Auth\PasswordBroker */ public function broker($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name)); } @@ -132,12 +134,12 @@ public function getDefaultDriver() /** * Set the default password broker name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->app['config']['auth.defaults.passwords'] = $name; + $this->app['config']['auth.defaults.passwords'] = enum_value($name); } /** diff --git a/tests/Auth/AuthPasswordBrokerManagerTest.php b/tests/Auth/AuthPasswordBrokerManagerTest.php new file mode 100644 index 000000000000..3eb8af30530c --- /dev/null +++ b/tests/Auth/AuthPasswordBrokerManagerTest.php @@ -0,0 +1,70 @@ +getApp(); + + $broker = m::mock(PasswordBroker::class); + + $manager = m::mock(PasswordBrokerManager::class, [$app])->makePartial()->shouldAllowMockingProtectedMethods(); + $manager->shouldReceive('resolve')->with('users')->andReturn($broker); + + $result1 = $manager->broker(PasswordBrokerName::Users); + $result2 = $manager->broker('users'); + + $this->assertSame($broker, $result1); + $this->assertSame($result1, $result2); + } + + public function testSetDefaultDriverAcceptsBackedEnum(): void + { + $app = $this->getApp(); + + $manager = new PasswordBrokerManager($app); + $manager->setDefaultDriver(PasswordBrokerName::Users); + + $this->assertSame('users', $app['config']['auth.defaults.passwords']); + } + + protected function getApp(): Container + { + $app = new Container; + + $app->singleton('config', fn () => new Config([ + 'auth' => [ + 'defaults' => ['passwords' => 'users'], + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_reset_tokens', + 'expire' => 60, + 'throttle' => 60, + ], + ], + ], + ])); + + return $app; + } +} + +enum PasswordBrokerName: string +{ + case Users = 'users'; +} From a5c4aac517edfd04e2f107fbaf9db49ad8e141e0 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:57:17 +0600 Subject: [PATCH 181/596] [13.x] Add enum support to BroadcastManager (#59713) --- .../Broadcasting/BroadcastManager.php | 18 +++-- .../Broadcasting/BroadcastManagerTest.php | 76 +++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index da90f13855b4..da6d7115d80e 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -30,6 +30,8 @@ use RuntimeException; use Throwable; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Contracts\Broadcasting\Broadcaster */ @@ -250,9 +252,9 @@ protected function mustBeUniqueAndCannotAcquireLock($event) } /** - * Get a driver instance. + * Get a broadcaster instance by name. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return mixed */ public function connection($name = null) @@ -263,12 +265,12 @@ public function connection($name = null) /** * Get a driver instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return mixed */ public function driver($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->drivers[$name] = $this->get($name); } @@ -473,23 +475,23 @@ public function getDefaultDriver() /** * Set the default driver name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->app['config']['broadcasting.default'] = $name; + $this->app['config']['broadcasting.default'] = enum_value($name); } /** * Disconnect the given driver / connection and remove it from local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return void */ public function purge($name = null) { - $name ??= $this->getDefaultDriver(); + $name = enum_value($name) ?? $this->getDefaultDriver(); unset($this->drivers[$name]); } diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index 6f33ac054fd9..048ca1a2dfe2 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -224,6 +224,77 @@ public function test_throw_exception_when_driver_creation_fails() } } + public function testBroadcastManagerCanResolveBackedEnumConnection(): void + { + $app = $this->getApp([ + 'broadcasting' => [ + 'connections' => [ + 'log' => ['driver' => 'log'], + ], + ], + ]); + + $driver = new stdClass; + $manager = new BroadcastManager($app); + $manager->extend('log', static fn () => $driver); + + $this->assertSame($driver, $manager->connection(BroadcastConnectionName::Log)); + $this->assertSame($manager->connection('log'), $manager->connection(BroadcastConnectionName::Log)); + } + + public function testBroadcastManagerCanResolveBackedEnumDriver(): void + { + $app = $this->getApp([ + 'broadcasting' => [ + 'connections' => [ + 'log' => ['driver' => 'log'], + ], + ], + ]); + + $driver = new stdClass; + $manager = new BroadcastManager($app); + $manager->extend('log', static fn () => $driver); + + $this->assertSame($driver, $manager->driver(BroadcastConnectionName::Log)); + $this->assertSame($manager->driver('log'), $manager->driver(BroadcastConnectionName::Log)); + } + + public function testSetDefaultDriverAcceptsBackedEnum(): void + { + $app = $this->getApp([ + 'broadcasting' => [ + 'default' => 'null', + 'connections' => [], + ], + ]); + + $manager = new BroadcastManager($app); + $manager->setDefaultDriver(BroadcastConnectionName::Log); + + $this->assertSame('log', $app['config']['broadcasting.default']); + } + + public function testPurgeAcceptsBackedEnum(): void + { + $app = $this->getApp([ + 'broadcasting' => [ + 'connections' => [ + 'log' => ['driver' => 'log'], + ], + ], + ]); + + $manager = new BroadcastManager($app); + $manager->extend('log', static fn () => new stdClass); + + $instance1 = $manager->connection(BroadcastConnectionName::Log); + $manager->purge(BroadcastConnectionName::Log); + $instance2 = $manager->connection(BroadcastConnectionName::Log); + + $this->assertNotSame($instance1, $instance2); + } + protected function getApp(array $userConfig) { $app = new Container; @@ -233,6 +304,11 @@ protected function getApp(array $userConfig) } } +enum BroadcastConnectionName: string +{ + case Log = 'log'; +} + class TestEvent implements ShouldBroadcast { /** From 9959a22f988a24b9b03948bc6a717046e87ddec0 Mon Sep 17 00:00:00 2001 From: Julien Date: Thu, 16 Apr 2026 15:57:43 +0200 Subject: [PATCH 182/596] Change attempts column type from tiny to small integer (#59718) Why are jobs restricted to "only" 255 attempts ? I reached the maximum on unique jobs that are repeatedly trying to reach an external service during one full day. SQLSTATE[22003]: Numeric value out of range: 1264 Out of range value for column 'attempts' at row 1 (Connection: mysql, Host: 127.0.0.1, Port: 3306, Database: my_db, SQL: update `jobs` set `reserved_at` = 1776316936, `attempts` = 256 where `id` = 10884941) --- src/Illuminate/Queue/Console/stubs/jobs.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Console/stubs/jobs.stub b/src/Illuminate/Queue/Console/stubs/jobs.stub index 3e3b587f9595..14350d292d5f 100644 --- a/src/Illuminate/Queue/Console/stubs/jobs.stub +++ b/src/Illuminate/Queue/Console/stubs/jobs.stub @@ -15,7 +15,7 @@ return new class extends Migration $table->bigIncrements('id'); $table->string('queue')->index(); $table->longText('payload'); - $table->unsignedTinyInteger('attempts'); + $table->unsignedSmallInteger('attempts'); $table->unsignedInteger('reserved_at')->nullable(); $table->unsignedInteger('available_at'); $table->unsignedInteger('created_at'); From 676aa065c31683bd6a3e8c7e855e03148ba58bfc Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:58:16 +0000 Subject: [PATCH 183/596] Update facade docblocks --- src/Illuminate/Support/Facades/Broadcast.php | 8 ++++---- src/Illuminate/Support/Facades/Password.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Support/Facades/Broadcast.php b/src/Illuminate/Support/Facades/Broadcast.php index 8de19f8b2e00..f40c8b36ba5e 100644 --- a/src/Illuminate/Support/Facades/Broadcast.php +++ b/src/Illuminate/Support/Facades/Broadcast.php @@ -14,13 +14,13 @@ * @method static \Illuminate\Broadcasting\AnonymousEvent presence(string $channel) * @method static \Illuminate\Broadcasting\PendingBroadcast event(mixed $event = null) * @method static void queue(mixed $event) - * @method static mixed connection(string|null $name = null) - * @method static mixed driver(string|null $name = null) + * @method static mixed connection(\UnitEnum|string|null $name = null) + * @method static mixed driver(\UnitEnum|string|null $name = null) * @method static \Pusher\Pusher pusher(array $config) * @method static \Ably\AblyRest ably(array $config) * @method static string getDefaultDriver() - * @method static void setDefaultDriver(string $name) - * @method static void purge(string|null $name = null) + * @method static void setDefaultDriver(\UnitEnum|string $name) + * @method static void purge(\UnitEnum|string|null $name = null) * @method static \Illuminate\Broadcasting\BroadcastManager extend(string $driver, \Closure $callback) * @method static \Illuminate\Contracts\Foundation\Application getApplication() * @method static \Illuminate\Broadcasting\BroadcastManager setApplication(\Illuminate\Contracts\Foundation\Application $app) diff --git a/src/Illuminate/Support/Facades/Password.php b/src/Illuminate/Support/Facades/Password.php index ac6f226aa251..9018c8b73bbc 100755 --- a/src/Illuminate/Support/Facades/Password.php +++ b/src/Illuminate/Support/Facades/Password.php @@ -5,9 +5,9 @@ use Illuminate\Contracts\Auth\PasswordBroker; /** - * @method static \Illuminate\Contracts\Auth\PasswordBroker broker(string|null $name = null) + * @method static \Illuminate\Contracts\Auth\PasswordBroker broker(\UnitEnum|string|null $name = null) * @method static string getDefaultDriver() - * @method static void setDefaultDriver(string $name) + * @method static void setDefaultDriver(\UnitEnum|string $name) * @method static string sendResetLink(array $credentials, \Closure|null $callback = null) * @method static mixed reset(array $credentials, \Closure $callback) * @method static \Illuminate\Contracts\Auth\CanResetPassword|null getUser(array $credentials) From 43bfa2ecf9298e0fdbb908e84e8de1630eba0071 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Thu, 16 Apr 2026 16:34:41 +0200 Subject: [PATCH 184/596] Get rid of useless Mockery::close (#59730) Co-authored-by: Lucas Michot --- tests/Auth/AuthPasswordBrokerManagerTest.php | 5 ----- tests/Queue/QueueRedisQueueTest.php | 6 ------ tests/Redis/ConcurrencyLimiterTest.php | 6 ------ 3 files changed, 17 deletions(-) diff --git a/tests/Auth/AuthPasswordBrokerManagerTest.php b/tests/Auth/AuthPasswordBrokerManagerTest.php index 3eb8af30530c..fbdbe0c00c34 100644 --- a/tests/Auth/AuthPasswordBrokerManagerTest.php +++ b/tests/Auth/AuthPasswordBrokerManagerTest.php @@ -11,11 +11,6 @@ class AuthPasswordBrokerManagerTest extends TestCase { - protected function tearDown(): void - { - m::close(); - } - public function testBrokerCanResolveBackedEnum(): void { $app = $this->getApp(); diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 4d2eb94239af..8511d5e793b7 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -16,12 +16,6 @@ class QueueRedisQueueTest extends TestCase { - protected function tearDown(): void - { - m::close(); - parent::tearDown(); - } - public function testPushProperlyPushesJobOntoRedis() { $uuid = Str::uuid(); diff --git a/tests/Redis/ConcurrencyLimiterTest.php b/tests/Redis/ConcurrencyLimiterTest.php index c656fb4b432e..37512ab86bc9 100644 --- a/tests/Redis/ConcurrencyLimiterTest.php +++ b/tests/Redis/ConcurrencyLimiterTest.php @@ -12,12 +12,6 @@ class ConcurrencyLimiterTest extends TestCase { - protected function tearDown(): void - { - m::close(); - parent::tearDown(); - } - public function testAcquireUsesHashTagsOnPhpRedisClusterConnection() { $connection = m::mock(PhpRedisClusterConnection::class); From 06adfabe79f7924dbc5ebfaf2b458291d817853d Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:35:15 +0000 Subject: [PATCH 185/596] Update facade docblocks --- src/Illuminate/Support/Facades/App.php | 16 +++++++------- src/Illuminate/Support/Facades/Bus.php | 2 +- src/Illuminate/Support/Facades/Cache.php | 6 ++--- src/Illuminate/Support/Facades/Config.php | 6 ++--- src/Illuminate/Support/Facades/Context.php | 22 +++++++++---------- src/Illuminate/Support/Facades/DB.php | 4 ++-- src/Illuminate/Support/Facades/Exceptions.php | 8 +++---- src/Illuminate/Support/Facades/Hash.php | 2 +- src/Illuminate/Support/Facades/Http.php | 18 +++++++-------- .../Support/Facades/MaintenanceMode.php | 2 +- .../Support/Facades/Notification.php | 2 +- src/Illuminate/Support/Facades/Process.php | 6 ++--- src/Illuminate/Support/Facades/Queue.php | 8 +++---- src/Illuminate/Support/Facades/Request.php | 8 +++---- src/Illuminate/Support/Facades/Schedule.php | 14 ++++++------ src/Illuminate/Support/Facades/Schema.php | 22 +++++++++---------- src/Illuminate/Support/Facades/Session.php | 2 +- src/Illuminate/Support/Facades/Storage.php | 10 ++++----- 18 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 541792f5f5e5..5bad0f492df4 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -53,7 +53,7 @@ * @method static void loadDeferredProviders() * @method static void loadDeferredProvider(string $service) * @method static void registerDeferredProvider(string $provider, string|null $service = null) - * @method static object|mixed make(string|string $abstract, array $parameters = []) + * @method static object|mixed make(string $abstract, array $parameters = []) * @method static bool bound(string $abstract) * @method static bool isBooted() * @method static void boot() @@ -79,7 +79,7 @@ * @method static never abort(int $code, string $message = '', array $headers = []) * @method static \Illuminate\Foundation\Application terminating(callable|string $callback) * @method static void terminate() - * @method static array getLoadedProviders() + * @method static array getLoadedProviders() * @method static bool providerIsLoaded(string $provider) * @method static array getDeferredServices() * @method static void setDeferredServices(array $services) @@ -119,11 +119,11 @@ * @method static mixed rebinding(string $abstract, \Closure $callback) * @method static mixed refresh(string $abstract, mixed $target, string $method) * @method static \Closure wrap(\Closure $callback, array $parameters = []) - * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) - * @method static \Closure|\Closure factory(string|string $abstract) - * @method static object|mixed makeWith(string|string|callable $abstract, array $parameters = []) - * @method static object|mixed get(string|string $id) - * @method static object build(\Closure|string $concrete) + * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) + * @method static \Closure|\Closure factory(string $abstract) + * @method static object|mixed makeWith(string|callable $abstract, array $parameters = []) + * @method static object|mixed get(string $id) + * @method static object build(\Closure|string $concrete) * @method static mixed resolveFromAttribute(\ReflectionAttribute $attribute) * @method static void beforeResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void resolving(\Closure|string $abstract, \Closure|null $callback = null) @@ -138,7 +138,7 @@ * @method static void forgetInstances() * @method static void forgetScopedInstances() * @method static void resolveEnvironmentUsing(callable|string|null $callback) - * @method static bool currentEnvironmentIs(array|string $environments) + * @method static bool currentEnvironmentIs(array|string $environments) * @method static \Illuminate\Foundation\Application getInstance() * @method static \Illuminate\Contracts\Container\Container|\Illuminate\Foundation\Application setInstance(\Illuminate\Contracts\Container\Container|null $container = null) * @method static void macro(string $name, object|callable $macro) diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index f0220accd31d..9894bc405900 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -47,7 +47,7 @@ * @method static \Illuminate\Support\Collection dispatched(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedSync(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedAfterResponse(string $command, callable|null $callback = null) - * @method static \Illuminate\Support\Collection batched(callable $callback) + * @method static \Illuminate\Support\Collection batched(callable $callback) * @method static bool hasDispatched(string $command) * @method static bool hasDispatchedSync(string $command) * @method static bool hasDispatchedAfterResponse(string $command) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 4fc659602694..1aee186ee638 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -23,13 +23,13 @@ * @method static bool missing(\UnitEnum|string $key) * @method static mixed get(\UnitEnum|array|string $key, mixed $default = null) * @method static array many(array $keys) - * @method static iterable getMultiple(iterable $keys, mixed $default = null) + * @method static iterable getMultiple(iterable $keys, mixed $default = null) * @method static mixed pull(\UnitEnum|array|string $key, mixed $default = null) * @method static string string(\UnitEnum|string $key, \Closure|string|null $default = null) * @method static int integer(\UnitEnum|string $key, \Closure|int|null $default = null) * @method static float float(\UnitEnum|string $key, \Closure|float|null $default = null) * @method static bool boolean(\UnitEnum|string $key, \Closure|bool|null $default = null) - * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) + * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) * @method static bool put(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool set(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) @@ -47,7 +47,7 @@ * @method static \Illuminate\Cache\Limiters\ConcurrencyLimiterBuilder funnel(\UnitEnum|string $name) * @method static bool forget(\UnitEnum|array|string $key) * @method static bool delete(\UnitEnum|array|string $key) - * @method static bool deleteMultiple(iterable $keys) + * @method static bool deleteMultiple(iterable $keys) * @method static bool clear() * @method static bool flushLocks() * @method static \Illuminate\Cache\TaggedCache tags(mixed $names) diff --git a/src/Illuminate/Support/Facades/Config.php b/src/Illuminate/Support/Facades/Config.php index 990e34739f68..09228769a306 100755 --- a/src/Illuminate/Support/Facades/Config.php +++ b/src/Illuminate/Support/Facades/Config.php @@ -5,13 +5,13 @@ /** * @method static bool has(string $key) * @method static mixed get(array|string $key, mixed $default = null) - * @method static array getMany(array $keys) + * @method static array getMany(array $keys) * @method static string string(string $key, \Closure|string|null $default = null) * @method static int integer(string $key, \Closure|int|null $default = null) * @method static float float(string $key, \Closure|float|null $default = null) * @method static bool boolean(string $key, \Closure|bool|null $default = null) - * @method static array array(string $key, \Closure|array|null $default = null) - * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) + * @method static array array(string $key, \Closure|array|null $default = null) + * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) * @method static void set(array|string $key, mixed $value = null) * @method static void prepend(string $key, mixed $value) * @method static void push(string $key, mixed $value) diff --git a/src/Illuminate/Support/Facades/Context.php b/src/Illuminate/Support/Facades/Context.php index 714ec2b6ddd8..be57f00fa6d0 100644 --- a/src/Illuminate/Support/Facades/Context.php +++ b/src/Illuminate/Support/Facades/Context.php @@ -7,22 +7,22 @@ * @method static bool missing(string $key) * @method static bool hasHidden(string $key) * @method static bool missingHidden(string $key) - * @method static array all() - * @method static array allHidden() + * @method static array all() + * @method static array allHidden() * @method static mixed get(string $key, mixed $default = null) * @method static mixed getHidden(string $key, mixed $default = null) * @method static mixed pull(string $key, mixed $default = null) * @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 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 mixed remember(string $key, mixed $value) * @method static mixed rememberHidden(string $key, mixed $value) - * @method static \Illuminate\Log\Context\Repository forget(string|array $key) - * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) + * @method static \Illuminate\Log\Context\Repository forget(string|array $key) + * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) * @method static \Illuminate\Log\Context\Repository addIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository addHiddenIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository push(string $key, mixed ...$values) @@ -33,7 +33,7 @@ * @method static \Illuminate\Log\Context\Repository decrement(string $key, int $amount = 1) * @method static bool stackContains(string $key, mixed $value, bool $strict = false) * @method static bool hiddenStackContains(string $key, mixed $value, bool $strict = false) - * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) + * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) * @method static bool isEmpty() * @method static \Illuminate\Log\Context\Repository dehydrating(callable $callback) * @method static \Illuminate\Log\Context\Repository hydrated(callable $callback) diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 425b8a83c153..3da739b441ba 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -23,7 +23,7 @@ * @method static string[] availableDrivers() * @method static void extend(string $name, callable $resolver) * @method static void forgetExtension(string $name) - * @method static array getConnections() + * @method static array getConnections() * @method static void setReconnector(callable $reconnector) * @method static \Illuminate\Database\DatabaseManager setApplication(\Illuminate\Contracts\Foundation\Application $app) * @method static void macro(string $name, object|callable $macro) @@ -42,7 +42,7 @@ * @method static array selectFromWriteConnection(string $query, array $bindings = []) * @method static array select(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) * @method static array selectResultSets(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) - * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) + * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []) * @method static bool insert(string $query, array $bindings = []) * @method static int update(string $query, array $bindings = []) * @method static int delete(string $query, array $bindings = []) diff --git a/src/Illuminate/Support/Facades/Exceptions.php b/src/Illuminate/Support/Facades/Exceptions.php index 59b4b07ef2d7..263b95bd0418 100644 --- a/src/Illuminate/Support/Facades/Exceptions.php +++ b/src/Illuminate/Support/Facades/Exceptions.php @@ -15,7 +15,7 @@ * @method static \Illuminate\Foundation\Exceptions\Handler dontReportWhen(callable $dontReportWhen) * @method static \Illuminate\Foundation\Exceptions\Handler ignore(array|string $exceptions) * @method static \Illuminate\Foundation\Exceptions\Handler dontFlash(array|string $attributes) - * @method static \Illuminate\Foundation\Exceptions\Handler level(string<\Throwable> $type, string $level) + * @method static \Illuminate\Foundation\Exceptions\Handler level(string $type, string $level) * @method static void report(\Throwable $e) * @method static bool shouldReport(\Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler throttleUsing(callable $throttleUsing) @@ -26,14 +26,14 @@ * @method static \Illuminate\Foundation\Exceptions\Handler shouldRenderJsonWhen(callable $callback) * @method static \Illuminate\Foundation\Exceptions\Handler dontReportDuplicates() * @method static \Illuminate\Contracts\Debug\ExceptionHandler handler() - * @method static void assertReported(\Closure|string<\Throwable> $exception) + * @method static void assertReported(\Closure|string $exception) * @method static void assertReportedCount(int $count) - * @method static void assertNotReported(\Closure|string<\Throwable> $exception) + * @method static void assertNotReported(\Closure|string $exception) * @method static void assertNothingReported() * @method static void renderForConsole(\Symfony\Component\Console\Output\OutputInterface $output, \Throwable $e) * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwOnReport() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwFirstReported() - * @method static array<\Throwable> reported() + * @method static array reported() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake setHandler(\Illuminate\Contracts\Debug\ExceptionHandler $handler) * * @see \Illuminate\Foundation\Exceptions\Handler diff --git a/src/Illuminate/Support/Facades/Hash.php b/src/Illuminate/Support/Facades/Hash.php index f18a5fcee1f2..5a7057ac36a0 100755 --- a/src/Illuminate/Support/Facades/Hash.php +++ b/src/Illuminate/Support/Facades/Hash.php @@ -14,7 +14,7 @@ * @method static string getDefaultDriver() * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Hashing\HashManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Hashing\HashManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Hashing\HashManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index a35310045307..4967b314d7a7 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -10,21 +10,21 @@ * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalOptions(\Closure|array $options) * @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\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 \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(array|null $only = null) + * @method static \Illuminate\Http\Client\Factory allowStrayRequests(array|null $only = null) * @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|\Closure $callback) - * @method static void assertSentInOrder(array $callbacks) + * @method static void assertSentInOrder(array $callbacks) * @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(\Closure|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() @@ -65,7 +65,7 @@ * @method static \Illuminate\Http\Client\PendingRequest withMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withResponseMiddleware(callable $middleware) - * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) + * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) * @method static \Illuminate\Http\Client\PendingRequest beforeSending(callable $callback) * @method static \Illuminate\Http\Client\PendingRequest afterResponse(callable|null $callback) * @method static \Illuminate\Http\Client\PendingRequest throw(callable|null $callback = null) @@ -79,7 +79,7 @@ * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface patch(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface put(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface delete(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) - * @method static array pool(callable $callback, int|null $concurrency = 0) + * @method static array pool(callable $callback, int|null $concurrency = 0) * @method static \Illuminate\Http\Client\Batch batch(callable $callback) * @method static \Illuminate\Http\Client\Response|\Illuminate\Http\Client\Promises\LazyPromise send(string $method, string $url, array $options = []) * @method static \GuzzleHttp\Client buildClient() @@ -93,9 +93,9 @@ * @method static array mergeOptions(array ...$options) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) * @method static bool isAllowedRequestUrl(string $url) - * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) + * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) * @method static \GuzzleHttp\Promise\PromiseInterface|null getPromise() - * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) + * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) * @method static \Illuminate\Http\Client\PendingRequest dontTruncateExceptions() * @method static \Illuminate\Http\Client\PendingRequest setClient(\GuzzleHttp\Client $client) * @method static \Illuminate\Http\Client\PendingRequest setHandler(callable $handler) diff --git a/src/Illuminate/Support/Facades/MaintenanceMode.php b/src/Illuminate/Support/Facades/MaintenanceMode.php index d88a01c7944a..90200acdd787 100644 --- a/src/Illuminate/Support/Facades/MaintenanceMode.php +++ b/src/Illuminate/Support/Facades/MaintenanceMode.php @@ -8,7 +8,7 @@ * @method static string getDefaultDriver() * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Foundation\MaintenanceModeManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Foundation\MaintenanceModeManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Foundation\MaintenanceModeManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Notification.php b/src/Illuminate/Support/Facades/Notification.php index bb731b170056..eb2e088b51ce 100644 --- a/src/Illuminate/Support/Facades/Notification.php +++ b/src/Illuminate/Support/Facades/Notification.php @@ -16,7 +16,7 @@ * @method static \Illuminate\Notifications\ChannelManager locale(string $locale) * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Notifications\ChannelManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Notifications\ChannelManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Notifications\ChannelManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index afb4a71b3d78..f15e70bcd292 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -6,7 +6,7 @@ use Illuminate\Process\Factory; /** - * @method static \Illuminate\Process\PendingProcess command(array|string $command) + * @method static \Illuminate\Process\PendingProcess command(array|string $command) * @method static \Illuminate\Process\PendingProcess path(string $path) * @method static \Illuminate\Process\PendingProcess timeout(\Carbon\CarbonInterval|int $timeout) * @method static \Illuminate\Process\PendingProcess idleTimeout(\Carbon\CarbonInterval|int $timeout) @@ -16,8 +16,8 @@ * @method static \Illuminate\Process\PendingProcess quietly() * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) - * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) - * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) * @method static bool supportsTty() * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) * @method static \Illuminate\Process\PendingProcess|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 09449af944bf..6adba39e69e6 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -64,15 +64,15 @@ * @method static void assertCount(int $expectedCount) * @method static void assertNothingPushed() * @method static \Illuminate\Support\Collection pushed(string $job, callable|null $callback = null) - * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) - * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) + * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) + * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) * @method static bool hasPushed(string $job) - * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) + * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) * @method static \Illuminate\Support\Collection delayedJobs(string|null $queue = null) * @method static \Illuminate\Support\Collection reservedJobs(string|null $queue = null) * @method static bool shouldFakeJob(object $job) * @method static array pushedJobs() - * @method static array rawPushes() + * @method static array rawPushes() * @method static \Illuminate\Support\Testing\Fakes\QueueFake serializeAndRestore(bool $serializeAndRestore = true) * @method static void releaseUniqueJobLocks() * diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 0ff5c7aac60c..2865715dcb98 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -152,9 +152,9 @@ * @method static string|array|null post(string|null $key = null, string|array|null $default = null) * @method static bool hasCookie(string $key) * @method static string|array|null cookie(string|null $key = null, string|array|null $default = null) - * @method static array allFiles() + * @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) @@ -175,8 +175,8 @@ * @method static float|int clamp(string $key, int|float $min, int|float $max, int|float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, \UnitEnum|string|null $tz = null) * @method static \Carbon\CarbonInterval|null interval(string $key, \Carbon\Unit|string|null $unit = null) - * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string<\BackedEnum> $enumClass, \BackedEnum|null $default = null) - * @method static \BackedEnum[] enums(string $key, string<\BackedEnum> $enumClass) + * @method static \BackedEnum|(\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) * @method static array only(mixed $keys) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index 7da6c9d1fb60..86a2c02e6933 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -51,7 +51,7 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyFifteenMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThirtyMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyOddHour(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyTwoHours(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThreeHours(array|string|int $offset = 0) @@ -60,8 +60,8 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daily() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes at(string $time) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes dailyAt(string $time) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekdays() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekends() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes mondays() @@ -74,14 +74,14 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weeklyOn(mixed $dayOfWeek, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes lastDayOfMonth(string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array>|int ...$days) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array|int ...$days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterlyOn(int $dayOfQuarter = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes days(mixed $days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes timezone(\UnitEnum|\DateTimeZone|string $timezone) * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 523813f228ed..5c617687bb3a 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -10,31 +10,31 @@ * @method static void morphUsingUlids() * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) - * @method static array getSchemas() + * @method static array getSchemas() * @method static bool hasTable(string $table) * @method static bool hasView(string $view) - * @method static array getTables(string|string[]|null $schema = null) - * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) - * @method static array getViews(string|string[]|null $schema = null) - * @method static array getTypes(string|string[]|null $schema = null) + * @method static array getTables(string|string[]|null $schema = null) + * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) + * @method static array getViews(string|string[]|null $schema = null) + * @method static array getTypes(string|string[]|null $schema = null) * @method static bool hasColumn(string $table, string $column) - * @method static bool hasColumns(string $table, array $columns) + * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) * @method static void whenTableDoesntHaveColumn(string $table, string $column, \Closure $callback) * @method static void whenTableHasIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static void whenTableDoesntHaveIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static string getColumnType(string $table, string $column, bool $fullDefinition = false) - * @method static array getColumnListing(string $table) - * @method static array getColumns(string $table) - * @method static array getIndexes(string $table) - * @method static array getIndexListing(string $table) + * @method static array getColumnListing(string $table) + * @method static array getColumns(string $table) + * @method static array getIndexes(string $table) + * @method static array getIndexListing(string $table) * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) * @method static void dropIfExists(string $table) - * @method static void dropColumns(string $table, string|array $columns) + * @method static void dropColumns(string $table, string|array $columns) * @method static void dropAllTables() * @method static void dropAllViews() * @method static void dropAllTypes() diff --git a/src/Illuminate/Support/Facades/Session.php b/src/Illuminate/Support/Facades/Session.php index 3cef4609632f..7d23e6c2e9fd 100755 --- a/src/Illuminate/Support/Facades/Session.php +++ b/src/Illuminate/Support/Facades/Session.php @@ -12,7 +12,7 @@ * @method static void setDefaultDriver(string $name) * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Session\SessionManager extend(string $driver, \Closure $callback) - * @method static array getDrivers() + * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() * @method static \Illuminate\Session\SessionManager setContainer(\Illuminate\Contracts\Container\Container $container) * @method static \Illuminate\Session\SessionManager forgetDrivers() diff --git a/src/Illuminate/Support/Facades/Storage.php b/src/Illuminate/Support/Facades/Storage.php index 941d5fe69d25..9f37e575eca4 100644 --- a/src/Illuminate/Support/Facades/Storage.php +++ b/src/Illuminate/Support/Facades/Storage.php @@ -40,10 +40,10 @@ * @method static bool move(string $from, string $to) * @method static int size(string $path) * @method static int lastModified(string $path) - * @method static array files(string|null $directory = null, bool $recursive = false) - * @method static array allFiles(string|null $directory = null) - * @method static array directories(string|null $directory = null, bool $recursive = false) - * @method static array allDirectories(string|null $directory = null) + * @method static array files(string|null $directory = null, bool $recursive = false) + * @method static array allFiles(string|null $directory = null) + * @method static array directories(string|null $directory = null, bool $recursive = false) + * @method static array allDirectories(string|null $directory = null) * @method static bool makeDirectory(string $path) * @method static bool deleteDirectory(string $directory) * @method static \Illuminate\Filesystem\FilesystemAdapter assertExists(string|array $path, string|null $content = null) @@ -81,7 +81,7 @@ * @method static mixed macroCall(string $method, array $parameters) * @method static bool has(string $location) * @method static string read(string $location) - * @method static \League\Flysystem\DirectoryListing<\League\Flysystem\StorageAttributes> listContents(string $location, bool $deep = false) + * @method static \League\Flysystem\DirectoryListing listContents(string $location, bool $deep = false) * @method static int fileSize(string $path) * @method static string visibility(string $path) * @method static void write(string $location, string $contents, array $config = []) From 04130c8a3c7c5f1cc28b4d77f6a048712e687394 Mon Sep 17 00:00:00 2001 From: Karim Mahmoud Hassan Date: Thu, 16 Apr 2026 16:48:44 +0200 Subject: [PATCH 186/596] [13.x] Fix Vite CSS not loaded from nested chunk imports (#59662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [13.x] Fix Vite CSS not loaded from nested chunk imports The `__invoke` method only resolved CSS from direct imports (one level deep). Vite 8 (Rolldown) keeps CSS on the chunk where the component is defined rather than hoisting it to the nearest entry, so CSS from deeply nested imports never received a `` tag. This adds a `resolveImports` method that recursively walks the full import tree—matching the approach already used by the prefetch code and documented in Vite's backend integration guide. The fix is backward-compatible: with older Vite/Rollup builds where CSS was hoisted, nested chunks have empty `css` arrays, so the recursive walk produces identical output. * Remove unnecessary pass-by-reference --------- Co-authored-by: Pascal Baljet --- src/Illuminate/Foundation/Vite.php | 30 ++++++++++++++- tests/Foundation/FoundationViteTest.php | 50 ++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index b348992c20f2..91b3486badf5 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -396,7 +396,7 @@ public function __invoke($entrypoints, $buildDirectory = null) $manifest, ]); - foreach ($chunk['imports'] ?? [] as $import) { + foreach ($this->resolveImports($manifest, $chunk) as $import) { $preloads->push([ $import, $this->assetPath("{$buildDirectory}/{$manifest[$import]['file']}"), @@ -984,6 +984,34 @@ public function manifestHash($buildDirectory = null) return md5_file($path) ?: null; } + /** + * Recursively resolve all imports for the given chunk. + * + * @param array $manifest + * @param array $chunk + * @param array $seen + * @return array + */ + protected function resolveImports($manifest, $chunk, $seen = []) + { + $imports = []; + + foreach ($chunk['imports'] ?? [] as $import) { + if (isset($seen[$import])) { + continue; + } + + $seen[$import] = true; + $imports[] = $import; + + if (isset($manifest[$import])) { + $imports = array_merge($imports, $this->resolveImports($manifest, $manifest[$import], $seen)); + } + } + + return $imports; + } + /** * Get the chunk for the given entry point / asset. * diff --git a/tests/Foundation/FoundationViteTest.php b/tests/Foundation/FoundationViteTest.php index 0ac9c39512f3..8389737c6074 100644 --- a/tests/Foundation/FoundationViteTest.php +++ b/tests/Foundation/FoundationViteTest.php @@ -75,6 +75,46 @@ public function testViteWithSharedCssImport() ); } + public function testViteWithNestedCssImport() + { + $buildDir = Str::random(); + $this->makeViteManifest([ + 'resources/js/app.js' => [ + 'src' => 'resources/js/app.js', + 'file' => 'assets/app.versioned.js', + 'imports' => [ + '_layout.js', + ], + ], + '_layout.js' => [ + 'file' => 'assets/layout.versioned.js', + 'css' => [ + 'assets/layout.versioned.css', + ], + 'imports' => [ + '_header.js', + ], + ], + '_header.js' => [ + 'file' => 'assets/header.versioned.js', + 'css' => [ + 'assets/header.versioned.css', + ], + ], + ], $buildDir); + + $result = app(Vite::class)(['resources/js/app.js'], $buildDir); + + $this->assertStringEndsWith( + '' + .'' + .'', + $result->toHtml() + ); + + $this->cleanViteManifest($buildDir); + } + public function testViteHotModuleReplacementWithJsOnly() { $this->makeViteHotFile(); @@ -728,12 +768,12 @@ public function testItGeneratesPreloadDirectivesForJsAndCssImports() .'' .'' .'' + .'' .'' .'' .'' .'' .'' - .'' .'' .'', $result->toHtml() ); @@ -754,6 +794,10 @@ public function testItGeneratesPreloadDirectivesForJsAndCssImports() 'rel="modulepreload"', 'as="script"', ], + 'https://example.com/'.$buildDir.'/assets/_plugin-vue_export-helper.cdc0426e.js' => [ + 'rel="modulepreload"', + 'as="script"', + ], 'https://example.com/'.$buildDir.'/assets/AuthenticationCardLogo.9999a373.js' => [ 'rel="modulepreload"', 'as="script"', @@ -774,10 +818,6 @@ public function testItGeneratesPreloadDirectivesForJsAndCssImports() 'rel="modulepreload"', 'as="script"', ], - 'https://example.com/'.$buildDir.'/assets/_plugin-vue_export-helper.cdc0426e.js' => [ - 'rel="modulepreload"', - 'as="script"', - ], ], ViteFacade::preloadedAssets()); $this->cleanViteManifest($buildDir); From b6ee18a4e2feb3c80790de46d0fd78421afe65ea Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 16 Apr 2026 09:49:41 -0500 Subject: [PATCH 187/596] wip --- src/Illuminate/Foundation/Vite.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 91b3486badf5..855aa73992e4 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -1002,6 +1002,7 @@ protected function resolveImports($manifest, $chunk, $seen = []) } $seen[$import] = true; + $imports[] = $import; if (isset($manifest[$import])) { From 030243fa779b25a74b4a6b3c62855264bd4f3bfb Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Fri, 17 Apr 2026 14:09:16 +0100 Subject: [PATCH 188/596] Support named credential providers for SQS queue connections (#59733) Allow SQS queue connections to specify a credential provider by name (ecs, instance) via the credentials config key, and configure Cloud to automatically use ECS credentials for managed queues. Co-authored-by: Claude Opus 4.6 (1M context) --- src/Illuminate/Foundation/Cloud.php | 16 +++++++- .../Queue/Connectors/SqsConnector.php | 35 ++++++++++++++++- tests/Integration/Foundation/CloudTest.php | 39 ++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 31a51c03cdd0..5cda82a7ae67 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -30,7 +30,7 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst static::configureDisks($app); static::configureUnpooledPostgresConnection($app); static::ensureMigrationsUseUnpooledConnection($app); - static::configureManagedQueues(); + static::configureManagedQueues($app); }, HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); @@ -117,10 +117,22 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): /** * Configure managed queues if applicable. */ - public static function configureManagedQueues(): void + public static function configureManagedQueues(Application $app): void { if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { Worker::$restartable = false; + + $app['config']->set( + 'queue.connections.sqs.credentials', + 'ecs' + ); + + if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { + $app['config']->set( + 'queue.connections.sqs.region', + $_SERVER['LARAVEL_CLOUD_REGION'] + ); + } } } diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index bb702f7536a0..70c90873d794 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -2,9 +2,11 @@ namespace Illuminate\Queue\Connectors; +use Aws\Credentials\CredentialProvider; use Aws\Sqs\SqsClient; use Illuminate\Queue\SqsQueue; use Illuminate\Support\Arr; +use InvalidArgumentException; class SqsConnector implements ConnectorInterface { @@ -18,7 +20,9 @@ public function connect(array $config) { $config = $this->getDefaultConfiguration($config); - if (! empty($config['key']) && ! empty($config['secret'])) { + if ($credentials = $this->resolveCredentialProvider($config)) { + $config['credentials'] = $credentials; + } elseif (! empty($config['key']) && ! empty($config['secret'])) { $config['credentials'] = Arr::only($config, ['key', 'secret']); if (! empty($config['token'])) { @@ -37,6 +41,35 @@ public function connect(array $config) ); } + /** + * Resolve a credential provider from the given config. + * + * @param array $config + * @return callable|null + * + * @throws \InvalidArgumentException + */ + protected function resolveCredentialProvider(array $config) + { + $credentials = $config['credentials'] ?? null; + + $provider = is_string($credentials) ? $credentials : ($credentials['provider'] ?? null); + + if (is_null($provider)) { + return null; + } + + $options = is_array($credentials) ? Arr::except($credentials, ['provider']) : []; + + return match ($provider) { + 'ecs' => CredentialProvider::ecsCredentials($options), + 'instance' => CredentialProvider::instanceProfile($options), + default => throw new InvalidArgumentException( + "Invalid credential provider [{$provider}]." + ), + }; + } + /** * Get the default configuration for SQS. * diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index ad6e35b55752..d79d31dfea09 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -60,7 +60,7 @@ public function test_it_disables_queue_restart_polling_for_managed_queues() $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; try { - Cloud::configureManagedQueues(); + Cloud::configureManagedQueues($this->app); $this->assertFalse(Worker::$restartable); } finally { @@ -69,6 +69,43 @@ public function test_it_disables_queue_restart_polling_for_managed_queues() } } + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function test_it_configures_managed_queue_credentials() + { + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + } + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function test_it_does_not_configure_managed_queues_when_not_enabled() + { + Cloud::configureManagedQueues($this->app); + + $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function test_it_configures_managed_queue_region() + { + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + } + } + public function test_it_respects_log_levels() { if (isset($_SERVER['LOG_LEVEL'])) { From 92fecddb6ff326dabf8c1de12c331fdf812994fd Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Fri, 17 Apr 2026 15:15:54 +0200 Subject: [PATCH 189/596] [13.x] Enforce stricter assertions (#59749) * Enforce stricter assertions * StyleCI fix * Fix test --------- Co-authored-by: Lucas Michot --- tests/Broadcasting/BroadcasterTest.php | 4 +- tests/Cache/CacheManagerTest.php | 2 +- tests/Cache/CacheSessionStoreTest.php | 2 +- tests/Console/CacheCommandMutexTest.php | 4 +- .../AfterResolvingAttributeCallbackTest.php | 2 +- tests/Container/ContainerCallTest.php | 2 +- tests/Container/ContainerTest.php | 8 +- .../ContextualAttributeBindingTest.php | 12 +-- tests/Container/UtilTest.php | 2 +- tests/Cookie/CookieTest.php | 4 +- .../DatabaseConcernsHasAttributesTest.php | 2 +- ...eConcernsPreventsCircularRecursionTest.php | 2 +- tests/Database/DatabaseConnectionTest.php | 4 +- ...tabaseEloquentBuilderCreateOrFirstTest.php | 2 +- .../Database/DatabaseEloquentBuilderTest.php | 6 +- .../DatabaseEloquentCollectionTest.php | 14 +-- .../Database/DatabaseEloquentFactoryTest.php | 22 ++-- .../DatabaseEloquentGlobalScopesTest.php | 10 +- .../DatabaseEloquentIntegrationTest.php | 2 +- tests/Database/DatabaseEloquentModelTest.php | 34 +++--- tests/Database/DatabaseEloquentPivotTest.php | 2 +- .../Database/DatabaseMigrationCreatorTest.php | 2 +- tests/Database/DatabaseQueryBuilderTest.php | 100 +++++++++--------- .../DatabaseTransactionsManagerTest.php | 8 +- .../EloquentModelCustomCastingTest.php | 4 +- tests/Events/EventsDispatcherTest.php | 6 +- tests/Filesystem/FilesystemAdapterTest.php | 8 +- tests/Filesystem/FilesystemManagerTest.php | 10 +- tests/Filesystem/FilesystemTest.php | 2 +- .../Configuration/MiddlewareTest.php | 6 +- .../Console/RouteListCommandTest.php | 32 +++--- .../Exceptions/Renderer/ListenerTest.php | 12 +-- .../Foundation/FoundationFormRequestTest.php | 2 +- .../FoundationInteractsWithDatabaseTest.php | 6 +- tests/Foundation/Http/KernelTest.php | 4 +- .../Concerns/MakesHttpRequestsTest.php | 6 +- tests/Http/HttpClientTest.php | 20 ++-- tests/Http/HttpRedirectResponseTest.php | 4 +- tests/Http/HttpRequestTest.php | 10 +- .../Middleware/PreventRequestForgeryTest.php | 8 +- tests/Http/Middleware/TrimStringsTest.php | 24 ++--- tests/Integration/Cache/RedisStoreTest.php | 14 +-- .../Concurrency/ConcurrencyTest.php | 6 +- .../InvokeSerializedClosureCommandTest.php | 10 +- .../Container/BuildableIntegrationTest.php | 6 +- ...extualAttributesBindingIntegrationTest.php | 4 +- .../Database/EloquentCursorPaginateTest.php | 6 +- .../Database/EloquentDeleteTest.php | 4 +- .../Database/EloquentHasManyThroughTest.php | 20 ++-- .../Database/EloquentPivotEventsTest.php | 2 +- .../Database/EloquentUpdateTest.php | 2 +- .../Database/EloquentWhereTest.php | 2 +- .../DatabaseMariaDbSchemaBuilderTest.php | 2 +- .../Database/ModelInspectorTest.php | 6 +- .../MySql/DatabaseMySqlSchemaBuilderTest.php | 2 +- .../Postgres/PostgresSchemaBuilderTest.php | 4 +- .../Integration/Database/QueryBuilderTest.php | 4 +- .../Database/SchemaBuilderSchemaNameTest.php | 10 +- .../Database/SchemaBuilderTest.php | 2 +- .../DatabaseSqliteSchemaBuilderTest.php | 2 +- tests/Integration/Events/DeferEventsTest.php | 8 +- tests/Integration/Events/EventFakeTest.php | 2 +- .../Integration/Filesystem/ServeFileTest.php | 2 +- tests/Integration/Foundation/CloudTest.php | 6 +- .../Foundation/DiscoverEventsTest.php | 2 +- .../Foundation/ExceptionHandlerTest.php | 2 +- .../Foundation/FoundationHelpersTest.php | 8 +- tests/Integration/Http/HttpClientTest.php | 12 +-- tests/Integration/Http/ResourceTest.php | 8 +- .../Log/ContextIntegrationTest.php | 2 +- tests/Integration/Queue/DebouncedJobTest.php | 2 +- tests/Integration/Queue/JobChainingTest.php | 8 +- .../Queue/ModelSerializationTest.php | 4 +- tests/Integration/Queue/QueueFakeTest.php | 4 +- tests/Integration/Queue/UniqueJobTest.php | 8 +- tests/Integration/Routing/RouteViewTest.php | 12 +-- .../Session/DatabaseSessionHandlerTest.php | 6 +- tests/Log/LogLoggerTest.php | 2 +- tests/Log/LogManagerTest.php | 6 +- tests/Mail/MailMailableTest.php | 8 +- tests/Mail/MailableAlternativeSyntaxTest.php | 20 ++-- tests/Pipeline/PipelineTransactionTest.php | 4 +- tests/Process/ProcessTest.php | 78 +++++++------- .../DatabaseUuidFailedJobProviderTest.php | 6 +- tests/Queue/InteractsWithQueueTest.php | 2 +- tests/Queue/QueueSqsQueueTest.php | 4 +- tests/Redis/RedisConnectorTest.php | 8 +- tests/Routing/RouteCollectionTest.php | 2 +- tests/Routing/RouteRegistrarTest.php | 6 +- tests/Routing/RoutingRouteTest.php | 2 +- tests/Routing/RoutingSortedMiddlewareTest.php | 2 +- .../Session/CacheBasedSessionHandlerTest.php | 4 +- tests/Session/FileSessionHandlerTest.php | 6 +- .../Middleware/AuthenticateSessionTest.php | 42 ++++---- tests/Session/SessionStoreTest.php | 2 +- tests/Support/SupportArrTest.php | 29 ++--- tests/Support/SupportCarbonTest.php | 10 +- tests/Support/SupportCollectionTest.php | 66 ++++++------ tests/Support/SupportHelpersTest.php | 82 +++++++------- tests/Support/SupportHtmlStringTest.php | 2 +- tests/Support/SupportJsTest.php | 22 ++-- .../SupportLazyCollectionIsLazyTest.php | 2 +- tests/Support/SupportMailTest.php | 4 +- tests/Support/SupportStrTest.php | 28 ++--- tests/Support/SupportStringableTest.php | 26 ++--- tests/Support/SupportTestingMailFakeTest.php | 2 +- tests/Support/SupportUriTest.php | 66 ++++++------ tests/Support/ValidatedInputTest.php | 2 +- tests/Testing/TestResponseTest.php | 2 +- .../Translation/TranslationFileLoaderTest.php | 4 +- .../TranslationMessageSelectorTest.php | 4 +- .../Translation/TranslationTranslatorTest.php | 2 +- tests/Validation/ValidationDateRuleTest.php | 36 +++---- tests/Validation/ValidationExceptionTest.php | 4 +- tests/Validation/ValidationExcludeIfTest.php | 2 +- .../Validation/ValidationInArrayKeysTest.php | 2 +- .../Validation/ValidationNumericRuleTest.php | 50 ++++----- .../Validation/ValidationProhibitedIfTest.php | 2 +- tests/Validation/ValidationRuleCanTest.php | 4 +- tests/Validation/ValidationRuleParserTest.php | 4 +- tests/Validation/ValidationValidatorTest.php | 34 +++--- tests/View/Blade/BladeBoolTest.php | 8 +- tests/View/ViewComponentAttributeBagTest.php | 20 ++-- 123 files changed, 671 insertions(+), 672 deletions(-) diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 2e1bbe3625e2..38ac5a9d46e8 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -53,13 +53,13 @@ public function testExtractingParametersWhileCheckingForUserAccess() // }; $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); - $this->assertEquals([], $parameters); + $this->assertSame([], $parameters); $callback = function ($user, $something) { // }; $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); - $this->assertEquals([], $parameters); + $this->assertSame([], $parameters); // Test Explicit Binding... $container = new Container; diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index a4f0eca03693..0a1319b0daf2 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -190,7 +190,7 @@ public function testItSetsDefaultDriverChangesGlobalConfig() $cacheManager->setDefaultDriver('><((((@>'); - $this->assertEquals('><((((@>', $app->get('config')->get('cache.default')); + $this->assertSame('><((((@>', $app->get('config')->get('cache.default')); } public function testItPurgesMemoizedStoreObjects() diff --git a/tests/Cache/CacheSessionStoreTest.php b/tests/Cache/CacheSessionStoreTest.php index 89f1c230bc68..02dda73f6d6c 100755 --- a/tests/Cache/CacheSessionStoreTest.php +++ b/tests/Cache/CacheSessionStoreTest.php @@ -207,7 +207,7 @@ public function testCacheKey() public function testItemKey() { $store = new SessionStore(self::getSession(), 'custom_prefix'); - $this->assertEquals('custom_prefix.foo', $store->itemKey('foo')); + $this->assertSame('custom_prefix.foo', $store->itemKey('foo')); } public function testValuesAreStoredByReference() diff --git a/tests/Console/CacheCommandMutexTest.php b/tests/Console/CacheCommandMutexTest.php index 9654ee28033c..3675faaaff2f 100644 --- a/tests/Console/CacheCommandMutexTest.php +++ b/tests/Console/CacheCommandMutexTest.php @@ -166,7 +166,7 @@ public function testCommandMutexNameWithoutIsolatedMutexNameMethod() $this->cacheRepository->shouldReceive('add') ->once() ->withArgs(function ($key) { - $this->assertEquals('framework'.DIRECTORY_SEPARATOR.'command-command-name', $key); + $this->assertSame('framework'.DIRECTORY_SEPARATOR.'command-command-name', $key); return true; }) @@ -196,7 +196,7 @@ public function isolatableId() $this->cacheRepository->shouldReceive('add') ->once() ->withArgs(function ($key) { - $this->assertEquals('framework'.DIRECTORY_SEPARATOR.'command-command-name-isolated', $key); + $this->assertSame('framework'.DIRECTORY_SEPARATOR.'command-command-name-isolated', $key); return true; }) diff --git a/tests/Container/AfterResolvingAttributeCallbackTest.php b/tests/Container/AfterResolvingAttributeCallbackTest.php index ea3aa4744413..91e4f681b2ab 100644 --- a/tests/Container/AfterResolvingAttributeCallbackTest.php +++ b/tests/Container/AfterResolvingAttributeCallbackTest.php @@ -55,7 +55,7 @@ public function testCallbackIsCalledAfterClassWithConstructorAndAttributeIsResol $instance = $container->make(ContainerTestHasSelfConfiguringAttributeAndConstructor::class); $this->assertInstanceOf(ContainerTestHasSelfConfiguringAttributeAndConstructor::class, $instance); - $this->assertEquals('the-right-value', $instance->value); + $this->assertSame('the-right-value', $instance->value); } public function testCallbackIsCalledOnAppCall() diff --git a/tests/Container/ContainerCallTest.php b/tests/Container/ContainerCallTest.php index 74460d99a087..80c5f83b9900 100644 --- a/tests/Container/ContainerCallTest.php +++ b/tests/Container/ContainerCallTest.php @@ -129,7 +129,7 @@ public function testCallWithDependencies() }); $this->assertInstanceOf(stdClass::class, $result[0]); - $this->assertEquals([], $result[1]); + $this->assertSame([], $result[1]); $result = $container->call(function (stdClass $foo, $bar = []) { return func_get_args(); diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 25f0ab464e0a..36f1cc03a440 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -661,7 +661,7 @@ public function testNestedParametersAreResetForFreshMake() return $config; }); - $this->assertEquals([], $container->make('foo', ['something'])); + $this->assertSame([], $container->make('foo', ['something'])); } public function testSingletonBindingsNotRespectedWithMakeParameters() @@ -920,7 +920,7 @@ public function testWithFactoryHasDependency() $this->assertInstanceOf(RequestDto::class, $r); $this->assertEquals(999, $r->userId); - $this->assertEquals('taylor@laravel.com', $r->email); + $this->assertSame('taylor@laravel.com', $r->email); } // public function testContainerCanCatchCircularDependency() @@ -1118,9 +1118,7 @@ class WildcardConcrete implements WildcardOnlyInterface { } -/* - * The order of these attributes matters because we want to ensure we only fallback to '*' when there's no more specific environment. - */ +// The order of these attributes matters because we want to ensure we only fallback to '*' when there's no more specific environment. #[Bind(FallbackConcrete::class)] #[Bind(ProdConcrete::class, environments: 'prod')] interface WildcardAndProdInterface diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index 78e321033f85..041f1087698e 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -100,7 +100,7 @@ public function testScalarDependencyCanBeResolvedFromAttributeBinding() $class = $container->make(ContainerTestHasConfigValueProperty::class); $this->assertInstanceOf(ContainerTestHasConfigValueProperty::class, $class); - $this->assertEquals('Europe/Paris', $class->timezone); + $this->assertSame('Europe/Paris', $class->timezone); } public function testScalarDependencyCanBeResolvedFromAttributeResolveMethod() @@ -115,7 +115,7 @@ public function testScalarDependencyCanBeResolvedFromAttributeResolveMethod() $class = $container->make(ContainerTestHasConfigValueWithResolveProperty::class); $this->assertInstanceOf(ContainerTestHasConfigValueWithResolveProperty::class, $class); - $this->assertEquals('production', $class->env); + $this->assertSame('production', $class->env); } public function testDependencyWithAfterCallbackAttributeCanBeResolved() @@ -124,7 +124,7 @@ public function testDependencyWithAfterCallbackAttributeCanBeResolved() $class = $container->make(ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback::class); - $this->assertEquals('Developer', $class->person->role); + $this->assertSame('Developer', $class->person->role); } public function testAuthedAttribute() @@ -289,7 +289,7 @@ public function testInjectionWithAttributeOnAppCall() return $hasAttribute->person; }); - $this->assertEquals('Taylor', $person->name); + $this->assertSame('Taylor', $person->name); } public function testAttributeOnAppCall() @@ -306,7 +306,7 @@ public function testAttributeOnAppCall() return $value; }); - $this->assertEquals('Europe/Paris', $value); + $this->assertSame('Europe/Paris', $value); $value = $container->call(function (#[Config('app.locale')] ?string $value) { return $value; @@ -329,7 +329,7 @@ public function testNestedAttributeOnAppCall() return $object; }); - $this->assertEquals('Europe/Paris', $value->timezone); + $this->assertSame('Europe/Paris', $value->timezone); $value = $container->call(function (LocaleObject $object) { return $object; diff --git a/tests/Container/UtilTest.php b/tests/Container/UtilTest.php index 928e07ad8841..cd5cbf56b53a 100644 --- a/tests/Container/UtilTest.php +++ b/tests/Container/UtilTest.php @@ -26,7 +26,7 @@ public function testArrayWrap() $this->assertEquals(['a'], Util::arrayWrap($string)); $this->assertEquals($array, Util::arrayWrap($array)); $this->assertEquals([$object], Util::arrayWrap($object)); - $this->assertEquals([], Util::arrayWrap(null)); + $this->assertSame([], Util::arrayWrap(null)); $this->assertEquals([null], Util::arrayWrap([null])); $this->assertEquals([null, null], Util::arrayWrap([null, null])); $this->assertEquals([''], Util::arrayWrap('')); diff --git a/tests/Cookie/CookieTest.php b/tests/Cookie/CookieTest.php index ee68be316c18..519e85b091d0 100755 --- a/tests/Cookie/CookieTest.php +++ b/tests/Cookie/CookieTest.php @@ -82,7 +82,7 @@ public function testQueuedCookiesWithHandlingEmptyValues(): void $cookie = $this->getCreator(); $cookie->queue($cookie->make('foo', '')); $this->assertTrue($cookie->hasQueued('foo')); - $this->assertEquals('', $cookie->queued('foo')->getValue()); + $this->assertSame('', $cookie->queued('foo')->getValue()); } public function testQueuedCookiesWithRepeatedValue(): void @@ -90,7 +90,7 @@ public function testQueuedCookiesWithRepeatedValue(): void $cookie = $this->getCreator(); $cookie->queue($cookie->make('foo', 'newBar')); $this->assertTrue($cookie->hasQueued('foo')); - $this->assertEquals('newBar', $cookie->queued('foo')->getValue()); + $this->assertSame('newBar', $cookie->queued('foo')->getValue()); $this->expectException(ArgumentCountError::class); $cookie->queue('invalidCookie'); diff --git a/tests/Database/DatabaseConcernsHasAttributesTest.php b/tests/Database/DatabaseConcernsHasAttributesTest.php index ac931b034a26..3bf1b62d3de7 100644 --- a/tests/Database/DatabaseConcernsHasAttributesTest.php +++ b/tests/Database/DatabaseConcernsHasAttributesTest.php @@ -54,7 +54,7 @@ public function testCastingEmptyStringToArrayDoesNotError() public function testUnsettingCachedAttribute() { $instance = new HasCacheableAttributeWithAccessor(); - $this->assertEquals('foo', $instance->getAttribute('cacheableProperty')); + $this->assertSame('foo', $instance->getAttribute('cacheableProperty')); $this->assertTrue($instance->cachedAttributeIsset('cacheableProperty')); unset($instance->cacheableProperty); diff --git a/tests/Database/DatabaseConcernsPreventsCircularRecursionTest.php b/tests/Database/DatabaseConcernsPreventsCircularRecursionTest.php index 4a49b08afbdf..359cd6d1a497 100644 --- a/tests/Database/DatabaseConcernsPreventsCircularRecursionTest.php +++ b/tests/Database/DatabaseConcernsPreventsCircularRecursionTest.php @@ -183,7 +183,7 @@ public function testMockedModelCallToWithoutRecursionMethodWorks(): void fn () => array_merge($mock->attributesToArray(), $mock->relationsToArray()), fn () => $mock->attributesToArray(), ); - $this->assertEquals([], $toArray); + $this->assertSame([], $toArray); } } diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index b13f3011fa6d..f91806bc6a65 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -552,8 +552,8 @@ public function testGetRawQueryLog() $log = $mock->getRawQueryLog(); - $this->assertEquals("select * from tbl where col = 'foo'", $log[0]['raw_query']); - $this->assertEquals(1.23, $log[0]['time']); + $this->assertSame("select * from tbl where col = 'foo'", $log[0]['raw_query']); + $this->assertSame(1.23, $log[0]['time']); } public function testQueryExceptionContainsReadConnectionDetailsWhenUsingReadPdo() diff --git a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php index b061a5f3bca9..2b5a4177bb52 100755 --- a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php @@ -530,7 +530,7 @@ public function testUpdateOrCreateMethodAcceptsClosureValuesAndUpdates(): void $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], fn () => ['val' => 'baz']); $this->assertFalse($result->wasRecentlyCreated); - $this->assertEquals('baz', $result->val); + $this->assertSame('baz', $result->val); } public function testUpdateOrCreateInvokesClosureExactlyOnceWhenCreating(): void diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 8b1709c0bd24..55f10f8bf0ba 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -336,7 +336,7 @@ public function testGetMethodDoesntHydrateEagerRelationsWhenNoResultsAreReturned $builder->getModel()->shouldReceive('newCollection')->with([])->andReturn(new Collection([])); $results = $builder->get(['foo']); - $this->assertEquals([], $results->all()); + $this->assertSame([], $results->all()); } public function testValueMethodWithModelFound() @@ -1107,7 +1107,7 @@ public function testSimpleWhereNot() $model = new EloquentBuilderTestStub(); $this->mockConnectionForModel($model, 'SQLite'); $query = $model->newQuery()->whereNot('name', 'foo')->whereNot('name', '<>', 'bar'); - $this->assertEquals('select * from "table" where not "name" = ? and not "name" <> ?', $query->toSql()); + $this->assertSame('select * from "table" where not "name" = ? and not "name" <> ?', $query->toSql()); $this->assertEquals(['foo', 'bar'], $query->getBindings()); } @@ -1137,7 +1137,7 @@ public function testSimpleOrWhereNot() $model = new EloquentBuilderTestStub(); $this->mockConnectionForModel($model, 'SQLite'); $query = $model->newQuery()->orWhereNot('name', 'foo')->orWhereNot('name', '<>', 'bar'); - $this->assertEquals('select * from "table" where not "name" = ? or not "name" <> ?', $query->toSql()); + $this->assertSame('select * from "table" where not "name" = ? or not "name" <> ?', $query->toSql()); $this->assertEquals(['foo', 'bar'], $query->getBindings()); } diff --git a/tests/Database/DatabaseEloquentCollectionTest.php b/tests/Database/DatabaseEloquentCollectionTest.php index cbc9ddc99dc1..5d5e526d3364 100755 --- a/tests/Database/DatabaseEloquentCollectionTest.php +++ b/tests/Database/DatabaseEloquentCollectionTest.php @@ -439,7 +439,7 @@ public function testCollectionIntersectWithNull() $c1 = new Collection([$one, $two, $three]); - $this->assertEquals([], $c1->intersect(null)->all()); + $this->assertSame([], $c1->intersect(null)->all()); } public function testCollectionIntersectsWithGivenCollection() @@ -543,7 +543,7 @@ public function testMakeVisibleRemovesHiddenFromEntireCollection() $c = new Collection([new TestEloquentCollectionModel]); $c = $c->makeVisible(['hidden']); - $this->assertEquals([], $c[0]->getHidden()); + $this->assertSame([], $c[0]->getHidden()); } public function testMergeHiddenAddsHiddenOnEntireCollection() @@ -602,7 +602,7 @@ public function testWithoutAppendsRemovesAppendsOnEntireCollection() { $this->seedData(); $c = EloquentAppendingTestUserModel::query()->get(); - $this->assertEquals('hello', $c->toArray()[0]['appended_field']); + $this->assertSame('hello', $c->toArray()[0]['appended_field']); $c = $c->withoutAppends(); $this->assertArrayNotHasKey('appended_field', $c->toArray()[0]); @@ -628,7 +628,7 @@ public function testMakeVisibleRemovesHiddenAndIncludesVisible() $c = new Collection([new TestEloquentCollectionModel]); $c = $c->makeVisible('hidden'); - $this->assertEquals([], $c[0]->getHidden()); + $this->assertSame([], $c[0]->getHidden()); $this->assertEquals(['visible', 'hidden'], $c[0]->getVisible()); } @@ -639,8 +639,8 @@ public function testMultiply() $c = new Collection([$a, $b]); - $this->assertEquals([], $c->multiply(-1)->all()); - $this->assertEquals([], $c->multiply(0)->all()); + $this->assertSame([], $c->multiply(-1)->all()); + $this->assertSame([], $c->multiply(0)->all()); $this->assertEquals([$a, $b], $c->multiply(1)->all()); @@ -704,7 +704,7 @@ public function getQueueableRelations() }, ]); - $this->assertEquals([], $c->getQueueableRelations()); + $this->assertSame([], $c->getQueueableRelations()); } public function testEmptyCollectionStayEmptyOnFresh() diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index d987b8faa813..d1bc9bd7f45f 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -185,7 +185,7 @@ public function test_expanded_closure_attribute_returning_a_factory_is_resolved( ]), ]); - $this->assertEquals('post-options', $post->user->options); + $this->assertSame('post-options', $post->user->options); } public function test_make_creates_unpersisted_model_instance() @@ -1031,56 +1031,56 @@ public function test_factory_model_has_many_relationship_has_pending_attributes( { FactoryTestUser::factory()->has(new FactoryTestPostFactory(), 'postsWithFooBarBazAsTitle')->create(); - $this->assertEquals('foo bar baz', FactoryTestPost::first()->title); + $this->assertSame('foo bar baz', FactoryTestPost::first()->title); } public function test_factory_model_has_many_relationship_has_pending_attributes_override() { FactoryTestUser::factory()->has((new FactoryTestPostFactory())->state(['title' => 'other title']), 'postsWithFooBarBazAsTitle')->create(); - $this->assertEquals('other title', FactoryTestPost::first()->title); + $this->assertSame('other title', FactoryTestPost::first()->title); } public function test_factory_model_has_one_relationship_has_pending_attributes() { FactoryTestUser::factory()->has(new FactoryTestPostFactory(), 'postWithFooBarBazAsTitle')->create(); - $this->assertEquals('foo bar baz', FactoryTestPost::first()->title); + $this->assertSame('foo bar baz', FactoryTestPost::first()->title); } public function test_factory_model_has_one_relationship_has_pending_attributes_override() { FactoryTestUser::factory()->has((new FactoryTestPostFactory())->state(['title' => 'other title']), 'postWithFooBarBazAsTitle')->create(); - $this->assertEquals('other title', FactoryTestPost::first()->title); + $this->assertSame('other title', FactoryTestPost::first()->title); } public function test_factory_model_belongs_to_many_relationship_has_pending_attributes() { FactoryTestUser::factory()->has(new FactoryTestRoleFactory(), 'rolesWithFooBarBazAsName')->create(); - $this->assertEquals('foo bar baz', FactoryTestRole::first()->name); + $this->assertSame('foo bar baz', FactoryTestRole::first()->name); } public function test_factory_model_belongs_to_many_relationship_has_pending_attributes_override() { FactoryTestUser::factory()->has((new FactoryTestRoleFactory())->state(['name' => 'other name']), 'rolesWithFooBarBazAsName')->create(); - $this->assertEquals('other name', FactoryTestRole::first()->name); + $this->assertSame('other name', FactoryTestRole::first()->name); } public function test_factory_model_morph_many_relationship_has_pending_attributes() { (new FactoryTestPostFactory())->has(new FactoryTestCommentFactory(), 'commentsWithFooBarBazAsBody')->create(); - $this->assertEquals('foo bar baz', FactoryTestComment::first()->body); + $this->assertSame('foo bar baz', FactoryTestComment::first()->body); } public function test_factory_model_morph_many_relationship_has_pending_attributes_override() { (new FactoryTestPostFactory())->has((new FactoryTestCommentFactory())->state(['body' => 'other body']), 'commentsWithFooBarBazAsBody')->create(); - $this->assertEquals('other body', FactoryTestComment::first()->body); + $this->assertSame('other body', FactoryTestComment::first()->body); } public function test_factory_can_insert() @@ -1107,9 +1107,9 @@ public function test_factory_can_insert_with_hidden() { (new FactoryTestUserFactory())->forEachSequence(['name' => Name::Taylor, 'options' => 'abc'])->insert(); $user = DB::table('users')->sole(); - $this->assertEquals('abc', $user->options); + $this->assertSame('abc', $user->options); $userModel = FactoryTestUser::query()->sole(); - $this->assertEquals('abc', $userModel->options); + $this->assertSame('abc', $userModel->options); } public function test_factory_can_insert_with_array_casts() diff --git a/tests/Database/DatabaseEloquentGlobalScopesTest.php b/tests/Database/DatabaseEloquentGlobalScopesTest.php index 62954c20b3da..fe4719cf071b 100644 --- a/tests/Database/DatabaseEloquentGlobalScopesTest.php +++ b/tests/Database/DatabaseEloquentGlobalScopesTest.php @@ -41,7 +41,7 @@ public function testGlobalScopeCanBeRemoved() $model = new EloquentGlobalScopesTestModel; $query = $model->newQuery()->withoutGlobalScope(ActiveScope::class); $this->assertSame('select * from "table"', $query->toSql()); - $this->assertEquals([], $query->getBindings()); + $this->assertSame([], $query->getBindings()); } public function testClassNameGlobalScopeIsApplied() @@ -97,7 +97,7 @@ public function testClosureGlobalScopeCanBeRemoved() $model = new EloquentClosureGlobalScopesTestModel; $query = $model->newQuery()->withoutGlobalScope('active_scope'); $this->assertSame('select * from "table" order by "name" asc', $query->toSql()); - $this->assertEquals([], $query->getBindings()); + $this->assertSame([], $query->getBindings()); } public function testGlobalScopeCanBeRemovedAfterTheQueryIsExecuted() @@ -109,7 +109,7 @@ public function testGlobalScopeCanBeRemovedAfterTheQueryIsExecuted() $query->withoutGlobalScope('active_scope'); $this->assertSame('select * from "table" order by "name" asc', $query->toSql()); - $this->assertEquals([], $query->getBindings()); + $this->assertSame([], $query->getBindings()); } public function testAllGlobalScopesCanBeRemoved() @@ -117,11 +117,11 @@ public function testAllGlobalScopesCanBeRemoved() $model = new EloquentClosureGlobalScopesTestModel; $query = $model->newQuery()->withoutGlobalScopes(); $this->assertSame('select * from "table"', $query->toSql()); - $this->assertEquals([], $query->getBindings()); + $this->assertSame([], $query->getBindings()); $query = EloquentClosureGlobalScopesTestModel::withoutGlobalScopes(); $this->assertSame('select * from "table"', $query->toSql()); - $this->assertEquals([], $query->getBindings()); + $this->assertSame([], $query->getBindings()); } public function testAllGlobalScopesCanBeRemovedExceptSpecified() diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 5560e2fa2db9..ab1c809914c3 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -2588,7 +2588,7 @@ public function testCanFillAndInsert() $this->assertNull($users[0]->birthday); $this->assertInstanceOf(\DateTime::class, $users[1]->birthday); $this->assertInstanceOf(\DateTime::class, $users[2]->birthday); - $this->assertEquals('1987-11-01', $users[2]->birthday->format('Y-m-d')); + $this->assertSame('1987-11-01', $users[2]->birthday->format('Y-m-d')); DB::flushQueryLog(); diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 84470778a14f..40f3f625ef4d 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -1459,7 +1459,7 @@ public function testToArray() $this->assertSame('boom', $array['names'][1]['bam']); $this->assertSame('abby', $array['partner']['name']); $this->assertNull($array['group']); - $this->assertEquals([], $array['multi']); + $this->assertSame([], $array['multi']); $this->assertFalse(isset($array['password'])); $model->setAppends(['appendable']); @@ -1814,7 +1814,7 @@ public function testUnderscorePropertiesAreNotFilled() { $model = new EloquentModelStub; $model->fill(['_method' => 'PUT']); - $this->assertEquals([], $model->getAttributes()); + $this->assertSame([], $model->getAttributes()); } public function testGuarded() @@ -2561,10 +2561,10 @@ public function testAppendingOfAttributes() $this->assertFalse($model->hasAppended('not_appended')); $model->setHidden(['is_admin', 'camelCased', 'StudlyCased']); - $this->assertEquals([], $model->toArray()); + $this->assertSame([], $model->toArray()); $model->setVisible([]); - $this->assertEquals([], $model->toArray()); + $this->assertSame([], $model->toArray()); } public function testMergeAppendsMergesAppends() @@ -2796,7 +2796,7 @@ public function testIncrementEachWithExtraColumnsOnExistingModel() $this->assertEquals(1, $result); $this->assertEquals(7, $model->foo); - $this->assertEquals('test', $model->category); + $this->assertSame('test', $model->category); } public function testIncrementEachFiresModelEvents() @@ -3138,8 +3138,8 @@ public function testMergeCastsMergesCastsUsingArrays() $this->assertCount($castCount + 2, $model->getCasts()); $this->assertArrayHasKey('foo', $model->getCasts()); - $this->assertEquals('MyClass:myArgumentA', $model->getCasts()['foo']); - $this->assertEquals('MyClass:myArgumentA,myArgumentB', $model->getCasts()['bar']); + $this->assertSame('MyClass:myArgumentA', $model->getCasts()['foo']); + $this->assertSame('MyClass:myArgumentA,myArgumentB', $model->getCasts()['bar']); } public function testUnsetCastAttributes() @@ -3356,8 +3356,8 @@ public function testThrowsWhenAccessingMissingAttributesWhichArePrimitiveCasts() $this->assertInstanceOf(Address::class, $model->address); $this->assertEquals(1, $model->id); - $this->assertEquals('ok', $model->this_is_fine); - $this->assertEquals('ok', $model->this_is_also_fine); + $this->assertSame('ok', $model->this_is_fine); + $this->assertSame('ok', $model->this_is_also_fine); // Primitive castables, enum castable $expectedExceptionCount = count($primitiveCasts) + 1; @@ -3388,7 +3388,7 @@ public function testUsesOverriddenHandlerWhenAccessingMissingAttributes() $model->this_attribute_does_not_exist; $this->assertInstanceOf(EloquentModelStub::class, $callbackModel); - $this->assertEquals('this_attribute_does_not_exist', $callbackKey); + $this->assertSame('this_attribute_does_not_exist', $callbackKey); Model::preventAccessingMissingAttributes($originalMode); Model::handleMissingAttributeViolationUsing(null); @@ -3553,8 +3553,8 @@ public function testGetOriginalCastsAttributes() $this->assertEquals(2, $model->getAttribute('intAttribute')); $this->assertIsFloat($model->getOriginal('floatAttribute')); - $this->assertEquals(0.1234, $model->getOriginal('floatAttribute')); - $this->assertEquals(0.443, $model->floatAttribute); + $this->assertSame(0.1234, $model->getOriginal('floatAttribute')); + $this->assertSame(0.443, $model->floatAttribute); $this->assertIsString($model->getOriginal('stringAttribute')); $this->assertSame('432', $model->getOriginal('stringAttribute')); @@ -3630,7 +3630,7 @@ public function testUsingStringableObjectCastUsesStringRepresentation() { $model = new EloquentModelCastingStub; - $this->assertEquals('int', $model->getCasts()['castStringableObject']); + $this->assertSame('int', $model->getCasts()['castStringableObject']); } public function testMergeingStringableObjectCastUSesStringRepresentation() @@ -3642,7 +3642,7 @@ public function testMergeingStringableObjectCastUSesStringRepresentation() 'something' => $stringable, ]); - $this->assertEquals('test', $model->getCasts()['something']); + $this->assertSame('test', $model->getCasts()['something']); } public function testUsingPlainObjectAsCastThrowsException() @@ -3688,8 +3688,8 @@ public function testDiscardChangesWithCasts() $model->address_line_one = '123 Main Street'; - $this->assertEquals('123 Main Street', $model->address->lineOne); - $this->assertEquals('123 MAIN STREET', $model->address_in_caps); + $this->assertSame('123 Main Street', $model->address->lineOne); + $this->assertSame('123 MAIN STREET', $model->address_in_caps); $model->discardChanges(); @@ -3805,7 +3805,7 @@ public function testUseFactoryAttribute() $this->assertInstanceOf(EloquentModelWithUseFactoryAttributeFactory::class, $model::factory()); $this->assertInstanceOf(EloquentModelWithUseFactoryAttributeFactory::class, $model::newFactory()); $this->assertEquals(EloquentModelWithUseFactoryAttribute::class, $factory->modelName()); - $this->assertEquals('test name', $instance->name); // Small smoke test to ensure the factory is working + $this->assertSame('test name', $instance->name); // Small smoke test to ensure the factory is working } public function testNestedModelBootingIsDisallowed() diff --git a/tests/Database/DatabaseEloquentPivotTest.php b/tests/Database/DatabaseEloquentPivotTest.php index ee13f58acefa..6af940ae7666 100755 --- a/tests/Database/DatabaseEloquentPivotTest.php +++ b/tests/Database/DatabaseEloquentPivotTest.php @@ -69,7 +69,7 @@ public function testPropertiesUnchangedAreNotDirty() $parent->shouldReceive('getConnectionName')->once()->andReturn('connection'); $pivot = Pivot::fromAttributes($parent, ['foo' => 'bar', 'shimy' => 'shake'], 'table', true); - $this->assertEquals([], $pivot->getDirty()); + $this->assertSame([], $pivot->getDirty()); } public function testPropertiesChangedAreDirty() diff --git a/tests/Database/DatabaseMigrationCreatorTest.php b/tests/Database/DatabaseMigrationCreatorTest.php index bf9a20420b9a..72cedd763979 100755 --- a/tests/Database/DatabaseMigrationCreatorTest.php +++ b/tests/Database/DatabaseMigrationCreatorTest.php @@ -47,7 +47,7 @@ public function testBasicCreateMethodCallsPostCreateHooks() $creator->create('create_bar', 'foo', $table); $this->assertEquals($_SERVER['__migration.creator.table'], $table); - $this->assertEquals('foo/foo_create_bar.php', $_SERVER['__migration.creator.path']); + $this->assertSame('foo/foo_create_bar.php', $_SERVER['__migration.creator.path']); unset($_SERVER['__migration.creator.table'], $_SERVER['__migration.creator.path']); } diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index f8a927e184a7..8b3888fb0097 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -418,15 +418,15 @@ public function testDateBasedWheresExpressionIsNotBound() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereDay('created_at', new Raw('NOW()')); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereMonth('created_at', new Raw('NOW()')); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereYear('created_at', new Raw('NOW()')); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testWhereDateMySql() @@ -571,7 +571,7 @@ public function testWhereTimeSqlServer() $builder = $this->getSqlServerBuilder(); $builder->select('*')->from('users')->whereTime('created_at', new Raw('NOW()')); $this->assertSame('select * from [users] where cast([created_at] as time) = NOW()', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testOrWhereTimeMySql() @@ -1135,7 +1135,7 @@ public function testWhereBetweens() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereBetween('id', [new Raw(1), new Raw(2)]); $this->assertSame('select * from "users" where "id" between 1 and 2', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $period = Carbon::now()->startOfDay()->toPeriod(Carbon::now()->addDay()->startOfDay()); @@ -1248,17 +1248,17 @@ public function testWhereBetweenColumns() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereBetweenColumns('id', ['users.created_at', 'users.updated_at']); $this->assertSame('select * from "users" where "id" between "users"."created_at" and "users"."updated_at"', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNotBetweenColumns('id', ['created_at', 'updated_at']); $this->assertSame('select * from "users" where "id" not between "created_at" and "updated_at"', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereBetweenColumns('id', [new Raw(1), new Raw(2)]); $this->assertSame('select * from "users" where "id" between 1 and 2', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $subqueryBuilder = $this->getBuilder(); $subqueryBuilder->select('created_at')->from('posts')->where('status', 'published')->orderByDesc('created_at')->limit(1); @@ -1501,7 +1501,7 @@ public function testEmptyWhereIns() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereIn('id', []); $this->assertSame('select * from "users" where 0 = 1', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereIn('id', []); @@ -1514,7 +1514,7 @@ public function testEmptyWhereNotIns() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNotIn('id', []); $this->assertSame('select * from "users" where 1 = 1', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereNotIn('id', []); @@ -1529,7 +1529,7 @@ public function testWhereIntegerInRaw() '1a', 2, Bar::FOO, ]); $this->assertSame('select * from "users" where "id" in (1, 2, 5)', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereIntegerInRaw('id', [ @@ -1539,7 +1539,7 @@ public function testWhereIntegerInRaw() ['id' => Bar::FOO], ]); $this->assertSame('select * from "users" where "id" in (1, 2, 3, 5)', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testOrWhereIntegerInRaw() @@ -1555,7 +1555,7 @@ public function testWhereIntegerNotInRaw() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereIntegerNotInRaw('id', ['1a', 2]); $this->assertSame('select * from "users" where "id" not in (1, 2)', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testOrWhereIntegerNotInRaw() @@ -1571,7 +1571,7 @@ public function testEmptyWhereIntegerInRaw() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereIntegerInRaw('id', []); $this->assertSame('select * from "users" where 0 = 1', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testEmptyWhereIntegerNotInRaw() @@ -1579,7 +1579,7 @@ public function testEmptyWhereIntegerNotInRaw() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereIntegerNotInRaw('id', []); $this->assertSame('select * from "users" where 1 = 1', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testBasicWhereColumn() @@ -1587,12 +1587,12 @@ public function testBasicWhereColumn() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn('first_name', 'last_name')->orWhereColumn('first_name', 'middle_name'); $this->assertSame('select * from "users" where "first_name" = "last_name" or "first_name" = "middle_name"', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn('updated_at', '>', 'created_at'); $this->assertSame('select * from "users" where "updated_at" > "created_at"', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testArrayWhereColumn() @@ -1605,7 +1605,7 @@ public function testArrayWhereColumn() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn($conditions); $this->assertSame('select * from "users" where ("first_name" = "last_name" and "updated_at" > "created_at")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testWhereFulltextMySql() @@ -2072,7 +2072,7 @@ public function testBasicWhereNulls() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNull('id'); $this->assertSame('select * from "users" where "id" is null', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereNull('id'); @@ -2085,7 +2085,7 @@ public function testBasicWhereNullExpressionsMysql() $builder = $this->getMysqlBuilder(); $builder->select('*')->from('users')->whereNull(new Raw('id')); $this->assertSame('select * from `users` where id is null', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getMysqlBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereNull(new Raw('id')); @@ -2126,7 +2126,7 @@ public function testArrayWhereNulls() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNull(['id', 'expires_at']); $this->assertSame('select * from "users" where "id" is null and "expires_at" is null', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereNull(['id', 'expires_at']); @@ -2139,7 +2139,7 @@ public function testBasicWhereNotNulls() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNotNull('id'); $this->assertSame('select * from "users" where "id" is not null', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '>', 1)->orWhereNotNull('id'); @@ -2152,7 +2152,7 @@ public function testArrayWhereNotNulls() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNotNull(['id', 'expires_at']); $this->assertSame('select * from "users" where "id" is not null and "expires_at" is not null', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '>', 1)->orWhereNotNull(['id', 'expires_at']); @@ -2413,7 +2413,7 @@ public function testReorder() $builder->select('*')->from('users')->orderByRaw('?', [true]); $this->assertEquals([true], $builder->getBindings()); $builder->reorder(); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testOrderBySubQueries() @@ -2943,49 +2943,49 @@ public function testWhereWithArrayConditions() $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn([['foo', '_foo'], ['bar', '_bar']]); $this->assertSame('select * from "users" where ("foo" = "_foo" and "bar" = "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn([['foo', '_foo'], ['bar', '_bar']], boolean: 'or'); $this->assertSame('select * from "users" where ("foo" = "_foo" or "bar" = "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn([['foo', '_foo'], ['bar', '_bar']], boolean: 'and'); $this->assertSame('select * from "users" where ("foo" = "_foo" and "bar" = "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn(['foo' => '_foo', 'bar' => '_bar']); $this->assertSame('select * from "users" where ("foo" = "_foo" and "bar" = "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn(['foo' => '_foo', 'bar' => '_bar'], boolean: 'or'); $this->assertSame('select * from "users" where ("foo" = "_foo" or "bar" = "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn(['foo' => '_foo', 'bar' => '_bar'], boolean: 'and'); $this->assertSame('select * from "users" where ("foo" = "_foo" and "bar" = "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); // whereColumn(col1, <, col2) $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn([['foo', '_foo'], ['bar', '<', '_bar']]); $this->assertSame('select * from "users" where ("foo" = "_foo" and "bar" < "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn([['foo', '_foo'], ['bar', '<', '_bar']], boolean: 'or'); $this->assertSame('select * from "users" where ("foo" = "_foo" or "bar" < "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereColumn([['foo', '_foo'], ['bar', '<', '_bar']], boolean: 'and'); $this->assertSame('select * from "users" where ("foo" = "_foo" and "bar" < "_bar")', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); // whereAll([...keys], value) @@ -5866,7 +5866,7 @@ public function testSubSelectResetBindings() $builder->select('*'); $this->assertSame('select * from "one"', $builder->toSql()); - $this->assertEquals([], $builder->getBindings()); + $this->assertSame([], $builder->getBindings()); } public function testSelectExpression() @@ -6297,7 +6297,7 @@ public function testCursorPaginate() $results = collect([['test' => 'foo'], ['test' => 'bar']]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select * from "foobar" where ("test" > ?) order by "test" asc limit 17', $builder->toSql()); $this->assertEquals(['bar'], $builder->bindings['where']); @@ -6335,7 +6335,7 @@ public function testCursorPaginateMultipleOrderColumns() $results = collect([['test' => 'foo', 'another' => 1], ['test' => 'bar', 'another' => 2]]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select * from "foobar" where ("test" > ? or ("test" = ? and ("another" > ?))) order by "test" asc, "another" asc limit 17', $builder->toSql() ); @@ -6373,7 +6373,7 @@ public function testCursorPaginateWithDefaultArguments() $results = collect([['test' => 'foo'], ['test' => 'bar']]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select * from "foobar" where ("test" > ?) order by "test" asc limit 16', $builder->toSql()); $this->assertEquals(['bar'], $builder->bindings['where']); @@ -6443,7 +6443,7 @@ public function testCursorPaginateWithSpecificColumns() $results = collect([['id' => 3, 'name' => 'Taylor'], ['id' => 5, 'name' => 'Mohamed']]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select * from "foobar" where ("id" > ?) order by "id" asc limit 17', $builder->toSql()); $this->assertEquals([2], $builder->bindings['where']); @@ -6481,7 +6481,7 @@ public function testCursorPaginateWithMixedOrders() $results = collect([['foo' => 1, 'bar' => 2, 'baz' => 4], ['foo' => 1, 'bar' => 1, 'baz' => 1]]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select * from "foobar" where ("foo" > ? or ("foo" = ? and ("bar" < ? or ("bar" = ? and ("baz" > ?))))) order by "foo" asc, "bar" desc, "baz" asc limit 17', $builder->toSql() ); @@ -6519,7 +6519,7 @@ public function testCursorPaginateWithDynamicColumnInSelectRaw() $results = collect([['test' => 'foo'], ['test' => 'bar']]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select *, (CONCAT(firstname, \' \', lastname)) as test from "foobar" where ((CONCAT(firstname, \' \', lastname)) > ?) order by "test" asc limit 16', $builder->toSql()); $this->assertEquals(['bar'], $builder->bindings['where']); @@ -6560,7 +6560,7 @@ public function testCursorPaginateWithDynamicColumnWithCastInSelectRaw() $results = collect([['test' => 'foo'], ['test' => 'bar']]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select *, (CAST(CONCAT(firstname, \' \', lastname) as VARCHAR)) as test from "foobar" where ((CAST(CONCAT(firstname, \' \', lastname) as VARCHAR)) > ?) order by "test" asc limit 16', $builder->toSql()); $this->assertEquals(['bar'], $builder->bindings['where']); @@ -6601,7 +6601,7 @@ public function testCursorPaginateWithDynamicColumnInSelectSub() $results = collect([['test' => 'foo'], ['test' => 'bar']]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) { - $this->assertEquals( + $this->assertSame( 'select *, (CONCAT(firstname, \' \', lastname)) as "test" from "foobar" where ((CONCAT(firstname, \' \', lastname)) > ?) order by "test" asc limit 16', $builder->toSql()); $this->assertEquals(['bar'], $builder->bindings['where']); @@ -6651,7 +6651,7 @@ public function testCursorPaginateWithUnionWheres() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" > ?)) order by "created_at" asc limit 17', $builder->toSql()); $this->assertEquals([$ts], $builder->bindings['where']); @@ -6700,7 +6700,7 @@ public function testCursorPaginateWithMultipleUnionsAndMultipleWheres() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where "extra" = ? and ("created_at" > ?)) union (select "id", "created_at", \'podcast\' as type from "podcasts" where "extra" = ? and ("created_at" > ?)) order by "created_at" asc limit 17', $builder->toSql()); $this->assertEquals([$ts], $builder->bindings['where']); @@ -6750,7 +6750,7 @@ public function testCursorPaginateWithUnionMultipleWheresMultipleOrders() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "start_time" as "created_at", "type" from "videos" where "extra" = ? and ("id" > ? or ("id" = ? and ("start_time" < ? or ("start_time" = ? and ("type" > ?)))))) union (select "id", "created_at", "type" from "news" where "extra" = ? and ("id" > ? or ("id" = ? and ("start_time" < ? or ("start_time" = ? and ("type" > ?)))))) union (select "id", "created_at", "type" from "podcasts" where "extra" = ? and ("id" > ? or ("id" = ? and ("start_time" < ? or ("start_time" = ? and ("type" > ?)))))) order by "id" asc, "created_at" desc, "type" asc limit 17', $builder->toSql()); $this->assertEquals(['first', 1, 1, $ts, $ts, 'news'], $builder->bindings['where']); @@ -6797,7 +6797,7 @@ public function testCursorPaginateWithUnionWheresWithRawOrderExpression() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "is_published", "start_time" as "created_at", \'video\' as type from "videos" where "is_published" = ? and ("start_time" > ?)) union (select "id", "is_published", "created_at", \'news\' as type from "news" where "is_published" = ? and ("created_at" > ?)) order by case when (id = 3 and type="news" then 0 else 1 end), "created_at" asc limit 17', $builder->toSql()); $this->assertEquals([true, $ts], $builder->bindings['where']); @@ -6844,7 +6844,7 @@ public function testCursorPaginateWithUnionWheresReverseOrder() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ?)) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" < ?)) order by "created_at" desc limit 17', $builder->toSql()); $this->assertEquals([$ts], $builder->bindings['where']); @@ -6891,7 +6891,7 @@ public function testCursorPaginateWithUnionWheresMultipleOrders() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ? or ("start_time" = ? and ("id" > ?)))) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" < ? or ("created_at" = ? and ("id" > ?)))) order by "created_at" desc, "id" asc limit 17', $builder->toSql()); $this->assertEquals([$ts, $ts, 1], $builder->bindings['where']); @@ -6940,7 +6940,7 @@ public function testCursorPaginateWithUnionWheresAndAliassedOrderColumns() ]); $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { - $this->assertEquals( + $this->assertSame( '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" > ?)) union (select "id", "init_at" as "created_at", \'podcast\' as type from "podcasts" where ("init_at" > ?)) order by "created_at" asc limit 17', $builder->toSql()); $this->assertEquals([$ts], $builder->bindings['where']); @@ -7569,7 +7569,7 @@ public function testCloneWithoutBindings() $this->assertEquals([0 => 'foo'], $builder->getBindings()); $this->assertSame('select * from "users" order by "email" asc', $clone->toSql()); - $this->assertEquals([], $clone->getBindings()); + $this->assertSame([], $clone->getBindings()); } public function testToRawSql() diff --git a/tests/Database/DatabaseTransactionsManagerTest.php b/tests/Database/DatabaseTransactionsManagerTest.php index d536a231753c..9cd4b6d0105d 100755 --- a/tests/Database/DatabaseTransactionsManagerTest.php +++ b/tests/Database/DatabaseTransactionsManagerTest.php @@ -304,21 +304,21 @@ public function testStageTransactions() $pendingTransactions = $manager->getPendingTransactions(); $this->assertEquals(1, $pendingTransactions[0]->level); - $this->assertEquals('default', $pendingTransactions[0]->connection); + $this->assertSame('default', $pendingTransactions[0]->connection); $this->assertEquals(1, $pendingTransactions[1]->level); - $this->assertEquals('admin', $pendingTransactions[1]->connection); + $this->assertSame('admin', $pendingTransactions[1]->connection); $manager->stageTransactions('default', 1); $this->assertCount(1, $manager->getPendingTransactions()); $this->assertCount(1, $manager->getCommittedTransactions()); - $this->assertEquals('default', $manager->getCommittedTransactions()[0]->connection); + $this->assertSame('default', $manager->getCommittedTransactions()[0]->connection); $manager->stageTransactions('admin', 1); $this->assertCount(0, $manager->getPendingTransactions()); $this->assertCount(2, $manager->getCommittedTransactions()); - $this->assertEquals('admin', $manager->getCommittedTransactions()[1]->connection); + $this->assertSame('admin', $manager->getCommittedTransactions()[1]->connection); } public function testStageTransactionsOnlyStagesTheTransactionsAtOrAboveTheGivenLevel() diff --git a/tests/Database/EloquentModelCustomCastingTest.php b/tests/Database/EloquentModelCustomCastingTest.php index 07fff202d7e4..701f8b240b55 100644 --- a/tests/Database/EloquentModelCustomCastingTest.php +++ b/tests/Database/EloquentModelCustomCastingTest.php @@ -186,10 +186,10 @@ public function testModelWithCustomCastsWorkWithCustomIncrementDecrement() $model->save(); $this->assertInstanceOf(Euro::class, $model->amount); - $this->assertEquals('2', $model->amount->value); + $this->assertSame('2', $model->amount->value); $model->increment('amount', new Euro('1')); - $this->assertEquals('3.00', $model->amount->value); + $this->assertSame('3.00', $model->amount->value); } public function testModelWithCustomCastsCompareFunction() diff --git a/tests/Events/EventsDispatcherTest.php b/tests/Events/EventsDispatcherTest.php index 0069ffe62603..8c2a873b9519 100755 --- a/tests/Events/EventsDispatcherTest.php +++ b/tests/Events/EventsDispatcherTest.php @@ -47,7 +47,7 @@ public function testDeferEventExecution() return 'callback_result'; }); - $this->assertEquals('callback_result', $result); + $this->assertSame('callback_result', $result); $this->assertSame('bar', $_SERVER['__event.test']); } @@ -196,7 +196,7 @@ public function testResponseWhenNoListenersAreSet() $d = new Dispatcher; $response = $d->dispatch('foo'); - $this->assertEquals([], $response); + $this->assertSame([], $response); $response = $d->dispatch('foo', [], true); $this->assertNull($response); @@ -605,7 +605,7 @@ public function testListenersObjectsCreationOrder() $d->listen(TestEvent::class, TestListener3::class); // Attaching events does not make any objects. - $this->assertEquals([], $_SERVER['__event.test']); + $this->assertSame([], $_SERVER['__event.test']); $d->dispatch(TestEvent::class); diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 141474aa56bc..ab14560c8d33 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -742,7 +742,7 @@ public function testPrefixesUrls() { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['url' => 'https://example.org/', 'prefix' => 'images']); - $this->assertEquals('https://example.org/images/picture.jpeg', $filesystemAdapter->url('picture.jpeg')); + $this->assertSame('https://example.org/images/picture.jpeg', $filesystemAdapter->url('picture.jpeg')); } public function testGetChecksum() @@ -750,8 +750,8 @@ public function testGetChecksum() $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $filesystemAdapter->write('path.txt', 'contents of file'); - $this->assertEquals('730bed78bccf58c2cfe44c29b71e5e6b', $filesystemAdapter->checksum('path.txt')); - $this->assertEquals('a5c3556d', $filesystemAdapter->checksum('path.txt', ['checksum_algo' => 'crc32'])); + $this->assertSame('730bed78bccf58c2cfe44c29b71e5e6b', $filesystemAdapter->checksum('path.txt')); + $this->assertSame('a5c3556d', $filesystemAdapter->checksum('path.txt', ['checksum_algo' => 'crc32'])); } public function testUsesRightSeperatorForS3AdapterWithoutDoublePrefixing() @@ -766,7 +766,7 @@ public function testUsesRightSeperatorForS3AdapterWithoutDoublePrefixing() ]); $path = $filesystemAdapter->path('different'); - $this->assertEquals('my-root/someprefix/different', $path); + $this->assertSame('my-root/someprefix/different', $path); } public function testTemporaryUploadUrlWithCustomCallback() diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 501db8a5bdcd..35b7dd17712a 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -57,7 +57,7 @@ public function testCanBuildReadOnlyDisks() file_put_contents(__DIR__.'/../../my-custom-path/path.txt', 'contents'); // read operations work - $this->assertEquals('contents', $disk->get('path.txt')); + $this->assertSame('contents', $disk->get('path.txt')); $this->assertEquals(['path.txt'], $disk->files()); // write operations fail @@ -95,7 +95,7 @@ public function testCanBuildScopedDisks() ]); $scoped->put('dirname/filename.txt', 'file content'); - $this->assertEquals('file content', $local->get('path-prefix/dirname/filename.txt')); + $this->assertSame('file content', $local->get('path-prefix/dirname/filename.txt')); $local->deleteDirectory('path-prefix'); } finally { rmdir(__DIR__.'/../../to-be-scoped'); @@ -127,7 +127,7 @@ public function testCanBuildScopedDiskFromScopedDisk() ]); $nestedScoped->put('dirname/filename.txt', 'file content'); - $this->assertEquals('file content', $root->get('scoped-from-root-prefix/nested-scoped-prefix/dirname/filename.txt')); + $this->assertSame('file content', $root->get('scoped-from-root-prefix/nested-scoped-prefix/dirname/filename.txt')); $root->deleteDirectory('scoped-from-root-prefix'); } finally { rmdir(__DIR__.'/../../root-to-be-scoped'); @@ -157,7 +157,7 @@ public function testCanBuildScopedDisksWithVisibility() $scoped->put('dirname/filename.txt', 'file content'); - $this->assertEquals('private', $scoped->getVisibility('dirname/filename.txt')); + $this->assertSame('private', $scoped->getVisibility('dirname/filename.txt')); } finally { unlink(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt'); rmdir(__DIR__.'/../../to-be-scoped/path-prefix/dirname'); @@ -209,7 +209,7 @@ public function testCanBuildInlineScopedDisks() $scoped->put('dirname/filename.txt', 'file content'); $this->assertTrue(is_dir(__DIR__.'/../../to-be-scoped/path-prefix')); - $this->assertEquals('file content', file_get_contents(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt')); + $this->assertSame('file content', file_get_contents(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt')); } finally { unlink(__DIR__.'/../../to-be-scoped/path-prefix/dirname/filename.txt'); rmdir(__DIR__.'/../../to-be-scoped/path-prefix/dirname'); diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index d6e8a736313a..8f0be49a5852 100755 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -725,6 +725,6 @@ public function testDirectoryOperationsWithSubdirectories() $allFiles = $files->allFiles($dirPath); $this->assertCount(1, $allFiles); - $this->assertEquals('test.txt', $allFiles[0]->getFilename()); + $this->assertSame('test.txt', $allFiles[0]->getFilename()); } } diff --git a/tests/Foundation/Configuration/MiddlewareTest.php b/tests/Foundation/Configuration/MiddlewareTest.php index 240c52dc1f54..ecf0c0e85b00 100644 --- a/tests/Foundation/Configuration/MiddlewareTest.php +++ b/tests/Foundation/Configuration/MiddlewareTest.php @@ -155,7 +155,7 @@ public function testTrustProxies() ], $method->invoke($middleware)); $configuration->trustProxies(at: '*'); - $this->assertEquals('*', $method->invoke($middleware)); + $this->assertSame('*', $method->invoke($middleware)); $configuration->trustProxies(at: [ '192.168.1.3', @@ -240,10 +240,10 @@ protected function allSubdomainsOfApplicationUrl() $this->assertEquals(['^(.+\.)?laravel\.test$'], $middleware->hosts()); $configuration->trustHosts(at: [], subdomains: false); - $this->assertEquals([], $middleware->hosts()); + $this->assertSame([], $middleware->hosts()); $configuration->trustHosts(at: static fn () => [], subdomains: false); - $this->assertEquals([], $middleware->hosts()); + $this->assertSame([], $middleware->hosts()); } public function testEncryptCookies() diff --git a/tests/Foundation/Console/RouteListCommandTest.php b/tests/Foundation/Console/RouteListCommandTest.php index 3e1135944221..76dd9cb1207f 100644 --- a/tests/Foundation/Console/RouteListCommandTest.php +++ b/tests/Foundation/Console/RouteListCommandTest.php @@ -80,9 +80,9 @@ public function testSortRouteListAsc() $routes = json_decode($output, true); $this->assertCount(3, $routes); - $this->assertEquals('example', $routes[0]['uri']); - $this->assertEquals('example-group', $routes[1]['uri']); - $this->assertEquals('sub-example', $routes[2]['uri']); + $this->assertSame('example', $routes[0]['uri']); + $this->assertSame('example-group', $routes[1]['uri']); + $this->assertSame('sub-example', $routes[2]['uri']); foreach ($routes as $route) { $this->assertArrayHasKey('path', $route); @@ -98,9 +98,9 @@ public function testSortRouteListDesc() $routes = json_decode($output, true); $this->assertCount(3, $routes); - $this->assertEquals('sub-example', $routes[0]['uri']); - $this->assertEquals('example-group', $routes[1]['uri']); - $this->assertEquals('example', $routes[2]['uri']); + $this->assertSame('sub-example', $routes[0]['uri']); + $this->assertSame('example-group', $routes[1]['uri']); + $this->assertSame('example', $routes[2]['uri']); foreach ($routes as $route) { $this->assertArrayHasKey('path', $route); @@ -116,9 +116,9 @@ public function testSortRouteListDefault() $routes = json_decode($output, true); $this->assertCount(3, $routes); - $this->assertEquals('example', $routes[0]['uri']); - $this->assertEquals('example-group', $routes[1]['uri']); - $this->assertEquals('sub-example', $routes[2]['uri']); + $this->assertSame('example', $routes[0]['uri']); + $this->assertSame('example-group', $routes[1]['uri']); + $this->assertSame('sub-example', $routes[2]['uri']); foreach ($routes as $route) { $this->assertArrayHasKey('path', $route); @@ -134,9 +134,9 @@ public function testSortRouteListPrecedence() $routes = json_decode($output, true); $this->assertCount(3, $routes); - $this->assertEquals('example', $routes[0]['uri']); - $this->assertEquals('sub-example', $routes[1]['uri']); - $this->assertEquals('example-group', $routes[2]['uri']); + $this->assertSame('example', $routes[0]['uri']); + $this->assertSame('sub-example', $routes[1]['uri']); + $this->assertSame('example-group', $routes[2]['uri']); foreach ($routes as $route) { $this->assertArrayHasKey('path', $route); @@ -216,11 +216,11 @@ public function testMiddlewareGroupsExpandCorrectlySortedIfVeryVerbose() $routes = json_decode($output, true); $this->assertCount(3, $routes); - $this->assertEquals('example', $routes[0]['uri']); + $this->assertSame('example', $routes[0]['uri']); $this->assertEquals(['exampleMiddleware'], $routes[0]['middleware']); - $this->assertEquals('example-group', $routes[1]['uri']); + $this->assertSame('example-group', $routes[1]['uri']); $this->assertEquals(['Middleware 5', 'Middleware 1', 'Middleware 4', 'Middleware 2', 'Middleware 3'], $routes[1]['middleware']); - $this->assertEquals('sub-example', $routes[2]['uri']); + $this->assertSame('sub-example', $routes[2]['uri']); $this->assertEquals(['exampleMiddleware'], $routes[2]['middleware']); } @@ -232,7 +232,7 @@ public function testFilterByMiddleware() $routes = json_decode($output, true); $this->assertCount(1, $routes); - $this->assertEquals('example-group', $routes[0]['uri']); + $this->assertSame('example-group', $routes[0]['uri']); $this->assertEquals(['web', 'auth'], $routes[0]['middleware']); $this->assertStringContainsString('RouteListCommandTest.php:', $routes[0]['path']); } diff --git a/tests/Foundation/Exceptions/Renderer/ListenerTest.php b/tests/Foundation/Exceptions/Renderer/ListenerTest.php index cef5c0c135f1..9eb238e1b5db 100644 --- a/tests/Foundation/Exceptions/Renderer/ListenerTest.php +++ b/tests/Foundation/Exceptions/Renderer/ListenerTest.php @@ -34,9 +34,9 @@ public function test_queries_returns_expected_shape_after_query_executed() $this->assertArrayHasKey('sql', $query); $this->assertArrayHasKey('bindings', $query); - $this->assertEquals('testing', $query['connectionName']); - $this->assertEquals(5.2, $query['time']); - $this->assertEquals('select * from users where id = ?', $query['sql']); + $this->assertSame('testing', $query['connectionName']); + $this->assertSame(5.2, $query['time']); + $this->assertSame('select * from users where id = ?', $query['sql']); $this->assertEquals(['foo'], $query['bindings']); } @@ -55,8 +55,8 @@ public function test_listener_caps_at_100_queries() } $this->assertCount(100, $listener->queries()); - $this->assertEquals('select 0', $listener->queries()[0]['sql']); - $this->assertEquals('select 99', $listener->queries()[99]['sql']); + $this->assertSame('select 0', $listener->queries()[0]['sql']); + $this->assertSame('select 99', $listener->queries()[99]['sql']); } public function test_large_sql_is_truncated() @@ -144,7 +144,7 @@ public function test_query_with_no_bindings_is_unchanged() new QueryExecuted('select count(*) from users', [], 1.0, $connection) ); - $this->assertEquals('select count(*) from users', $listener->queries()[0]['sql']); + $this->assertSame('select count(*) from users', $listener->queries()[0]['sql']); $this->assertEmpty($listener->queries()[0]['bindings']); } diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index e96de23a5200..56fe0c50e1ca 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -229,7 +229,7 @@ public function testRequestCanPassWithoutRulesMethod() $request->validateResolved(); - $this->assertEquals([], $request->all()); + $this->assertSame([], $request->all()); } public function testRequestWithGetRules() diff --git a/tests/Foundation/FoundationInteractsWithDatabaseTest.php b/tests/Foundation/FoundationInteractsWithDatabaseTest.php index 9253952fda0c..a0a39e0eb924 100644 --- a/tests/Foundation/FoundationInteractsWithDatabaseTest.php +++ b/tests/Foundation/FoundationInteractsWithDatabaseTest.php @@ -372,7 +372,7 @@ public function testGetTableNameFromModel() $this->assertEquals($this->table, $this->getTable(ProductStub::class)); $this->assertEquals($this->table, $this->getTable(new ProductStub)); $this->assertEquals($this->table, $this->getTable($this->table)); - $this->assertEquals('all_products', $this->getTable((new ProductStub)->setTable('all_products'))); + $this->assertSame('all_products', $this->getTable((new ProductStub)->setTable('all_products'))); } public function testGetTableConnectionNameFromModel() @@ -384,8 +384,8 @@ public function testGetTableConnectionNameFromModel() public function testGetTableCustomizedDeletedAtColumnName() { - $this->assertEquals('trashed_at', $this->getDeletedAtColumn(CustomProductStub::class)); - $this->assertEquals('trashed_at', $this->getDeletedAtColumn(new CustomProductStub())); + $this->assertSame('trashed_at', $this->getDeletedAtColumn(CustomProductStub::class)); + $this->assertSame('trashed_at', $this->getDeletedAtColumn(new CustomProductStub())); } public function testExpectsDatabaseQueryCount() diff --git a/tests/Foundation/Http/KernelTest.php b/tests/Foundation/Http/KernelTest.php index 8df127e89298..57e53cee3f4c 100644 --- a/tests/Foundation/Http/KernelTest.php +++ b/tests/Foundation/Http/KernelTest.php @@ -17,14 +17,14 @@ public function testGetMiddlewareGroups() { $kernel = new Kernel($this->getApplication(), $this->getRouter()); - $this->assertEquals([], $kernel->getMiddlewareGroups()); + $this->assertSame([], $kernel->getMiddlewareGroups()); } public function testGetRouteMiddleware() { $kernel = new Kernel($this->getApplication(), $this->getRouter()); - $this->assertEquals([], $kernel->getRouteMiddleware()); + $this->assertSame([], $kernel->getRouteMiddleware()); } public function testGetMiddlewarePriority() diff --git a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php index 3a5332fdf20e..86340e27f347 100644 --- a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php +++ b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php @@ -34,7 +34,7 @@ public function testFromRemoveHeader() { $this->withHeader('name', 'Milwad')->from('previous/url'); - $this->assertEquals('Milwad', $this->defaultHeaders['name']); + $this->assertSame('Milwad', $this->defaultHeaders['name']); $this->withoutHeader('name')->from('previous/url'); @@ -48,8 +48,8 @@ public function testFromRemoveHeaders() 'foo' => 'bar', ])->from('previous/url'); - $this->assertEquals('Milwad', $this->defaultHeaders['name']); - $this->assertEquals('bar', $this->defaultHeaders['foo']); + $this->assertSame('Milwad', $this->defaultHeaders['name']); + $this->assertSame('bar', $this->defaultHeaders['foo']); $this->withoutHeaders(['name', 'foo'])->from('previous/url'); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index d8963e1f1155..db86f7ab8861 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -1421,12 +1421,12 @@ public function testRequestLevelTruncationLevelOnRequestException() } // Ensure the exception message is truncated according to the request level truncation setting. - $this->assertEquals("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); + $this->assertSame("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); $exception->report(); // Ensure that the truncation level is not changed when reporting the exception. - $this->assertEquals("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); + $this->assertSame("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); $this->assertEquals(60, RequestException::$truncateAt); } @@ -1449,7 +1449,7 @@ public function testNoTruncationOnRequestLevel() $exception->report(); - $this->assertEquals("HTTP request returned status code 403:\nHTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\n\r\n[\"error\"]\n", $exception->getMessage()); + $this->assertSame("HTTP request returned status code 403:\nHTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\n\r\n[\"error\"]\n", $exception->getMessage()); $this->assertEquals(60, RequestException::$truncateAt); } @@ -1471,7 +1471,7 @@ public function testRequestExceptionDoesNotTruncateButRequestDoes() $exception->report(); - $this->assertEquals("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); + $this->assertSame("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); $this->assertFalse(RequestException::$truncateAt); } @@ -1488,7 +1488,7 @@ public function testAsyncRequestExceptionsRespectRequestTruncation() $exception->report(); $this->assertInstanceOf(RequestException::class, $exception); - $this->assertEquals("HTTP request returned status code 403:\n[\"er (truncated...)\n", $exception->getMessage()); + $this->assertSame("HTTP request returned status code 403:\n[\"er (truncated...)\n", $exception->getMessage()); $this->assertFalse(RequestException::$truncateAt); } @@ -2402,7 +2402,7 @@ public function testExceptionThrownInRetryCallbackWithoutRetrying() $this->assertNotNull($exception); $this->assertInstanceOf(Exception::class, $exception); - $this->assertEquals('Foo bar', $exception->getMessage()); + $this->assertSame('Foo bar', $exception->getMessage()); $this->factory->assertSentCount(1); } @@ -2427,7 +2427,7 @@ public function testExceptionThrownInRetryCallbackWithoutRetryingWithBackoffArra $this->assertNotNull($exception); $this->assertInstanceOf(Exception::class, $exception); - $this->assertEquals('Foo bar', $exception->getMessage()); + $this->assertSame('Foo bar', $exception->getMessage()); $this->factory->assertSentCount(1); } @@ -2600,7 +2600,7 @@ public function testExceptionThrownInRetryCallbackIsReturnedWithoutRetryingInPoo $this->assertNotNull($exception); $this->assertInstanceOf(Exception::class, $exception); - $this->assertEquals('Foo bar', $exception->getMessage()); + $this->assertSame('Foo bar', $exception->getMessage()); $this->factory->assertSentCount(1); } @@ -3693,7 +3693,7 @@ public function testItCanEnforceFakingInThePool() }); $this->assertInstanceOf(StrayRequestException::class, $exception); - $this->assertEquals('Attempted request to [https://laravel.com] without a matching fake.', $exception->getMessage()); + $this->assertSame('Attempted request to [https://laravel.com] without a matching fake.', $exception->getMessage()); } public function testPreventingStrayRequests() @@ -4411,7 +4411,7 @@ public function testAfterResponseWithAsync() $this->assertInstanceOf(Request::class, $requestReceived); $this->assertSame('http://200.com', (string) $requestReceived->url()); $this->assertInstanceOf(TestResponse::class, $o['401-response']); - $this->assertEquals('different', $o['401-response']->body()); + $this->assertSame('different', $o['401-response']->body()); $this->assertInstanceOf(RequestException::class, $o['401-throwing']); $this->assertInstanceOf(TestResponse::class, $o['401-throwing']->response); } diff --git a/tests/Http/HttpRedirectResponseTest.php b/tests/Http/HttpRedirectResponseTest.php index eeeabb6204dd..7f5cfda514db 100755 --- a/tests/Http/HttpRedirectResponseTest.php +++ b/tests/Http/HttpRedirectResponseTest.php @@ -78,8 +78,8 @@ public function testWithCookies() new Cookie('name', 'milwad'), ]); - $this->assertEquals('name', $response->headers->getCookies()[0]->getName()); - $this->assertEquals('milwad', $response->headers->getCookies()[0]->getValue()); + $this->assertSame('name', $response->headers->getCookies()[0]->getName()); + $this->assertSame('milwad', $response->headers->getCookies()[0]->getValue()); } public function testOnlyInputOnRedirect() diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 0013113c3bc3..fc7e144e9859 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -1053,15 +1053,15 @@ public function testOnlyMethod() $request = Request::create('/', 'GET', ['developer' => ['name' => 'Taylor', 'age' => null]]); $this->assertEquals(['developer' => ['name' => 'Taylor']], $request->only('developer.name', 'developer.skills')); $this->assertEquals(['developer' => ['age' => null]], $request->only('developer.age')); - $this->assertEquals([], $request->only('developer.skills')); + $this->assertSame([], $request->only('developer.skills')); } public function testExceptMethod() { $request = Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 25]); $this->assertEquals(['name' => 'Taylor'], $request->except('age')); - $this->assertEquals([], $request->except('age', 'name')); - $this->assertEquals([], $request->except(['age', 'name'])); + $this->assertSame([], $request->except('age', 'name')); + $this->assertSame([], $request->except(['age', 'name'])); } public function testQueryMethod() @@ -1764,12 +1764,12 @@ public function testNonJsonRequestDoesntFillRequestBodyParams() $params = ['foo' => 'bar']; $getRequest = Request::create('/', 'GET', $params, [], [], []); - $this->assertEquals([], $getRequest->request->all()); + $this->assertSame([], $getRequest->request->all()); $this->assertEquals($getRequest->query->all(), $params); $postRequest = Request::create('/', 'POST', $params, [], [], []); $this->assertEquals($postRequest->request->all(), $params); - $this->assertEquals([], $postRequest->query->all()); + $this->assertSame([], $postRequest->query->all()); } /** diff --git a/tests/Http/Middleware/PreventRequestForgeryTest.php b/tests/Http/Middleware/PreventRequestForgeryTest.php index df54a6f2dae4..656443e05b5a 100644 --- a/tests/Http/Middleware/PreventRequestForgeryTest.php +++ b/tests/Http/Middleware/PreventRequestForgeryTest.php @@ -29,7 +29,7 @@ public function test_same_origin_header_passes() $response = $middleware->handle($request, fn () => new Response('OK')); - $this->assertEquals('OK', $response->getContent()); + $this->assertSame('OK', $response->getContent()); } public function test_same_site_header_rejected_by_default() @@ -51,7 +51,7 @@ public function test_same_site_header_passes_when_allowed() $response = $middleware->handle($request, fn () => new Response('OK')); - $this->assertEquals('OK', $response->getContent()); + $this->assertSame('OK', $response->getContent()); } public function test_cross_site_with_valid_token_passes() @@ -61,7 +61,7 @@ public function test_cross_site_with_valid_token_passes() $response = $middleware->handle($request, fn () => new Response('OK')); - $this->assertEquals('OK', $response->getContent()); + $this->assertSame('OK', $response->getContent()); } public function test_cross_site_without_token_fails() @@ -118,7 +118,7 @@ public function test_origin_only_mode_passes_same_origin() $response = $middleware->handle($request, fn () => new Response('OK')); - $this->assertEquals('OK', $response->getContent()); + $this->assertSame('OK', $response->getContent()); } protected function createRequest(array $server = [], ?string $token = null) diff --git a/tests/Http/Middleware/TrimStringsTest.php b/tests/Http/Middleware/TrimStringsTest.php index 7d37662ef1ab..1dce23c4b3aa 100644 --- a/tests/Http/Middleware/TrimStringsTest.php +++ b/tests/Http/Middleware/TrimStringsTest.php @@ -22,7 +22,7 @@ public function test_no_zero_width_space_character_returns_the_same_string() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title does not contain any zero-width space', $req->title); + $this->assertSame('This title does not contain any zero-width space', $req->title); }); } @@ -40,7 +40,7 @@ public function test_leading_zero_width_space_character_is_trimmed() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a zero-width space at the beginning', $req->title); + $this->assertSame('This title contains a zero-width space at the beginning', $req->title); }); } @@ -57,7 +57,7 @@ public function test_trim_strings_can_globally_ignore_certain_inputs() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals(' test title ', $req->globally_ignored_title); + $this->assertSame(' test title ', $req->globally_ignored_title); }); } @@ -75,7 +75,7 @@ public function test_trailing_zero_width_space_character_is_trimmed() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a zero-width space at the end', $req->title); + $this->assertSame('This title contains a zero-width space at the end', $req->title); }); } @@ -93,7 +93,7 @@ public function test_leading_zero_width_non_breakable_space_character_is_trimmed $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a zero-width non-breakable space at the beginning', $req->title); + $this->assertSame('This title contains a zero-width non-breakable space at the beginning', $req->title); }); } @@ -111,7 +111,7 @@ public function test_leading_multiple_zero_width_non_breakable_space_characters_ $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a zero-width non-breakable space at the beginning', $req->title); + $this->assertSame('This title contains a zero-width non-breakable space at the beginning', $req->title); }); } @@ -129,7 +129,7 @@ public function test_combination_of_leading_and_trailing_zero_width_non_breakabl $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a combination of zero-width non-breakable space and zero-width spaces characters at the beginning and the end', $req->title); + $this->assertSame('This title contains a combination of zero-width non-breakable space and zero-width spaces characters at the beginning and the end', $req->title); }); } @@ -147,7 +147,7 @@ public function test_leading_invisible_characters_are_trimmed() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a invisible character at the beginning', $req->title); + $this->assertSame('This title contains a invisible character at the beginning', $req->title); }); } @@ -165,7 +165,7 @@ public function test_trailing_invisible_characters_are_trimmed() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a invisible character at the end', $req->title); + $this->assertSame('This title contains a invisible character at the end', $req->title); }); } @@ -183,7 +183,7 @@ public function test_leading_multiple_invisible_characters_are_trimmed() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a invisible character at the beginning', $req->title); + $this->assertSame('This title contains a invisible character at the beginning', $req->title); }); } @@ -201,7 +201,7 @@ public function test_trailing_multiple_invisible_characters_are_trimmed() $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a invisible character at the end', $req->title); + $this->assertSame('This title contains a invisible character at the end', $req->title); }); } @@ -219,7 +219,7 @@ public function test_combination_of_leading_and_trailing_multiple_invisible_char $middleware = new TrimStrings; $middleware->handle($request, function ($req) { - $this->assertEquals('This title contains a combination of a invisible character at beginning and the end', $req->title); + $this->assertSame('This title contains a combination of a invisible character at beginning and the end', $req->title); }); } diff --git a/tests/Integration/Cache/RedisStoreTest.php b/tests/Integration/Cache/RedisStoreTest.php index 9859ff94ce66..1fb5bf6761b9 100644 --- a/tests/Integration/Cache/RedisStoreTest.php +++ b/tests/Integration/Cache/RedisStoreTest.php @@ -108,7 +108,7 @@ public function testTagsCanBeAccessed(string $cachePrefix) Cache::store('redis')->tags(['people', 'author'])->put('name', 'Sally', 5); Cache::store('redis')->tags(['people', 'author'])->put('age', 30, 5); - $this->assertEquals('Sally', Cache::store('redis')->tags(['people', 'author'])->get('name')); + $this->assertSame('Sally', Cache::store('redis')->tags(['people', 'author'])->get('name')); $this->assertEquals(30, Cache::store('redis')->tags(['people', 'author'])->get('age')); Cache::store('redis')->tags(['people', 'author'])->flush(); @@ -124,7 +124,7 @@ public function testTagEntriesCanBeStoredForever() Cache::store('redis')->tags(['people', 'author'])->forever('name', 'Sally'); Cache::store('redis')->tags(['people', 'author'])->forever('age', 30); - $this->assertEquals('Sally', Cache::store('redis')->tags(['people', 'author'])->get('name')); + $this->assertSame('Sally', Cache::store('redis')->tags(['people', 'author'])->get('name')); $this->assertEquals(30, Cache::store('redis')->tags(['people', 'author'])->get('age')); Cache::store('redis')->tags(['people', 'author'])->flush(); @@ -198,7 +198,7 @@ public function testTagsCanBeFlushedBySingleKey() Cache::store('redis')->tags(['artist'])->flush(); - $this->assertEquals('Sally', Cache::store('redis')->tags(['people', 'author'])->get('person-1')); + $this->assertSame('Sally', Cache::store('redis')->tags(['people', 'author'])->get('person-1')); $this->assertNull(Cache::store('redis')->tags(['people', 'artist'])->get('person-2')); $keyCount = Cache::store('redis')->connection()->keys('*'); @@ -240,7 +240,7 @@ public function testMultipleItemsCanBeSetAndRetrieved() 'norf' => null, ], $store->many(['foo', 'fizz', 'quz', 'norf'])); - $this->assertEquals([], $store->many([])); + $this->assertSame([], $store->many([])); } public function testPutManyCallsPutWhenClustered() @@ -285,9 +285,9 @@ public function testTagsCanBeFlushedWithLargeNumberOfKeys() Cache::store('redis')->tags($tags)->put("key:{$i}", "value:{$i}", 300); } - $this->assertEquals('value:1', Cache::store('redis')->tags($tags)->get('key:1')); - $this->assertEquals('value:2500', Cache::store('redis')->tags($tags)->get('key:2500')); - $this->assertEquals('value:5000', Cache::store('redis')->tags($tags)->get('key:5000')); + $this->assertSame('value:1', Cache::store('redis')->tags($tags)->get('key:1')); + $this->assertSame('value:2500', Cache::store('redis')->tags($tags)->get('key:2500')); + $this->assertSame('value:5000', Cache::store('redis')->tags($tags)->get('key:5000')); Cache::store('redis')->tags($tags)->flush(); diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index 19a75d9ba491..c58c50dbf55b 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -154,9 +154,9 @@ function () { }, ]); - $this->assertEquals('first', $first); - $this->assertEquals('second', $second); - $this->assertEquals('third', $third); + $this->assertSame('first', $first); + $this->assertSame('second', $second); + $this->assertSame('third', $third); } } diff --git a/tests/Integration/Concurrency/Console/InvokeSerializedClosureCommandTest.php b/tests/Integration/Concurrency/Console/InvokeSerializedClosureCommandTest.php index f538aacb8492..a26404ffabca 100644 --- a/tests/Integration/Concurrency/Console/InvokeSerializedClosureCommandTest.php +++ b/tests/Integration/Concurrency/Console/InvokeSerializedClosureCommandTest.php @@ -48,7 +48,7 @@ public function testItCanInvokeSerializedClosureFromArgument() // Verify the result $this->assertTrue($result['successful']); - $this->assertEquals('Hello, World!', unserialize($result['result'])); + $this->assertSame('Hello, World!', unserialize($result['result'])); } public function testItCanInvokeSerializedClosureFromEnvironment() @@ -71,7 +71,7 @@ public function testItCanInvokeSerializedClosureFromEnvironment() // Verify the result $this->assertTrue($result['successful']); - $this->assertEquals('From Environment', unserialize($result['result'])); + $this->assertSame('From Environment', unserialize($result['result'])); // Clean up unset($_SERVER['LARAVEL_INVOKABLE_CLOSURE']); @@ -112,8 +112,8 @@ public function testItHandlesExceptionsGracefully() // Verify the exception was caught $this->assertFalse($result['successful']); - $this->assertEquals('RuntimeException', $result['exception']); - $this->assertEquals('Test exception', $result['message']); + $this->assertSame('RuntimeException', $result['exception']); + $this->assertSame('Test exception', $result['message']); } public function testItHandlesCustomExceptionWithParameters() @@ -136,6 +136,6 @@ public function testItHandlesCustomExceptionWithParameters() // Verify the exception was caught and parameters were captured $this->assertFalse($result['successful']); $this->assertArrayHasKey('parameters', $result); - $this->assertEquals('Test param', $result['parameters']['customParam'] ?? null); + $this->assertSame('Test param', $result['parameters']['customParam'] ?? null); } } diff --git a/tests/Integration/Container/BuildableIntegrationTest.php b/tests/Integration/Container/BuildableIntegrationTest.php index 4fec6a7b611f..6f658ef179f7 100644 --- a/tests/Integration/Container/BuildableIntegrationTest.php +++ b/tests/Integration/Container/BuildableIntegrationTest.php @@ -26,9 +26,9 @@ public function test_build_method_can_resolve_itself_via_container(): void $config = $this->app->make(AolInstantMessengerConfig::class); $this->assertEquals(500, $config->awayMessageDuration); - $this->assertEquals('sad emo lyrics', $config->awayMessage); - $this->assertEquals('api-key', $config->apiKey); - $this->assertEquals('cosmastech', $config->userName); + $this->assertSame('sad emo lyrics', $config->awayMessage); + $this->assertSame('api-key', $config->apiKey); + $this->assertSame('cosmastech', $config->userName); config(['aim.away_message.duration' => 5]); diff --git a/tests/Integration/Container/ContextualAttributesBindingIntegrationTest.php b/tests/Integration/Container/ContextualAttributesBindingIntegrationTest.php index 084373f55f6c..f3e4caccc6c4 100644 --- a/tests/Integration/Container/ContextualAttributesBindingIntegrationTest.php +++ b/tests/Integration/Container/ContextualAttributesBindingIntegrationTest.php @@ -31,10 +31,10 @@ public function testLogAttributeCanSetName() $records = new Collection($testHandler->getRecords()); $this->assertCount(2, $records); - $this->assertEquals('hello', $records->firstWhere(function (LogRecord $record) { + $this->assertSame('hello', $records->firstWhere(function (LogRecord $record) { return $record->channel === 'testing'; })->message); - $this->assertEquals('bye', $records->firstWhere(function (LogRecord $record) { + $this->assertSame('bye', $records->firstWhere(function (LogRecord $record) { return $record->channel === 'look-ma-a-channel-name'; })->message); } diff --git a/tests/Integration/Database/EloquentCursorPaginateTest.php b/tests/Integration/Database/EloquentCursorPaginateTest.php index 2d5308161490..08ae3eedaa85 100644 --- a/tests/Integration/Database/EloquentCursorPaginateTest.php +++ b/tests/Integration/Database/EloquentCursorPaginateTest.php @@ -202,10 +202,10 @@ public function testPaginationWithMultipleUnionAndMultipleWhereClauses() $this->assertSame(['id'], $result->getOptions()['parameters']); $postB = $table2->where('id', '>', 1)->first(); - $this->assertEquals('Post B', $postB->title, 'Expect `Post B` is the result of the second query'); + $this->assertSame('Post B', $postB->title, 'Expect `Post B` is the result of the second query'); $this->assertCount(1, $result->items(), 'Expect cursor paginated query should have 1 result'); - $this->assertEquals('Post B', current($result->items())->title, 'Expect the paginated query would return `Post B`'); + $this->assertSame('Post B', current($result->items())->title, 'Expect the paginated query would return `Post B`'); } public function testPaginationWithMultipleAliases() @@ -232,7 +232,7 @@ public function testPaginationWithMultipleAliases() $this->assertSame(['alias'], $result->getOptions()['parameters']); $this->assertCount(1, $result->items(), 'Expect cursor paginated query should have 1 result'); - $this->assertEquals('B (post)', current($result->items())->alias, 'Expect the paginated query would return `B (post)`'); + $this->assertSame('B (post)', current($result->items())->alias, 'Expect the paginated query would return `B (post)`'); } public function testPaginationWithAliasedOrderBy() diff --git a/tests/Integration/Database/EloquentDeleteTest.php b/tests/Integration/Database/EloquentDeleteTest.php index 6145417f9107..c5d95c7e3fc2 100644 --- a/tests/Integration/Database/EloquentDeleteTest.php +++ b/tests/Integration/Database/EloquentDeleteTest.php @@ -133,7 +133,7 @@ public function testDeleteQuietly() $post = Post::query()->create([]); $result = $post->deleteQuietly(); - $this->assertEquals('\(^_^)/', $_SERVER['(-_-)']); + $this->assertSame('\(^_^)/', $_SERVER['(-_-)']); $this->assertTrue($result); $this->assertFalse($post->exists); @@ -144,7 +144,7 @@ public function testDeleteQuietly() $role = Role::create([]); $result = $role->deleteQuietly(); $this->assertTrue($result); - $this->assertEquals('\(^_^)/', $_SERVER['(-_-)']); + $this->assertSame('\(^_^)/', $_SERVER['(-_-)']); unset($_SERVER['(-_-)']); } diff --git a/tests/Integration/Database/EloquentHasManyThroughTest.php b/tests/Integration/Database/EloquentHasManyThroughTest.php index 606f20f9ab9b..2e50c56cdf5c 100644 --- a/tests/Integration/Database/EloquentHasManyThroughTest.php +++ b/tests/Integration/Database/EloquentHasManyThroughTest.php @@ -196,8 +196,8 @@ public function testFirstOrCreateWhenModelDoesntExist() $this->assertTrue($mate->wasRecentlyCreated); $this->assertNull($mate->team_id); - $this->assertEquals('Adam', $mate->name); - $this->assertEquals('adam', $mate->slug); + $this->assertSame('Adam', $mate->name); + $this->assertSame('adam', $mate->slug); } public function testFirstOrCreateWhenModelExists() @@ -212,8 +212,8 @@ public function testFirstOrCreateWhenModelExists() $this->assertFalse($mate->wasRecentlyCreated); $this->assertNotNull($mate->team_id); $this->assertTrue($team->is($mate->team)); - $this->assertEquals('Adam Wathan', $mate->name); - $this->assertEquals('adam', $mate->slug); + $this->assertSame('Adam Wathan', $mate->name); + $this->assertSame('adam', $mate->slug); } public function testFirstOrCreateRegressionIssue() @@ -234,8 +234,8 @@ public function testFirstOrCreateRegressionIssue() $this->assertFalse($newJohn->wasRecentlyCreated); $this->assertTrue($john->is($newJohn)); - $this->assertEquals('john', $newJohn->refresh()->slug); - $this->assertEquals('John', $newJohn->name); + $this->assertSame('john', $newJohn->refresh()->slug); + $this->assertSame('John', $newJohn->name); $this->assertSame('john', $john->refresh()->slug); $this->assertSame('John', $john->name); @@ -254,7 +254,7 @@ public function testCreateOrFirstWhenRecordDoesntExist() ); $this->assertTrue($article->wasRecentlyCreated); - $this->assertEquals('Laravel Forever', $article->title); + $this->assertSame('Laravel Forever', $article->title); $this->assertTrue($tony->is($article->user)); } @@ -274,7 +274,7 @@ public function testCreateOrFirstWhenRecordExists() ); $this->assertFalse($newArticle->wasRecentlyCreated); - $this->assertEquals('Laravel Forever', $newArticle->title); + $this->assertSame('Laravel Forever', $newArticle->title); $this->assertTrue($taylor->is($newArticle->user)); $this->assertTrue($existingArticle->is($newArticle)); } @@ -295,7 +295,7 @@ public function testCreateOrFirstWhenRecordExistsInTransaction() )); $this->assertFalse($newArticle->wasRecentlyCreated); - $this->assertEquals('Laravel Forever', $newArticle->title); + $this->assertSame('Laravel Forever', $newArticle->title); $this->assertTrue($taylor->is($newArticle->user)); $this->assertTrue($existingArticle->is($newArticle)); } @@ -317,7 +317,7 @@ public function testCreateOrFirstRegressionIssue() $this->assertFalse($newArticle->wasRecentlyCreated); $this->assertTrue($existingTaylorArticle->is($newArticle)); - $this->assertEquals('Laravel Forever', $newArticle->refresh()->title); + $this->assertSame('Laravel Forever', $newArticle->refresh()->title); $this->assertTrue($taylor->is($newArticle->user)); $this->assertSame('Laravel Forever', $existingTaylorArticle->refresh()->title); diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index 8521b948e8e9..dc7984c52cba 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -113,7 +113,7 @@ public function testPivotWithPivotCriteriaTriggerEventsToBeFiredOnCreateUpdateNo PivotEventsTestCollaborator::$eventsCalled = []; $project->contributors()->detach($user->id); - $this->assertEquals([], PivotEventsTestCollaborator::$eventsCalled); + $this->assertSame([], PivotEventsTestCollaborator::$eventsCalled); } public function testCustomPivotUpdateEventHasExistingAttributes() diff --git a/tests/Integration/Database/EloquentUpdateTest.php b/tests/Integration/Database/EloquentUpdateTest.php index 55b5934e15dc..0df53a53f301 100644 --- a/tests/Integration/Database/EloquentUpdateTest.php +++ b/tests/Integration/Database/EloquentUpdateTest.php @@ -269,7 +269,7 @@ public function testIncrementEachDoesNotResetUnrelatedDirtyAttributes() $post->incrementEach(['views' => 1]); $this->assertTrue($post->isDirty('name')); - $this->assertEquals('Changed', $post->name); + $this->assertSame('Changed', $post->name); $this->assertFalse($post->isDirty('views')); } diff --git a/tests/Integration/Database/EloquentWhereTest.php b/tests/Integration/Database/EloquentWhereTest.php index ff6c66595086..0d4fcb7e37d7 100644 --- a/tests/Integration/Database/EloquentWhereTest.php +++ b/tests/Integration/Database/EloquentWhereTest.php @@ -298,7 +298,7 @@ public function testSoleValue() 'address' => 'test-address', ]); - $this->assertEquals('test-name', UserWhereTest::where('name', 'test-name')->soleValue('name')); + $this->assertSame('test-name', UserWhereTest::where('name', 'test-name')->soleValue('name')); } public function testChunkMap() diff --git a/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php b/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php index c018f2956707..f0a8f0dffb53 100644 --- a/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php +++ b/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php @@ -25,7 +25,7 @@ public function testAddCommentToTable() ->select('table_comment as table_comment') ->first(); - $this->assertEquals('This is a comment', $tableInfo->table_comment); + $this->assertSame('This is a comment', $tableInfo->table_comment); Schema::drop('users'); } diff --git a/tests/Integration/Database/ModelInspectorTest.php b/tests/Integration/Database/ModelInspectorTest.php index ec64035bd7b4..b4d10411e78f 100644 --- a/tests/Integration/Database/ModelInspectorTest.php +++ b/tests/Integration/Database/ModelInspectorTest.php @@ -58,7 +58,7 @@ private function assertModelInfo(ModelInfo|array $modelInfo) { $this->assertEquals(ModelInspectorTestModel::class, $modelInfo['class']); $this->assertEquals(Schema::getConnection()->getConfig()['name'], $modelInfo['database']); - $this->assertEquals('model_info_extractor_test_model', $modelInfo['table']); + $this->assertSame('model_info_extractor_test_model', $modelInfo['table']); $this->assertNull($modelInfo['policy']); $this->assertCount(8, $modelInfo['attributes']); @@ -167,9 +167,9 @@ private function assertModelInfo(ModelInfo|array $modelInfo) $this->assertEmpty($modelInfo['events']); $this->assertCount(1, $modelInfo['observers']); - $this->assertEquals('created', $modelInfo['observers'][0]['event']); + $this->assertSame('created', $modelInfo['observers'][0]['event']); $this->assertCount(1, $modelInfo['observers'][0]['observer']); - $this->assertEquals("Illuminate\Tests\Integration\Database\ModelInspectorTestModelObserver@created", $modelInfo['observers'][0]['observer'][0]); + $this->assertSame("Illuminate\Tests\Integration\Database\ModelInspectorTestModelObserver@created", $modelInfo['observers'][0]['observer'][0]); $this->assertEquals(ModelInspectorTestModelEloquentCollection::class, $modelInfo['collection']); $this->assertEquals(ModelInspectorTestModelBuilder::class, $modelInfo['builder']); } diff --git a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php index 949ec4819185..022045f4e741 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php @@ -26,7 +26,7 @@ public function testAddCommentToTable() ->select('table_comment as table_comment') ->first(); - $this->assertEquals('This is a comment', $tableInfo->table_comment); + $this->assertSame('This is a comment', $tableInfo->table_comment); Schema::drop('users'); } diff --git a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php index 6d2308fb9d41..fe8e4125f85c 100644 --- a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php +++ b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php @@ -117,7 +117,7 @@ public function testAddTableCommentOnNewTable() $table->comment('This is a comment'); }); - $this->assertEquals('This is a comment', DB::selectOne("select obj_description('public.posts'::regclass, 'pg_class')")->obj_description); + $this->assertSame('This is a comment', DB::selectOne("select obj_description('public.posts'::regclass, 'pg_class')")->obj_description); } public function testAddTableCommentOnExistingTable() @@ -131,7 +131,7 @@ public function testAddTableCommentOnExistingTable() $table->comment('This is a new comment'); }); - $this->assertEquals('This is a new comment', DB::selectOne("select obj_description('public.posts'::regclass, 'pg_class')")->obj_description); + $this->assertSame('This is a new comment', DB::selectOne("select obj_description('public.posts'::regclass, 'pg_class')")->obj_description); } public function testGetTables() diff --git a/tests/Integration/Database/QueryBuilderTest.php b/tests/Integration/Database/QueryBuilderTest.php index 57a1593dac69..65eed4dfb630 100644 --- a/tests/Integration/Database/QueryBuilderTest.php +++ b/tests/Integration/Database/QueryBuilderTest.php @@ -138,8 +138,8 @@ public function testIncrement() $rows = DB::table('accounting')->get(); - $this->assertEquals(1.5, $rows[0]->wallet_1); - $this->assertEquals(1.5, $rows[1]->wallet_1); + $this->assertSame(1.5, (float) $rows[0]->wallet_1); + $this->assertSame(1.5, (float) $rows[1]->wallet_1); Schema::drop('accounting'); } diff --git a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php index c2ec3e57c392..364b68c6c231 100644 --- a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php +++ b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php @@ -526,16 +526,16 @@ public function testComment($connection) $tableName = $connection === 'with-prefix' ? 'example_table' : 'table'; $defaultSchema = $this->driver === 'pgsql' ? 'public' : 'laravel'; - $this->assertEquals('comment on schema table', + $this->assertSame('comment on schema table', $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === 'my_schema')['comment'] ); - $this->assertEquals('comment on table', + $this->assertSame('comment on table', $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === $defaultSchema)['comment'] ); - $this->assertEquals('comment on schema column', + $this->assertSame('comment on schema column', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'name')['comment'] ); - $this->assertEquals('comment on column', + $this->assertSame('comment on column', collect($schema->getColumns('table'))->firstWhere('name', 'name')['comment'] ); } @@ -579,7 +579,7 @@ public function testHasTable($connection) 'database.connections.'.$connection.'.password' => 'Passw0rd', ]); - $this->assertEquals('my_schema', $schema->getCurrentSchemaName()); + $this->assertSame('my_schema', $schema->getCurrentSchemaName()); $schema->create('table', function (Blueprint $table) { $table->id(); diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 1628a5b7ac76..3e7a6e68e3c7 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -793,7 +793,7 @@ public function testAddingMacros() { Schema::macro('foo', fn () => 'foo'); - $this->assertEquals('foo', Schema::foo()); + $this->assertSame('foo', Schema::foo()); Schema::macro('hasForeignKeyForColumn', function (string $column, string $table, string $foreignTable) { return collect(Schema::getForeignKeys($table)) diff --git a/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php b/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php index f9ef9c11a46b..694b8af4b25a 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php @@ -72,7 +72,7 @@ public function testGetViews() $tableView = Schema::getViews(); $this->assertCount(1, $tableView); - $this->assertEquals('users_view', $tableView[0]['name']); + $this->assertSame('users_view', $tableView[0]['name']); DB::connection('conn1')->statement(<<<'SQL' DROP VIEW IF EXISTS users_view; diff --git a/tests/Integration/Events/DeferEventsTest.php b/tests/Integration/Events/DeferEventsTest.php index 861e31c441db..59aaf29c9ac2 100644 --- a/tests/Integration/Events/DeferEventsTest.php +++ b/tests/Integration/Events/DeferEventsTest.php @@ -24,7 +24,7 @@ public function testDeferEvents() return 'callback_result'; }); - $this->assertEquals('callback_result', $response); + $this->assertSame('callback_result', $response); $this->assertSame('bar', $_SERVER['__event.test']); } @@ -45,7 +45,7 @@ public function testDeferModelEvents() return 'model_event_response'; }); - $this->assertEquals('model_event_response', $response); + $this->assertSame('model_event_response', $response); $this->assertContains('saved', $_SERVER['__model_event.test']); } @@ -74,7 +74,7 @@ public function testDeferMultipleModelEvents() return 'multiple_models_response'; }); - $this->assertEquals('multiple_models_response', $response); + $this->assertSame('multiple_models_response', $response); $this->assertSame(['saved:TestModel', 'created:AnotherTestModel'], $_SERVER['__model_events']); } @@ -100,7 +100,7 @@ public function testDeferSpecificModelEvents() return 'specific_model_defer_result'; }, ['eloquent.saved: '.TestModel::class]); - $this->assertEquals('specific_model_defer_result', $response); + $this->assertSame('specific_model_defer_result', $response); $this->assertSame(['creating', 'saved'], $_SERVER['__model_events']); } } diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php index 319703f0c827..e4eee6420124 100644 --- a/tests/Integration/Events/EventFakeTest.php +++ b/tests/Integration/Events/EventFakeTest.php @@ -183,7 +183,7 @@ public function testMissingMethodsAreForwarded() { Event::macro('foo', fn () => 'bar'); - $this->assertEquals('bar', Event::fake()->foo()); + $this->assertSame('bar', Event::fake()->foo()); } public function testShouldDispatchAfterCommitEventsAreNotDispatchedIfTransactionFails() diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php index 89e5f8a8f131..5b6a2f2859fd 100644 --- a/tests/Integration/Filesystem/ServeFileTest.php +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -29,7 +29,7 @@ public function testItCanServeAnExistingFile() $response = $this->get($url); - $this->assertEquals('Hello World', $response->streamedContent()); + $this->assertSame('Hello World', $response->streamedContent()); } public function testItWill404OnMissingFile() diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index d79d31dfea09..1527f0baf1ee 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -48,8 +48,8 @@ public function test_it_can_configure_disks() Cloud::configureDisks($this->app); - $this->assertEquals('test-disk-2', $this->app['config']->get('filesystems.default')); - $this->assertEquals('test-access-key-id', $this->app['config']->get('filesystems.disks.test-disk.key')); + $this->assertSame('test-disk-2', $this->app['config']->get('filesystems.default')); + $this->assertSame('test-access-key-id', $this->app['config']->get('filesystems.disks.test-disk.key')); unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); } @@ -116,7 +116,7 @@ public function test_it_respects_log_levels() Cloud::configureCloudLogging($this->app); - $this->assertEquals('notice', $this->app['config']->get('logging.channels.laravel-cloud-socket.level')); + $this->assertSame('notice', $this->app['config']->get('logging.channels.laravel-cloud-socket.level')); unset($_SERVER['LOG_LEVEL']); diff --git a/tests/Integration/Foundation/DiscoverEventsTest.php b/tests/Integration/Foundation/DiscoverEventsTest.php index 417358539e24..bdf864b1f0eb 100644 --- a/tests/Integration/Foundation/DiscoverEventsTest.php +++ b/tests/Integration/Foundation/DiscoverEventsTest.php @@ -81,7 +81,7 @@ public function testNoExceptionForEmptyDirectories(): void { $events = DiscoverEvents::within([], getcwd()); - $this->assertEquals([], $events); + $this->assertSame([], $events); } public function testEventsCanBeDiscoveredUsingCustomClassNameGuessing() diff --git a/tests/Integration/Foundation/ExceptionHandlerTest.php b/tests/Integration/Foundation/ExceptionHandlerTest.php index 0b0f22a5354b..900c03510069 100644 --- a/tests/Integration/Foundation/ExceptionHandlerTest.php +++ b/tests/Integration/Foundation/ExceptionHandlerTest.php @@ -74,7 +74,7 @@ public function toResponse($request) ->assertStatus(500) ->assertSee('shouldnt report'); - $this->assertEquals([], $reported); + $this->assertSame([], $reported); } public function testItRendersAuthorizationExceptionsWithCustomStatusCode() diff --git a/tests/Integration/Foundation/FoundationHelpersTest.php b/tests/Integration/Foundation/FoundationHelpersTest.php index a55e6c73cf42..7ea8ec98ff82 100644 --- a/tests/Integration/Foundation/FoundationHelpersTest.php +++ b/tests/Integration/Foundation/FoundationHelpersTest.php @@ -13,14 +13,14 @@ class FoundationHelpersTest extends TestCase { public function testRescue() { - $this->assertEquals( + $this->assertSame( 'rescued!', rescue(function () { throw new Exception; }, 'rescued!') ); - $this->assertEquals( + $this->assertSame( 'rescued!', rescue(function () { throw new Exception; @@ -29,7 +29,7 @@ public function testRescue() }) ); - $this->assertEquals( + $this->assertSame( 'no need to rescue', rescue(function () { return 'no need to rescue'; @@ -44,7 +44,7 @@ public function test(int $a) } }; - $this->assertEquals( + $this->assertSame( 'rescued!', rescue(function () use ($testClass) { $testClass->test([]); diff --git a/tests/Integration/Http/HttpClientTest.php b/tests/Integration/Http/HttpClientTest.php index 7e49fc10aa80..d152c7455ba4 100644 --- a/tests/Integration/Http/HttpClientTest.php +++ b/tests/Integration/Http/HttpClientTest.php @@ -86,8 +86,8 @@ public function testForwardsCallsToPromise() }) ->wait(); - $this->assertEquals('faked response', $myFakedResponse); - $this->assertEquals('stub', $r); + $this->assertSame('faked response', (string) $myFakedResponse); + $this->assertSame('stub', $r); } public function testCanSetRequestAttributes() @@ -105,10 +105,10 @@ public function testCanSetRequestAttributes() $response3 = Http::get('https://some-store.myshopify.com/admin/api/2025-10/graphql.json'); $response4 = Http::withAttributes(['name' => 'fourth'])->get('https://some-store.myshopify.com/admin/api/2025-10/graphql.json'); - $this->assertEquals('first response', $response1->body()); - $this->assertEquals('second response', $response2->body()); - $this->assertEquals('unnamed', $response3->body()); - $this->assertEquals('unnamed', $response4->body()); + $this->assertSame('first response', $response1->body()); + $this->assertSame('second response', $response2->body()); + $this->assertSame('unnamed', $response3->body()); + $this->assertSame('unnamed', $response4->body()); } public function testAsyncCanHandleThrownException() diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php index f872ccba0140..5e468aa400d3 100644 --- a/tests/Integration/Http/ResourceTest.php +++ b/tests/Integration/Http/ResourceTest.php @@ -907,7 +907,7 @@ public function testResourcesMayCustomizeJsonOptions() '/', ['Accept' => 'application/json'] ); - $this->assertEquals( + $this->assertSame( '{"data":{"id":5,"title":"Test Title","reading_time":3.0}}', $response->baseResponse->content() ); @@ -925,7 +925,7 @@ public function testCollectionResourcesMayCustomizeJsonOptions() '/', ['Accept' => 'application/json'] ); - $this->assertEquals( + $this->assertSame( '{"data":[{"id":5,"title":"Test Title","reading_time":3.0}]}', $response->baseResponse->content() ); @@ -946,7 +946,7 @@ public function testResourcesMayCustomizeJsonOptionsOnPaginatedResponse() '/', ['Accept' => 'application/json'] ); - $this->assertEquals( + $this->assertSame( '{"data":[{"id":5,"title":"Test Title","reading_time":3.0}],"links":{"first":"\/?page=1","last":"\/?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"« Previous","page":null,"active":false},{"url":"\/?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Next »","page":null,"active":false}],"path":"\/","per_page":15,"to":1,"total":10}}', $response->baseResponse->content() ); @@ -966,7 +966,7 @@ public function testResourcesMayCustomizeJsonOptionsWithTypeHintedConstructor() '/', ['Accept' => 'application/json'] ); - $this->assertEquals( + $this->assertSame( '{"data":{"id":5,"title":"Test Title","reading_time":3.0}}', $response->baseResponse->content() ); diff --git a/tests/Integration/Log/ContextIntegrationTest.php b/tests/Integration/Log/ContextIntegrationTest.php index 2d7fe074f0e5..59a4c6966a07 100644 --- a/tests/Integration/Log/ContextIntegrationTest.php +++ b/tests/Integration/Log/ContextIntegrationTest.php @@ -20,7 +20,7 @@ class ContextIntegrationTest extends TestCase public function test_it_can_hydrate_null() { Context::hydrate(null); - $this->assertEquals([], Context::all()); + $this->assertSame([], Context::all()); } public function test_it_handles_eloquent() diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php index 0c36149613c3..6068f7fe35d1 100644 --- a/tests/Integration/Queue/DebouncedJobTest.php +++ b/tests/Integration/Queue/DebouncedJobTest.php @@ -131,7 +131,7 @@ public function testDebounceOwnerSurvivesSerialization() $restored = unserialize(serialize($job)); - $this->assertEquals('test-owner-token-123', $restored->debounceOwner); + $this->assertSame('test-owner-token-123', $restored->debounceOwner); } public function testDifferentDebounceIdsDoNotInterfere() diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 8cf10be0e966..02ca6a4d9902 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -653,7 +653,7 @@ public function testChainConditionable() $chain->onConnection('sync2'); }); - $this->assertEquals('sync2', $chain->connection); + $this->assertSame('sync2', $chain->connection); $chain = Bus::chain([]) ->onConnection('sync1') @@ -661,7 +661,7 @@ public function testChainConditionable() $chain->onConnection('sync2'); }); - $this->assertEquals('sync1', $chain->connection); + $this->assertSame('sync1', $chain->connection); } public function testBatchConditionable() @@ -672,14 +672,14 @@ public function testBatchConditionable() $batch->onConnection('sync2'); }); - $this->assertEquals('sync2', $batch->connection()); + $this->assertSame('sync2', $batch->connection()); $batch = Bus::batch([]) ->onConnection('sync1') ->when(false, function (PendingBatch $batch) { $batch->onConnection('sync2'); }); - $this->assertEquals('sync1', $batch->connection()); + $this->assertSame('sync1', $batch->connection()); } public function testJobsAreChainedWhenDispatchIfIsTrue() diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php index 1fa68472821c..9eff3448bebb 100644 --- a/tests/Integration/Queue/ModelSerializationTest.php +++ b/tests/Integration/Queue/ModelSerializationTest.php @@ -469,7 +469,7 @@ public function test_it_respects_without_relations_attribute_applied_to_class() $unserialized = unserialize($serialized); $this->assertFalse($unserialized->user->relationLoaded('roles')); - $this->assertEquals('hello', $unserialized->value->value); + $this->assertSame('hello', $unserialized->value->value); } #[WithConfig('database.default', 'testing')] @@ -485,7 +485,7 @@ public function test_it_respects_without_relations_attribute_applied_to_parent_c $unserialized = unserialize($serialized); $this->assertFalse($unserialized->user->relationLoaded('roles')); - $this->assertEquals('hello', $unserialized->value->value); + $this->assertSame('hello', $unserialized->value->value); } public function test_serialization_types_empty_custom_eloquent_collection() diff --git a/tests/Integration/Queue/QueueFakeTest.php b/tests/Integration/Queue/QueueFakeTest.php index 62d416c3da6c..8ed86476136f 100644 --- a/tests/Integration/Queue/QueueFakeTest.php +++ b/tests/Integration/Queue/QueueFakeTest.php @@ -46,7 +46,7 @@ public function testFakeForReturnValue() return 'test-value'; }); - $this->assertEquals('test-value', $result); + $this->assertSame('test-value', $result); } public function testFakeExceptForReturnValue() @@ -55,7 +55,7 @@ public function testFakeExceptForReturnValue() return 'test-value'; }, []); - $this->assertEquals('test-value', $result); + $this->assertSame('test-value', $result); } } diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index 978826fad225..9a7c84fb7419 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -246,7 +246,7 @@ public function testLockUsesDisplayNameWhenAvailable() public function testUniqueLockCreatesKeyWithClassName() { - $this->assertEquals( + $this->assertSame( 'laravel_unique_job:'.UniqueTestJob::class.':', UniqueLock::getKey(new UniqueTestJob) ); @@ -254,7 +254,7 @@ public function testUniqueLockCreatesKeyWithClassName() public function testUniqueLockCreatesKeyWithIdAndClassName() { - $this->assertEquals( + $this->assertSame( 'laravel_unique_job:'.UniqueIdTestJob::class.':unique-id-1', UniqueLock::getKey(new UniqueIdTestJob) ); @@ -262,7 +262,7 @@ public function testUniqueLockCreatesKeyWithIdAndClassName() public function testUniqueLockCreatesKeyWithDisplayNameWhenAvailable() { - $this->assertEquals( + $this->assertSame( 'laravel_unique_job:'.hash('xxh128', 'App\\Actions\\UniqueTestAction').':unique-id-2', UniqueLock::getKey(new UniqueIdTestJobWithDisplayName) ); @@ -270,7 +270,7 @@ public function testUniqueLockCreatesKeyWithDisplayNameWhenAvailable() public function testUniqueLockCreatesKeyWithIdAndDisplayNameWhenAvailable() { - $this->assertEquals( + $this->assertSame( 'laravel_unique_job:'.hash('xxh128', 'App\\Actions\\UniqueTestAction').':unique-id-2', UniqueLock::getKey(new UniqueIdTestJobWithDisplayName) ); diff --git a/tests/Integration/Routing/RouteViewTest.php b/tests/Integration/Routing/RouteViewTest.php index 88fc61081ea9..520f501f5833 100644 --- a/tests/Integration/Routing/RouteViewTest.php +++ b/tests/Integration/Routing/RouteViewTest.php @@ -28,15 +28,15 @@ public function testRouteViewWithParams() $this->assertStringContainsString('Test bar', $this->get('/route/value1')->getContent()); tap($this->get('/route/value1/value2'), function ($response) { - $this->assertEquals('value1', $response->viewData('param')); - $this->assertEquals('value1', $response->baseRequest->route('param')); - $this->assertEquals('value2', $response->baseRequest->route('param2')); + $this->assertSame('value1', $response->viewData('param')); + $this->assertSame('value1', $response->baseRequest->route('param')); + $this->assertSame('value2', $response->baseRequest->route('param2')); }); tap($this->get('/route/value1/value2'), function ($response) { - $this->assertEquals('value2', $response->viewData('param2')); - $this->assertEquals('value1', $response->baseRequest->route('param')); - $this->assertEquals('value2', $response->baseRequest->route('param2')); + $this->assertSame('value2', $response->viewData('param2')); + $this->assertSame('value1', $response->baseRequest->route('param')); + $this->assertSame('value2', $response->baseRequest->route('param2')); }); } diff --git a/tests/Integration/Session/DatabaseSessionHandlerTest.php b/tests/Integration/Session/DatabaseSessionHandlerTest.php index 950e11a889c8..fb2690f50577 100644 --- a/tests/Integration/Session/DatabaseSessionHandlerTest.php +++ b/tests/Integration/Session/DatabaseSessionHandlerTest.php @@ -17,7 +17,7 @@ public function test_basic_read_write_functionality() $handler->setContainer($this->app); // read non-existing session id: - $this->assertEquals('', $handler->read('invalid_session_id')); + $this->assertSame('', $handler->read('invalid_session_id')); // open and close: $this->assertTrue($handler->open('', '')); @@ -47,7 +47,7 @@ public function test_basic_read_write_functionality() // read expired: Carbon::setTestNow(Carbon::now()->addMinutes(2)); - $this->assertEquals('', $handler->read('valid_session_id_2425')); + $this->assertSame('', $handler->read('valid_session_id_2425')); // rewriting an expired session-id, makes it live: $this->assertTrue($handler->write('valid_session_id_2425', json_encode(['come' => 'alive']))); @@ -103,7 +103,7 @@ public function test_it_can_work_without_container() // write and read: $this->assertTrue($handler->write('session_id', 'some data')); - $this->assertEquals('some data', $handler->read('session_id')); + $this->assertSame('some data', $handler->read('session_id')); $this->assertEquals(1, $connection->table('sessions')->count()); $session = $connection->table('sessions')->first(); diff --git a/tests/Log/LogLoggerTest.php b/tests/Log/LogLoggerTest.php index 23b71f68e367..c887e1d8f3fe 100755 --- a/tests/Log/LogLoggerTest.php +++ b/tests/Log/LogLoggerTest.php @@ -80,7 +80,7 @@ public function testLoggerFiresEventsDispatcher() $this->assertSame('foo', $_SERVER['__log.message']); unset($_SERVER['__log.message']); $this->assertTrue(isset($_SERVER['__log.context'])); - $this->assertEquals([], $_SERVER['__log.context']); + $this->assertSame([], $_SERVER['__log.context']); unset($_SERVER['__log.context']); } diff --git a/tests/Log/LogManagerTest.php b/tests/Log/LogManagerTest.php index 3345328a73d2..8cf0b9391011 100755 --- a/tests/Log/LogManagerTest.php +++ b/tests/Log/LogManagerTest.php @@ -47,7 +47,7 @@ public function testLogManagerGetDefaultDriver() //we don't specify any channel name $manager->channel(); $this->assertCount(1, $manager->getChannels()); - $this->assertEquals('single', $manager->getDefaultDriver()); + $this->assertSame('single', $manager->getDefaultDriver()); } public function testStackChannel() @@ -726,7 +726,7 @@ public function testLogManagerCreateCustomFormatterWithTap() $format = new ReflectionProperty(get_class($formatter), 'format'); - $this->assertEquals( + $this->assertSame( '[%datetime%] %channel%.%level_name%: %message% %context% %extra%', rtrim($format->getValue($formatter))); } @@ -750,7 +750,7 @@ public function testDriverUsersPsrLoggerManagerReturnsLogger() // Then $this->assertCount(1, $loggerSpy->logs); - $this->assertEquals('some alert', $loggerSpy->logs[0]['message']); + $this->assertSame('some alert', $loggerSpy->logs[0]['message']); } public function testCustomDriverClosureBoundObjectIsLogManager() diff --git a/tests/Mail/MailMailableTest.php b/tests/Mail/MailMailableTest.php index 92b00976d6b8..e5f22fdb34ab 100644 --- a/tests/Mail/MailMailableTest.php +++ b/tests/Mail/MailMailableTest.php @@ -1183,12 +1183,12 @@ public function testMailableHeadersGetSent(): void $this->assertSame('custom-message-id@example.com', $sentMessage->getMessageId()); $this->assertTrue($sentMessage->getOriginalMessage()->getHeaders()->has('references')); - $this->assertEquals('References', $sentMessage->getOriginalMessage()->getHeaders()->get('references')->getName()); - $this->assertEquals('', $sentMessage->getOriginalMessage()->getHeaders()->get('references')->getValue()); + $this->assertSame('References', $sentMessage->getOriginalMessage()->getHeaders()->get('references')->getName()); + $this->assertSame('', $sentMessage->getOriginalMessage()->getHeaders()->get('references')->getValue()); $this->assertTrue($sentMessage->getOriginalMessage()->getHeaders()->has('x-custom-header')); - $this->assertEquals('X-Custom-Header', $sentMessage->getOriginalMessage()->getHeaders()->get('x-custom-header')->getName()); - $this->assertEquals('Custom Value', $sentMessage->getOriginalMessage()->getHeaders()->get('x-custom-header')->getValue()); + $this->assertSame('X-Custom-Header', $sentMessage->getOriginalMessage()->getHeaders()->get('x-custom-header')->getName()); + $this->assertSame('Custom Value', $sentMessage->getOriginalMessage()->getHeaders()->get('x-custom-header')->getValue()); } public function testMailableAttributesInBuild(): void diff --git a/tests/Mail/MailableAlternativeSyntaxTest.php b/tests/Mail/MailableAlternativeSyntaxTest.php index 20f07c559e0f..6781027c6e49 100644 --- a/tests/Mail/MailableAlternativeSyntaxTest.php +++ b/tests/Mail/MailableAlternativeSyntaxTest.php @@ -33,7 +33,7 @@ public function testBasicMailableInspection(): void $method = $reflection->getMethod('prepareMailableForDelivery'); $method->invoke($mailable); - $this->assertEquals('test-view', $mailable->view); + $this->assertSame('test-view', $mailable->view); $this->assertEquals(['test-data-key' => 'test-data-value'], $mailable->viewData); $this->assertEquals(2, count($mailable->to)); $this->assertEquals(1, count($mailable->cc)); @@ -46,24 +46,24 @@ public function testEnvelopesCanReceiveAdditionalRecipients(): void $envelope->to(new Address('taylorotwell@example.com')); $this->assertCount(2, $envelope->to); - $this->assertEquals('taylor@example.com', $envelope->to[0]->address); - $this->assertEquals('taylorotwell@example.com', $envelope->to[1]->address); + $this->assertSame('taylor@example.com', $envelope->to[0]->address); + $this->assertSame('taylorotwell@example.com', $envelope->to[1]->address); $envelope->to('abigailotwell@example.com', 'Abigail Otwell'); - $this->assertEquals('abigailotwell@example.com', $envelope->to[2]->address); - $this->assertEquals('Abigail Otwell', $envelope->to[2]->name); + $this->assertSame('abigailotwell@example.com', $envelope->to[2]->address); + $this->assertSame('Abigail Otwell', $envelope->to[2]->name); $envelope->to('adam@example.com'); - $this->assertEquals('adam@example.com', $envelope->to[3]->address); + $this->assertSame('adam@example.com', $envelope->to[3]->address); $this->assertNull($envelope->to[3]->name); $envelope->to(['jeffrey@example.com', 'tyler@example.com']); - $this->assertEquals('jeffrey@example.com', $envelope->to[4]->address); - $this->assertEquals('tyler@example.com', $envelope->to[5]->address); + $this->assertSame('jeffrey@example.com', $envelope->to[4]->address); + $this->assertSame('tyler@example.com', $envelope->to[5]->address); $envelope->from('dries@example.com', 'Dries Vints'); - $this->assertEquals('dries@example.com', $envelope->from->address); - $this->assertEquals('Dries Vints', $envelope->from->name); + $this->assertSame('dries@example.com', $envelope->from->address); + $this->assertSame('Dries Vints', $envelope->from->name); } } diff --git a/tests/Pipeline/PipelineTransactionTest.php b/tests/Pipeline/PipelineTransactionTest.php index 2f52d2a7fcb7..facf722c85a0 100644 --- a/tests/Pipeline/PipelineTransactionTest.php +++ b/tests/Pipeline/PipelineTransactionTest.php @@ -25,7 +25,7 @@ public function testPipelineTransaction() ]) ->thenReturn(); - $this->assertEquals('some string', $result); + $this->assertSame('some string', $result); Event::assertDispatchedTimes(TransactionBeginning::class, 1); Event::assertDispatchedTimes(TransactionCommitted::class, 1); } @@ -55,7 +55,7 @@ function ($value, $next) { ]) ->thenReturn(); - $this->assertEquals('some string', $result); + $this->assertSame('some string', $result); Event::dispatched(TransactionBeginning::class, function (TransactionBeginning $event) use ($connectionName) { return $event->connection === $connectionName; }); diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 3b84497a76f0..69e28a56841c 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -24,7 +24,7 @@ public function testSuccessfulProcess() $this->assertFalse($result->failed()); $this->assertEquals(0, $result->exitCode()); $this->assertTrue(str_contains($result->output(), 'ProcessTest.php')); - $this->assertEquals('', $result->errorOutput()); + $this->assertSame('', $result->errorOutput()); $result->throw(); $result->throwIf(true); @@ -169,8 +169,8 @@ public function testBasicProcessFake() $result = $factory->run('ls -la'); - $this->assertEquals('', $result->output()); - $this->assertEquals('', $result->errorOutput()); + $this->assertSame('', $result->output()); + $this->assertSame('', $result->errorOutput()); $this->assertEquals(0, $result->exitCode()); $this->assertTrue($result->successful()); } @@ -243,56 +243,56 @@ public function testBasicProcessFakeWithCustomOutput() $factory->fake(fn () => $factory->result('test output')); $result = $factory->run('ls -la'); - $this->assertEquals("test output\n", $result->output()); + $this->assertSame("test output\n", $result->output()); // Array of output... $factory = new Factory; $factory->fake(fn () => $factory->result(['line 1', 'line 2'])); $result = $factory->run('ls -la'); - $this->assertEquals("line 1\nline 2\n", $result->output()); + $this->assertSame("line 1\nline 2\n", $result->output()); // Array of output with empty line... $factory = new Factory; $factory->fake(fn () => $factory->result(['line 1', '', 'line 2'])); $result = $factory->run('ls -la'); - $this->assertEquals("line 1\n\nline 2\n", $result->output()); + $this->assertSame("line 1\n\nline 2\n", $result->output()); // Plain string... $factory = new Factory; $factory->fake(fn () => 'test output'); $result = $factory->run('ls -la'); - $this->assertEquals("test output\n", $result->output()); + $this->assertSame("test output\n", $result->output()); // Plain array... $factory = new Factory; $factory->fake(fn () => ['line 1', 'line 2']); $result = $factory->run('ls -la'); - $this->assertEquals("line 1\nline 2\n", $result->output()); + $this->assertSame("line 1\nline 2\n", $result->output()); // Plain array with empty line... $factory = new Factory; $factory->fake(fn () => ['line 1', '', 'line 2']); $result = $factory->run('ls -la'); - $this->assertEquals("line 1\n\nline 2\n", $result->output()); + $this->assertSame("line 1\n\nline 2\n", $result->output()); // Process description... $factory = new Factory; $factory->fake(fn () => $factory->describe()->output('line 1')->output('line 2')); $result = $factory->run('ls -la'); - $this->assertEquals("line 1\nline 2\n", $result->output()); + $this->assertSame("line 1\nline 2\n", $result->output()); // Process description with empty line... $factory = new Factory; $factory->fake(fn () => $factory->describe()->output('line 1')->output('')->output('line 2')); $result = $factory->run('ls -la'); - $this->assertEquals("line 1\n\nline 2\n", $result->output()); + $this->assertSame("line 1\n\nline 2\n", $result->output()); } public function testProcessFakeWithErrorOutput() @@ -301,24 +301,24 @@ public function testProcessFakeWithErrorOutput() $factory->fake(fn () => $factory->result('standard output', 'error output')); $result = $factory->run('ls -la'); - $this->assertEquals("standard output\n", $result->output()); - $this->assertEquals("error output\n", $result->errorOutput()); + $this->assertSame("standard output\n", $result->output()); + $this->assertSame("error output\n", $result->errorOutput()); // Array of error output... $factory = new Factory; $factory->fake(fn () => $factory->result('standard output', ['line 1', 'line 2'])); $result = $factory->run('ls -la'); - $this->assertEquals("standard output\n", $result->output()); - $this->assertEquals("line 1\nline 2\n", $result->errorOutput()); + $this->assertSame("standard output\n", $result->output()); + $this->assertSame("line 1\nline 2\n", $result->errorOutput()); // Using process description... $factory = new Factory; $factory->fake(fn () => $factory->describe()->output('standard output')->errorOutput('error output')); $result = $factory->run('ls -la'); - $this->assertEquals("standard output\n", $result->output()); - $this->assertEquals("error output\n", $result->errorOutput()); + $this->assertSame("standard output\n", $result->output()); + $this->assertSame("error output\n", $result->errorOutput()); } public function testCustomizedFakesPerCommand() @@ -331,10 +331,10 @@ public function testCustomizedFakesPerCommand() ]); $result = $factory->run('ls -la'); - $this->assertEquals("ls command\n", $result->output()); + $this->assertSame("ls command\n", $result->output()); $result = $factory->run('cat composer.json'); - $this->assertEquals("cat command\n", $result->output()); + $this->assertSame("cat command\n", $result->output()); } public function testProcessFakeSequences() @@ -349,13 +349,13 @@ public function testProcessFakeSequences() ]); $result = $factory->run('ls -la'); - $this->assertEquals("ls command 1\n", $result->output()); + $this->assertSame("ls command 1\n", $result->output()); $result = $factory->run('ls -la'); - $this->assertEquals("ls command 2\n", $result->output()); + $this->assertSame("ls command 2\n", $result->output()); $result = $factory->run('cat composer.json'); - $this->assertEquals("cat command\n", $result->output()); + $this->assertSame("cat command\n", $result->output()); } public function testProcessFakeSequencesCanReturnEmptyResultsWhenSequenceIsEmpty() @@ -370,13 +370,13 @@ public function testProcessFakeSequencesCanReturnEmptyResultsWhenSequenceIsEmpty ]); $result = $factory->run('ls -la'); - $this->assertEquals("ls command 1\n", $result->output()); + $this->assertSame("ls command 1\n", $result->output()); $result = $factory->run('ls -la'); - $this->assertEquals("ls command 2\n", $result->output()); + $this->assertSame("ls command 2\n", $result->output()); $result = $factory->run('ls -la'); - $this->assertEquals('', $result->output()); + $this->assertSame('', $result->output()); } public function testProcessFakeSequencesCanThrowWhenSequenceIsEmpty() @@ -392,10 +392,10 @@ public function testProcessFakeSequencesCanThrowWhenSequenceIsEmpty() ]); $result = $factory->run('ls -la'); - $this->assertEquals("ls command 1\n", $result->output()); + $this->assertSame("ls command 1\n", $result->output()); $result = $factory->run('ls -la'); - $this->assertEquals("ls command 2\n", $result->output()); + $this->assertSame("ls command 2\n", $result->output()); $result = $factory->run('ls -la'); } @@ -503,8 +503,8 @@ public function testRealProcessesCanHaveErrorOutput() $result = $factory->path(__DIR__)->run('echo "Hello World" >&2; exit 1;'); $this->assertFalse($result->successful()); - $this->assertEquals('', $result->output()); - $this->assertEquals("Hello World\n", $result->errorOutput()); + $this->assertSame('', $result->output()); + $this->assertSame("Hello World\n", $result->errorOutput()); } public function testFakeProcessesCanThrowWithoutOutput() @@ -771,14 +771,14 @@ public function testFakeInvokedProcessOutputWithLatestOutput() $output[] = $process->output(); } - $this->assertEquals("ONE\n", $latestOutput[0]); - $this->assertEquals("ONE\nTWO\n", $output[0]); + $this->assertSame("ONE\n", $latestOutput[0]); + $this->assertSame("ONE\nTWO\n", $output[0]); - $this->assertEquals("THREE\n", $latestOutput[1]); - $this->assertEquals("ONE\nTWO\nTHREE\n", $output[1]); + $this->assertSame("THREE\n", $latestOutput[1]); + $this->assertSame("ONE\nTWO\nTHREE\n", $output[1]); - $this->assertEquals('', $latestOutput[2]); - $this->assertEquals("ONE\nTWO\nTHREE\n", $output[2]); + $this->assertSame('', $latestOutput[2]); + $this->assertSame("ONE\nTWO\nTHREE\n", $output[2]); } public function testFakeInvokedProcessWaitUntil() @@ -824,7 +824,7 @@ public function testFakeInvokedProcessWaitUntilWithNoCallback() $this->assertInstanceOf(ProcessResult::class, $result); $this->assertTrue($result->successful()); - $this->assertEquals("OUTPUT\n", $result->output()); + $this->assertSame("OUTPUT\n", $result->output()); } public function testFakeInvokedProcessWaitUntilWithErrorOutput() @@ -960,7 +960,7 @@ public function testFakeInvokedProcessWaitUntilFollowedByWait() $this->assertInstanceOf(ProcessResult::class, $result); $this->assertTrue($result->successful()); $this->assertCount(1, $waitUntilCallbacks); - $this->assertEquals("FIRST\n", $waitUntilCallbacks[0]); + $this->assertSame("FIRST\n", $waitUntilCallbacks[0]); $this->assertCount(2, $waitCallbacks); $this->assertContains("SECOND\n", $waitCallbacks); $this->assertContains("THIRD\n", $waitCallbacks); @@ -1082,7 +1082,7 @@ public function testProcessWithMultipleEnvironmentVariablesAndSequences() ])->run('printenv TEST_VAR OTHER_VAR'); $this->assertTrue($result->successful()); - $this->assertEquals("test_value\nother_value\n", $result->output()); + $this->assertSame("test_value\nother_value\n", $result->output()); $result = $factory->env([ 'TEST_VAR' => 'new_test_value', @@ -1090,7 +1090,7 @@ public function testProcessWithMultipleEnvironmentVariablesAndSequences() ])->run('printenv TEST_VAR OTHER_VAR'); $this->assertTrue($result->successful()); - $this->assertEquals("new_test_value\nnew_other_value\n", $result->output()); + $this->assertSame("new_test_value\nnew_other_value\n", $result->output()); $factory->assertRanTimes(function ($process) { return str_contains($process->command, 'printenv TEST_VAR OTHER_VAR'); diff --git a/tests/Queue/DatabaseUuidFailedJobProviderTest.php b/tests/Queue/DatabaseUuidFailedJobProviderTest.php index e8718e3ae1c0..51820fb5ae36 100644 --- a/tests/Queue/DatabaseUuidFailedJobProviderTest.php +++ b/tests/Queue/DatabaseUuidFailedJobProviderTest.php @@ -52,9 +52,9 @@ public function testFindingFailedJobsById() $provider->log('connection-1', 'queue-1', json_encode(['uuid' => 'uuid-1']), new RuntimeException()); $this->assertNull($provider->find('uuid-2')); - $this->assertEquals('uuid-1', $provider->find('uuid-1')->id); - $this->assertEquals('queue-1', $provider->find('uuid-1')->queue); - $this->assertEquals('connection-1', $provider->find('uuid-1')->connection); + $this->assertSame('uuid-1', $provider->find('uuid-1')->id); + $this->assertSame('queue-1', $provider->find('uuid-1')->queue); + $this->assertSame('connection-1', $provider->find('uuid-1')->connection); } public function testRemovingJobsById() diff --git a/tests/Queue/InteractsWithQueueTest.php b/tests/Queue/InteractsWithQueueTest.php index dcae9eafaa5a..01e561e05d8e 100644 --- a/tests/Queue/InteractsWithQueueTest.php +++ b/tests/Queue/InteractsWithQueueTest.php @@ -15,7 +15,7 @@ public function testCreatesAnExceptionFromString() $queueJob = m::mock(Job::class); $queueJob->shouldReceive('fail')->withArgs(function ($e) { $this->assertInstanceOf(Exception::class, $e); - $this->assertEquals('Whoops!', $e->getMessage()); + $this->assertSame('Whoops!', $e->getMessage()); return true; }); diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 63230f58e95b..842537eb8f12 100755 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -242,7 +242,7 @@ public function testGetQueueProperlyResolvesFifoUrlWithSuffix() { $this->queueName = 'emails.fifo'; $queue = new SqsQueue($this->sqs, $this->queueName, $this->prefix, $suffix = '-staging'); - $this->assertEquals("{$this->prefix}emails-staging.fifo", $queue->getQueue(null)); + $this->assertSame("{$this->prefix}emails-staging.fifo", $queue->getQueue(null)); $queueUrl = $this->baseUrl.'/'.$this->account.'/test'.$suffix.'.fifo'; $this->assertEquals($queueUrl, $queue->getQueue('test.fifo')); } @@ -258,7 +258,7 @@ public function testGetQueueEnsuresTheQueueIsOnlySuffixedOnce() public function testGetFifoQueueEnsuresTheQueueIsOnlySuffixedOnce() { $queue = new SqsQueue($this->sqs, "{$this->queueName}-staging.fifo", $this->prefix, $suffix = '-staging'); - $this->assertEquals("{$this->prefix}{$this->queueName}{$suffix}.fifo", $queue->getQueue(null)); + $this->assertSame("{$this->prefix}{$this->queueName}{$suffix}.fifo", $queue->getQueue(null)); $queueUrl = $this->baseUrl.'/'.$this->account.'/test'.$suffix.'.fifo'; $this->assertEquals($queueUrl, $queue->getQueue('test-staging.fifo')); } diff --git a/tests/Redis/RedisConnectorTest.php b/tests/Redis/RedisConnectorTest.php index 82fbe0b5e44b..b7dda0717353 100644 --- a/tests/Redis/RedisConnectorTest.php +++ b/tests/Redis/RedisConnectorTest.php @@ -251,7 +251,7 @@ public function testPrefixOverrideBehaviour() ], ]); $predisClient1 = $predis1->client(); - $this->assertEquals('test_default_options_', $predisClient1->getOptions()->prefix->getPrefix()); + $this->assertSame('test_default_options_', $predisClient1->getOptions()->prefix->getPrefix()); $predis2 = new RedisManager(new Application, 'predis', [ 'cluster' => false, @@ -271,7 +271,7 @@ public function testPrefixOverrideBehaviour() ], ]); $predisClient2 = $predis2->client(); - $this->assertEquals('test_default_config_', $predisClient2->getOptions()->prefix->getPrefix()); + $this->assertSame('test_default_config_', $predisClient2->getOptions()->prefix->getPrefix()); $phpRedis1 = new RedisManager(new Application, 'phpredis', [ 'cluster' => false, @@ -290,7 +290,7 @@ public function testPrefixOverrideBehaviour() ], ]); $phpRedisClient1 = $phpRedis1->connection()->client(); - $this->assertEquals('test_default_options_', $phpRedisClient1->getOption(Redis::OPT_PREFIX)); + $this->assertSame('test_default_options_', $phpRedisClient1->getOption(Redis::OPT_PREFIX)); $phpRedis2 = new RedisManager(new Application, 'phpredis', [ 'cluster' => false, @@ -310,6 +310,6 @@ public function testPrefixOverrideBehaviour() ], ]); $phpRedisClient2 = $phpRedis2->connection()->client(); - $this->assertEquals('test_default_config_', $phpRedisClient2->getOption(Redis::OPT_PREFIX)); + $this->assertSame('test_default_config_', $phpRedisClient2->getOption(Redis::OPT_PREFIX)); } } diff --git a/tests/Routing/RouteCollectionTest.php b/tests/Routing/RouteCollectionTest.php index e061b9ba777b..1ab1dbfd7ddb 100644 --- a/tests/Routing/RouteCollectionTest.php +++ b/tests/Routing/RouteCollectionTest.php @@ -357,7 +357,7 @@ public function testOverlappingRoutesMatchesFirstRoute() $request = Request::create('users/1/show', 'GET'); $this->assertCount(2, $this->routeCollection->getRoutes()); - $this->assertEquals('first', $this->routeCollection->match($request)->getName()); + $this->assertSame('first', $this->routeCollection->match($request)->getName()); } public function testPrependsRoutesWithDomain() diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index 1fcc8ffcf52f..f94ca8e55c54 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -1320,7 +1320,7 @@ public function testCanRemoveMiddlewareFromGroup() $this->router->removeMiddlewareFromGroup('web', 'test-middleware'); - $this->assertEquals([], $this->router->getMiddlewareGroups()['web']); + $this->assertSame([], $this->router->getMiddlewareGroups()['web']); } public function testCanRemoveMiddlewareFromGroupNotUnregisteredMiddleware() @@ -1329,14 +1329,14 @@ public function testCanRemoveMiddlewareFromGroupNotUnregisteredMiddleware() $this->router->removeMiddlewareFromGroup('web', 'different-test-middleware'); - $this->assertEquals([], $this->router->getMiddlewareGroups()['web']); + $this->assertSame([], $this->router->getMiddlewareGroups()['web']); } public function testCanRemoveMiddlewareFromGroupUnregisteredGroup() { $this->router->removeMiddlewareFromGroup('web', ['test-middleware']); - $this->assertEquals([], $this->router->getMiddlewareGroups()); + $this->assertSame([], $this->router->getMiddlewareGroups()); } public function testCanRegisterSingleton() diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index ad240fe15d30..b653ce430420 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -708,7 +708,7 @@ public function testControllerCallActionMethodParameters() unset($_SERVER['__test.controller_callAction_parameters']); $router->get(($str = Str::random()).'', RouteTestAnotherControllerWithParameterStub::class.'@oneArgument'); $router->dispatch(Request::create($str, 'GET')); - $this->assertEquals([], $_SERVER['__test.controller_callAction_parameters']); + $this->assertSame([], $_SERVER['__test.controller_callAction_parameters']); // With model bindings unset($_SERVER['__test.controller_callAction_parameters']); diff --git a/tests/Routing/RoutingSortedMiddlewareTest.php b/tests/Routing/RoutingSortedMiddlewareTest.php index 4416f9be3686..e302f127dc2e 100644 --- a/tests/Routing/RoutingSortedMiddlewareTest.php +++ b/tests/Routing/RoutingSortedMiddlewareTest.php @@ -41,7 +41,7 @@ public function testMiddlewareCanBeSortedByPriority() $this->assertEquals($expected, (new SortedMiddleware($priority, $middleware))->all()); - $this->assertEquals([], (new SortedMiddleware(['First'], []))->all()); + $this->assertSame([], (new SortedMiddleware(['First'], []))->all()); $this->assertEquals(['First'], (new SortedMiddleware(['First'], ['First']))->all()); $this->assertEquals(['First', 'Second'], (new SortedMiddleware(['First', 'Second'], ['Second', 'First']))->all()); } diff --git a/tests/Session/CacheBasedSessionHandlerTest.php b/tests/Session/CacheBasedSessionHandlerTest.php index 76fa6acd6200..d437e4a14254 100644 --- a/tests/Session/CacheBasedSessionHandlerTest.php +++ b/tests/Session/CacheBasedSessionHandlerTest.php @@ -37,7 +37,7 @@ public function test_read_returns_data_from_cache() $this->cacheMock->shouldReceive('get')->once()->with('session_id', '')->andReturn('session_data'); $data = $this->sessionHandler->read(sessionId: 'session_id'); - $this->assertEquals('session_data', $data); + $this->assertSame('session_data', $data); } public function test_read_returns_empty_string_if_no_data() @@ -45,7 +45,7 @@ public function test_read_returns_empty_string_if_no_data() $this->cacheMock->shouldReceive('get')->once()->with('some_id', '')->andReturn(''); $data = $this->sessionHandler->read(sessionId: 'some_id'); - $this->assertEquals('', $data); + $this->assertSame('', $data); } public function test_write_stores_data_in_cache() diff --git a/tests/Session/FileSessionHandlerTest.php b/tests/Session/FileSessionHandlerTest.php index ab5a2d5d2ffb..e14ef4c43c98 100644 --- a/tests/Session/FileSessionHandlerTest.php +++ b/tests/Session/FileSessionHandlerTest.php @@ -49,7 +49,7 @@ public function test_read_returns_data_when_file_exists_and_is_valid() $result = $this->sessionHandler->read($sessionId); - $this->assertEquals('session_data', $result); + $this->assertSame('session_data', $result); } public function test_read_returns_data_when_file_exists_but_expired() @@ -66,7 +66,7 @@ public function test_read_returns_data_when_file_exists_but_expired() $result = $this->sessionHandler->read($sessionId); - $this->assertEquals('', $result); + $this->assertSame('', $result); } public function test_read_returns_empty_string_when_file_does_not_exist() @@ -79,7 +79,7 @@ public function test_read_returns_empty_string_when_file_does_not_exist() $result = $this->sessionHandler->read($sessionId); - $this->assertEquals('', $result); + $this->assertSame('', $result); } public function test_write_stores_data() diff --git a/tests/Session/Middleware/AuthenticateSessionTest.php b/tests/Session/Middleware/AuthenticateSessionTest.php index de9a078d9d70..3a4cae096b65 100644 --- a/tests/Session/Middleware/AuthenticateSessionTest.php +++ b/tests/Session/Middleware/AuthenticateSessionTest.php @@ -24,7 +24,7 @@ public function test_handle_without_session() $middleware = new AuthenticateSession($authFactory); $response = $middleware->handle($request, $next); - $this->assertEquals('next-1', $response); + $this->assertSame('next-1', $response); } public function test_handle_with_session_without_request_user() @@ -40,7 +40,7 @@ public function test_handle_with_session_without_request_user() $next = fn () => 'next-2'; $middleware = new AuthenticateSession($authFactory); $response = $middleware->handle($request, $next); - $this->assertEquals('next-2', $response); + $this->assertSame('next-2', $response); } public function test_handle_with_session_without_auth_password() @@ -67,7 +67,7 @@ public function getAuthPassword() $middleware = new AuthenticateSession($authFactory); $response = $middleware->handle($request, $next); - $this->assertEquals('next-3', $response); + $this->assertSame('next-3', $response); } public function test_handle_with_session_with_user_auth_password_on_request_via_remember_false() @@ -96,8 +96,8 @@ public function getAuthPassword() $middleware = new AuthenticateSession($authFactory); $response = $middleware->handle($request, fn () => 'next-4'); - $this->assertEquals('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web')); - $this->assertEquals('next-4', $response); + $this->assertSame('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web')); + $this->assertSame('next-4', $response); } public function test_handle_with_invalid_password_hash() @@ -140,9 +140,9 @@ public function getAuthPassword() $middleware->handle($request, fn () => 'next-7'); } catch (AuthenticationException $e) { $message = $e->getMessage(); - $this->assertEquals('i-wanna-go-home', $e->redirectTo($request)); + $this->assertSame('i-wanna-go-home', $e->redirectTo($request)); } - $this->assertEquals('Unauthenticated.', $message); + $this->assertSame('Unauthenticated.', $message); // ensure session is flushed: $this->assertNull($session->get('a')); @@ -185,7 +185,7 @@ public function getAuthPassword() } catch (AuthenticationException $e) { $message = $e->getMessage(); } - $this->assertEquals('Unauthenticated.', $message); + $this->assertSame('Unauthenticated.', $message); // ensure session is flushed $this->assertNull($session->get('password_hash_web')); @@ -230,7 +230,7 @@ public function getAuthPassword() } catch (AuthenticationException $e) { $message = $e->getMessage(); } - $this->assertEquals('Unauthenticated.', $message); + $this->assertSame('Unauthenticated.', $message); // ensure session is flushed: $this->assertNull($session->get('password_hash_web')); @@ -271,11 +271,11 @@ public function getAuthPassword() $middleware = new AuthenticateSession($authFactory); $response = $middleware->handle($request, fn () => 'next-8'); - $this->assertEquals('next-8', $response); + $this->assertSame('next-8', $response); // ensure session is flushed: - $this->assertEquals('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web')); - $this->assertEquals('1', $session->get('a')); - $this->assertEquals('2', $session->get('b')); + $this->assertSame('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web')); + $this->assertSame('1', $session->get('a')); + $this->assertSame('2', $session->get('b')); } public function test_handle_with_old_format_cookie_for_backward_compatibility() @@ -311,11 +311,11 @@ public function getAuthPassword() $response = $middleware->handle($request, fn () => 'next-9'); // Should succeed because of backward compatibility fallback - $this->assertEquals('next-9', $response); + $this->assertSame('next-9', $response); // Session should be updated to new format (HMAC) - $this->assertEquals('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web')); - $this->assertEquals('1', $session->get('a')); - $this->assertEquals('2', $session->get('b')); + $this->assertSame('mac:my-pass-(*&^%$#!@', $session->get('password_hash_web')); + $this->assertSame('1', $session->get('a')); + $this->assertSame('2', $session->get('b')); } public function test_handle_with_old_format_cookie_and_legacy_guard() @@ -351,10 +351,10 @@ public function getAuthPassword() $response = $middleware->handle($request, fn () => 'next-9'); // Should succeed because of backward compatibility fallback - $this->assertEquals('next-9', $response); + $this->assertSame('next-9', $response); // Session should stay intact - $this->assertEquals('my-pass-(*&^%$#!@', $session->get('password_hash_web')); - $this->assertEquals('1', $session->get('a')); - $this->assertEquals('2', $session->get('b')); + $this->assertSame('my-pass-(*&^%$#!@', $session->get('password_hash_web')); + $this->assertSame('1', $session->get('a')); + $this->assertSame('2', $session->get('b')); } } diff --git a/tests/Session/SessionStoreTest.php b/tests/Session/SessionStoreTest.php index 948c7aebbb08..5b2539ad42e6 100644 --- a/tests/Session/SessionStoreTest.php +++ b/tests/Session/SessionStoreTest.php @@ -816,7 +816,7 @@ public function testValidationErrorsCanBeReadAsJson() $this->assertInstanceOf(ViewErrorBag::class, $errors); $this->assertInstanceOf(MessageBag::class, $errors->getBags()['default']); - $this->assertEquals('

:message

', $errors->getBags()['default']->getFormat()); + $this->assertSame('

:message

', $errors->getBags()['default']->getFormat()); $this->assertEquals(['first_name' => [ 'Your first name is required', 'Your first name must be at least 1 character', diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index f3e60953b9bf..73d42f32420b 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -108,7 +108,7 @@ public function testCollapse() // Case with empty two-dimensional arrays $emptyArray = [[], [], []]; - $this->assertEquals([], Arr::collapse($emptyArray)); + $this->assertSame([], Arr::collapse($emptyArray)); // Case with both empty arrays and arrays with elements $mixedArray = [[], [1, 2], [], ['foo', 'bar']]; @@ -170,8 +170,8 @@ public function testDivide(): void { // Test dividing an empty array [$keys, $values] = Arr::divide([]); - $this->assertEquals([], $keys); - $this->assertEquals([], $values); + $this->assertSame([], $keys); + $this->assertSame([], $values); // Test dividing an array with a single key-value pair [$keys, $values] = Arr::divide(['name' => 'Desk']); @@ -349,16 +349,16 @@ public function testExceptValues() $array = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 3]; $this->assertEquals(['b' => 2, 'd' => 3], Arr::exceptValues($array, 1)); - $this->assertEquals([], Arr::exceptValues([], 'foo')); + $this->assertSame([], Arr::exceptValues([], 'foo')); $this->assertEquals(['foo', 'bar'], Arr::exceptValues(['foo', 'bar'], [])); $array = [1, '1', 2, '2', 3]; $this->assertEquals([1 => '1', 3 => '2'], Arr::exceptValues($array, [1, 2, 3], true)); - $this->assertEquals([], Arr::exceptValues($array, [1, 2, 3])); + $this->assertSame([], Arr::exceptValues($array, [1, 2, 3])); $array = ['a' => true, 'b' => false, 'c' => 1, 'd' => 0]; $this->assertEquals(['a' => true, 'b' => false], Arr::exceptValues($array, [1, 0], true)); - $this->assertEquals([], Arr::exceptValues($array, [1, 0])); + $this->assertSame([], Arr::exceptValues($array, [1, 0])); } public function testExists() @@ -384,7 +384,7 @@ public function testWhereNotNull(): void $this->assertEquals([1, 2, 3], $array); $array = array_values(Arr::whereNotNull([null, null, null])); - $this->assertEquals([], $array); + $this->assertSame([], $array); $array = array_values(Arr::whereNotNull(['a', null, 'b', null, 'c'])); $this->assertEquals(['a', 'b', 'c'], $array); @@ -939,8 +939,8 @@ public function testOnlyValues() $array = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 3]; $this->assertEquals(['a' => 1, 'c' => 1], Arr::onlyValues($array, 1)); - $this->assertEquals([], Arr::onlyValues([], 'foo')); - $this->assertEquals([], Arr::onlyValues(['foo', 'bar'], [])); + $this->assertSame([], Arr::onlyValues([], 'foo')); + $this->assertSame([], Arr::onlyValues(['foo', 'bar'], [])); $array = [1, '1', 2, '2', 3]; $this->assertEquals([0 => 1, 2 => 2, 4 => 3], Arr::onlyValues($array, [1, 2, 3], true)); @@ -1091,7 +1091,7 @@ public function testMapWithEmptyArray() $mapped = Arr::map([], static function ($value, $key) { return $key.'-'.$value; }); - $this->assertEquals([], $mapped); + $this->assertSame([], $mapped); } public function testMapNullValues() @@ -1410,7 +1410,7 @@ public function testSoleThrowsExceptionIfMoreThanOneItemExists() public function testEmptyShuffle() { - $this->assertEquals([], Arr::shuffle([])); + $this->assertSame([], Arr::shuffle([])); } public function testSort() @@ -1713,7 +1713,8 @@ public function testFrom() $this->assertSame($subject, Arr::from($items)); $items = new WeakMap; - $items[$temp = new class {}] = 'bar'; + $items[$temp = new class { + }] = 'bar'; $this->assertSame(['bar'], Arr::from($items)); $this->expectException(InvalidArgumentException::class); @@ -1730,7 +1731,7 @@ public function testWrap() $this->assertEquals(['a'], Arr::wrap($string)); $this->assertEquals($array, Arr::wrap($array)); $this->assertEquals([$object], Arr::wrap($object)); - $this->assertEquals([], Arr::wrap(null)); + $this->assertSame([], Arr::wrap(null)); $this->assertEquals([null], Arr::wrap([null])); $this->assertEquals([null, null], Arr::wrap([null, null])); $this->assertEquals([''], Arr::wrap('')); @@ -1846,7 +1847,7 @@ public function testTake(): void $this->assertEquals([4, 5, 6], Arr::take($array, -3)); // Test with zero limit, should return an empty array. - $this->assertEquals([], Arr::take($array, 0)); + $this->assertSame([], Arr::take($array, 0)); // Test with a limit greater than the array size, should return the entire array. $this->assertEquals([1, 2, 3, 4, 5, 6], Arr::take($array, 10)); diff --git a/tests/Support/SupportCarbonTest.php b/tests/Support/SupportCarbonTest.php index 9500e2559469..df035124e4e8 100644 --- a/tests/Support/SupportCarbonTest.php +++ b/tests/Support/SupportCarbonTest.php @@ -127,19 +127,19 @@ public function testCarbonIsConditionable() public function testCreateFromUid() { $ulid = Carbon::createFromId('01DXH9C4P0ED4AGJJP9CRKQ55C'); - $this->assertEquals('2020-01-01 19:30:00.000000', $ulid->toDateTimeString('microsecond')); + $this->assertSame('2020-01-01 19:30:00.000000', $ulid->toDateTimeString('microsecond')); $uuidv1 = Carbon::createFromId('71513cb4-f071-11ed-a0cf-325096b39f47'); - $this->assertEquals('2023-05-12 03:02:34.147346', $uuidv1->toDateTimeString('microsecond')); + $this->assertSame('2023-05-12 03:02:34.147346', $uuidv1->toDateTimeString('microsecond')); $uuidv2 = Carbon::createFromId('000003e8-f072-21ed-9200-325096b39f47'); - $this->assertEquals('2023-05-12 03:06:33.529139', $uuidv2->toDateTimeString('microsecond')); + $this->assertSame('2023-05-12 03:06:33.529139', $uuidv2->toDateTimeString('microsecond')); $uuidv6 = Carbon::createFromId('1edf0746-5d1c-6ce8-88ad-e0cb4effa035'); - $this->assertEquals('2023-05-12 03:23:43.347428', $uuidv6->toDateTimeString('microsecond')); + $this->assertSame('2023-05-12 03:23:43.347428', $uuidv6->toDateTimeString('microsecond')); $uuidv7 = Carbon::createFromId('01880dfa-2825-72e4-acbb-b1e4981cf8af'); - $this->assertEquals('2023-05-12 03:21:18.117000', $uuidv7->toDateTimeString('microsecond')); + $this->assertSame('2023-05-12 03:21:18.117000', $uuidv7->toDateTimeString('microsecond')); } public function testPlus(): void diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 3506655b6868..70a430205408 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -1187,7 +1187,7 @@ public function testWhere($collection) ]); $this->assertEquals([['v' => 2, 'g' => 3]], $c->where('v', 2)->where('g', 3)->values()->all()); $this->assertEquals([['v' => 2, 'g' => 3]], $c->where('v', 2)->where('g', '>', 2)->values()->all()); - $this->assertEquals([], $c->where('v', 2)->where('g', 4)->values()->all()); + $this->assertSame([], $c->where('v', 2)->where('g', 4)->values()->all()); $this->assertEquals([['v' => 2, 'g' => null]], $c->where('v', 2)->whereNull('g')->values()->all()); } @@ -1216,7 +1216,7 @@ public function testWhereIn($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); $this->assertEquals([['v' => 1], ['v' => 3], ['v' => '3']], $c->whereIn('v', [1, 3])->values()->all()); - $this->assertEquals([], $c->whereIn('v', [2])->whereIn('v', [1, 3])->values()->all()); + $this->assertSame([], $c->whereIn('v', [2])->whereIn('v', [1, 3])->values()->all()); $this->assertEquals([['v' => 1]], $c->whereIn('v', [1])->whereIn('v', [1, 3])->values()->all()); } @@ -1263,8 +1263,8 @@ public function testValue($collection) { $c = new $collection([['id' => 1, 'name' => 'Hello'], ['id' => 2, 'name' => 'World']]); - $this->assertEquals('Hello', $c->value('name')); - $this->assertEquals('World', $c->where('id', 2)->value('name')); + $this->assertSame('Hello', $c->value('name')); + $this->assertSame('World', $c->where('id', 2)->value('name')); $c = new $collection([ ['id' => 1, 'pivot' => ['value' => 'foo']], @@ -1272,8 +1272,8 @@ public function testValue($collection) ]); $this->assertEquals(['value' => 'foo'], $c->value('pivot')); - $this->assertEquals('foo', $c->value('pivot.value')); - $this->assertEquals('bar', $c->where('id', 2)->value('pivot.value')); + $this->assertSame('foo', $c->value('pivot.value')); + $this->assertSame('bar', $c->where('id', 2)->value('pivot.value')); } #[DataProvider('collectionClassProvider')] @@ -1294,7 +1294,7 @@ public function testValueWithNegativeValue($collection) $c = new $collection([['id' => 1, 'balance' => ''], ['id' => 2, 'balance' => 200]]); - $this->assertEquals('', $c->value('balance')); + $this->assertSame('', $c->value('balance')); $c = new $collection([['id' => 1, 'balance' => null], ['id' => 2, 'balance' => 200]]); @@ -1318,7 +1318,7 @@ public function testValueWithObjects($collection) literal(id: 3, balance: 200), ]); - $this->assertEquals('', $c->value('balance')); + $this->assertSame('', $c->value('balance')); $c = new $collection([ literal(id: 1), @@ -1463,8 +1463,8 @@ public function testMultiplyCollection($collection) { $c = new $collection(['Hello', 1, ['tags' => ['a', 'b'], 'admin']]); - $this->assertEquals([], $c->multiply(-1)->all()); - $this->assertEquals([], $c->multiply(0)->all()); + $this->assertSame([], $c->multiply(-1)->all()); + $this->assertSame([], $c->multiply(0)->all()); $this->assertEquals( ['Hello', 1, ['tags' => ['a', 'b'], 'admin']], @@ -1757,7 +1757,7 @@ public function testEachSpread($collection) public function testIntersectNull($collection) { $c = new $collection(['id' => 1, 'first_word' => 'Hello']); - $this->assertEquals([], $c->intersect(null)->all()); + $this->assertSame([], $c->intersect(null)->all()); } #[DataProvider('collectionClassProvider')] @@ -1772,7 +1772,7 @@ public function testIntersectUsingWithNull($collection) { $collect = new $collection(['green', 'brown', 'blue']); - $this->assertEquals([], $collect->intersectUsing(null, 'strcasecmp')->all()); + $this->assertSame([], $collect->intersectUsing(null, 'strcasecmp')->all()); } #[DataProvider('collectionClassProvider')] @@ -1788,7 +1788,7 @@ public function testIntersectAssocWithNull($collection) { $array1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); - $this->assertEquals([], $array1->intersectAssoc(null)->all()); + $this->assertSame([], $array1->intersectAssoc(null)->all()); } #[DataProvider('collectionClassProvider')] @@ -1805,7 +1805,7 @@ public function testIntersectAssocUsingWithNull($collection) { $array1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); - $this->assertEquals([], $array1->intersectAssocUsing(null, 'strcasecmp')->all()); + $this->assertSame([], $array1->intersectAssocUsing(null, 'strcasecmp')->all()); } #[DataProvider('collectionClassProvider')] @@ -1821,7 +1821,7 @@ public function testIntersectAssocUsingCollection($collection) public function testIntersectByKeysNull($collection) { $c = new $collection(['name' => 'Mateus', 'age' => 18]); - $this->assertEquals([], $c->intersectByKeys(null)->all()); + $this->assertSame([], $c->intersectByKeys(null)->all()); } #[DataProvider('collectionClassProvider')] @@ -1920,7 +1920,7 @@ public function testCollapse($collection) // Case with empty two-dimensional arrays $data = new $collection([[], [], []]); - $this->assertEquals([], $data->collapse()->all()); + $this->assertSame([], $data->collapse()->all()); // Case with both empty arrays and arrays with elements $data = new $collection([[], [1, 2], [], ['foo', 'bar']]); @@ -1947,7 +1947,7 @@ public function testCollapseWithKeys($collection) // Case with an already flat collection $data = new $collection(['a', 'b', 'c']); - $this->assertEquals([], $data->collapseWithKeys()->all()); + $this->assertSame([], $data->collapseWithKeys()->all()); } #[DataProvider('collectionClassProvider')] @@ -2886,10 +2886,10 @@ public function testMakeMethod($collection) public function testMakeMethodFromNull($collection) { $data = $collection::make(null); - $this->assertEquals([], $data->all()); + $this->assertSame([], $data->all()); $data = $collection::make(); - $this->assertEquals([], $data->all()); + $this->assertSame([], $data->all()); } #[DataProvider('collectionClassProvider')] @@ -3090,10 +3090,10 @@ public function testConstructMethod($collection) public function testConstructMethodFromNull($collection) { $data = new $collection(null); - $this->assertEquals([], $data->all()); + $this->assertSame([], $data->all()); $data = new $collection; - $this->assertEquals([], $data->all()); + $this->assertSame([], $data->all()); } #[DataProvider('collectionClassProvider')] @@ -4101,7 +4101,7 @@ public function testPullRemovesItemFromCollection() $c->pull(0); $this->assertEquals([1 => 'bar'], $c->all()); $c->pull(1); - $this->assertEquals([], $c->all()); + $this->assertSame([], $c->all()); } public function testPullRemovesItemFromNestedCollection() @@ -4258,7 +4258,7 @@ public function testBeforeInStrictMode($collection) $this->assertEquals(false, $c->before(0, true)); $this->assertEquals(0, $c->before(1, true)); $this->assertEquals(1, $c->before([], true)); - $this->assertEquals([], $c->before('', true)); + $this->assertSame([], $c->before('', true)); } #[DataProvider('collectionClassProvider')] @@ -4299,13 +4299,13 @@ public function testAfterReturnsItemAfterTheGivenItem($collection) $this->assertEquals(3, $c->after(2)); $this->assertEquals(4, $c->after(3)); $this->assertEquals(2, $c->after(4)); - $this->assertEquals('taylor', $c->after(5)); - $this->assertEquals('laravel', $c->after('taylor')); + $this->assertSame('taylor', $c->after(5)); + $this->assertSame('laravel', $c->after('taylor')); $this->assertEquals(4, $c->after(function ($value) { return $value > 2; })); - $this->assertEquals('laravel', $c->after(function ($value) { + $this->assertSame('laravel', $c->after(function ($value) { return ! is_numeric($value); })); } @@ -4319,8 +4319,8 @@ public function testAfterInStrictMode($collection) $this->assertNull($c->after('1', true)); $this->assertNull($c->after('', true)); $this->assertEquals(0, $c->after(false, true)); - $this->assertEquals([], $c->after(1, true)); - $this->assertEquals('', $c->after([], true)); + $this->assertSame([], $c->after(1, true)); + $this->assertSame('', $c->after([], true)); } #[DataProvider('collectionClassProvider')] @@ -4369,7 +4369,7 @@ public function testPaginate($collection) $this->assertEquals(['one', 'two'], $c->forPage(0, 2)->all()); $this->assertEquals(['one', 'two'], $c->forPage(1, 2)->all()); $this->assertEquals([2 => 'three', 3 => 'four'], $c->forPage(2, 2)->all()); - $this->assertEquals([], $c->forPage(3, 2)->all()); + $this->assertSame([], $c->forPage(3, 2)->all()); } #[IgnoreDeprecations] @@ -4726,7 +4726,7 @@ public function testGettingAvgItemsFromCollection($collection) $c = new $collection([['foo' => 1], ['foo' => 2]]); $this->assertIsFloat($c->avg('foo')); - $this->assertEquals(1.5, $c->avg('foo')); + $this->assertSame(1.5, $c->avg('foo')); $c = new $collection([ ['foo' => 1], ['foo' => 2], @@ -5019,7 +5019,7 @@ public function testEvenMedianCollection($collection) (object) ['foo' => 0], (object) ['foo' => 3], ]); - $this->assertEquals(1.5, $data->median('foo')); + $this->assertSame(1.5, $data->median('foo')); } #[DataProvider('collectionClassProvider')] @@ -5772,7 +5772,7 @@ public function testGetWithNullReturnsNull($collection) public function testGetWithDefaultValue($collection) { $data = new $collection(['name' => 'taylor', 'framework' => 'laravel']); - $this->assertEquals('34', $data->get('age', 34)); + $this->assertSame('34', (string) $data->get('age', 34)); } #[DataProvider('collectionClassProvider')] @@ -5782,7 +5782,7 @@ public function testGetWithCallbackAsDefaultValue($collection) $result = $data->get('email', function () { return 'taylor@example.com'; }); - $this->assertEquals('taylor@example.com', $result); + $this->assertSame('taylor@example.com', $result); } #[DataProvider('collectionClassProvider')] diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 738a79890a93..a3f86f30ff87 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -57,7 +57,7 @@ public function testE() public function testEWithInvalidCodePoints() { $str = mb_convert_encoding('føø bar', 'ISO-8859-1', 'UTF-8'); - $this->assertEquals('f�� bar', e($str)); + $this->assertSame('f�� bar', e($str)); } public function testEWithEnums() @@ -127,26 +127,26 @@ public function testClassBasename() public function testWhen() { - $this->assertEquals('Hello', when(true, 'Hello')); + $this->assertSame('Hello', when(true, 'Hello')); $this->assertNull(when(false, 'Hello')); - $this->assertEquals('There', when(1 === 1, 'There')); // strict types - $this->assertEquals('There', when(1 == '1', 'There')); // loose types + $this->assertSame('There', when(1 === 1, 'There')); // strict types + $this->assertSame('There', when(1 == '1', 'There')); // loose types $this->assertNull(when(1 == 2, 'There')); $this->assertNull(when('1', fn () => null)); $this->assertNull(when(0, fn () => null)); - $this->assertEquals('True', when([1, 2, 3, 4], 'True')); // Array + $this->assertSame('True', when([1, 2, 3, 4], 'True')); // Array $this->assertNull(when([], 'True')); // Empty Array = Falsy - $this->assertEquals('True', when(new StdClass, fn () => 'True')); // Object - $this->assertEquals('World', when(false, 'Hello', 'World')); - $this->assertEquals('World', when(1 === 0, 'Hello', 'World')); // strict types - $this->assertEquals('World', when(1 == '0', 'Hello', 'World')); // loose types + $this->assertSame('True', when(new StdClass, fn () => 'True')); // Object + $this->assertSame('World', when(false, 'Hello', 'World')); + $this->assertSame('World', when(1 === 0, 'Hello', 'World')); // strict types + $this->assertSame('World', when(1 == '0', 'Hello', 'World')); // loose types $this->assertNull(when('', fn () => 'There', fn () => null)); $this->assertNull(when(0, fn () => 'There', fn () => null)); - $this->assertEquals('False', when([], 'True', 'False')); // Empty Array = Falsy + $this->assertSame('False', when([], 'True', 'False')); // Empty Array = Falsy $this->assertTrue(when(true, fn ($value) => $value, fn ($value) => ! $value)); // lazy evaluation $this->assertTrue(when(false, fn ($value) => $value, fn ($value) => ! $value)); // lazy evaluation - $this->assertEquals('Hello', when(fn () => true, 'Hello')); // lazy evaluation condition - $this->assertEquals('World', when(fn () => false, 'Hello', 'World')); // lazy evaluation condition + $this->assertSame('Hello', when(fn () => true, 'Hello')); // lazy evaluation condition + $this->assertSame('World', when(fn () => false, 'Hello', 'World')); // lazy evaluation condition } public function testFilled() @@ -340,8 +340,8 @@ public function testDataGetWithDoubleNestedArraysCollapsesResult() $this->assertEquals(['taylor', 'abigail', 'abigail', 'dayle', 'dayle', 'taylor'], data_get($array, 'posts.*.comments.*.author')); $this->assertEquals([4, 3, 2, null, null, 1], data_get($array, 'posts.*.comments.*.likes')); - $this->assertEquals([], data_get($array, 'posts.*.users.*.name', 'irrelevant')); - $this->assertEquals([], data_get($array, 'posts.*.users.*.name')); + $this->assertSame([], data_get($array, 'posts.*.users.*.name', 'irrelevant')); + $this->assertSame([], data_get($array, 'posts.*.users.*.name')); } public function testDataGetFirstLastDirectives() @@ -364,13 +364,13 @@ public function testDataGetFirstLastDirectives() 'empty' => [], ]; - $this->assertEquals('LHR', data_get($array, 'flights.0.segments.{first}.from')); - $this->assertEquals('PKX', data_get($array, 'flights.0.segments.{last}.to')); + $this->assertSame('LHR', data_get($array, 'flights.0.segments.{first}.from')); + $this->assertSame('PKX', data_get($array, 'flights.0.segments.{last}.to')); - $this->assertEquals('LHR', data_get($array, 'flights.{first}.segments.{first}.from')); - $this->assertEquals('PEK', data_get($array, 'flights.{last}.segments.{last}.to')); - $this->assertEquals('PKX', data_get($array, 'flights.{first}.segments.{last}.to')); - $this->assertEquals('LGW', data_get($array, 'flights.{last}.segments.{first}.from')); + $this->assertSame('LHR', data_get($array, 'flights.{first}.segments.{first}.from')); + $this->assertSame('PEK', data_get($array, 'flights.{last}.segments.{last}.to')); + $this->assertSame('PKX', data_get($array, 'flights.{first}.segments.{last}.to')); + $this->assertSame('LGW', data_get($array, 'flights.{last}.segments.{first}.from')); $this->assertEquals(['LHR', 'IST'], data_get($array, 'flights.{first}.segments.*.from')); $this->assertEquals(['SAW', 'PEK'], data_get($array, 'flights.{last}.segments.*.to')); @@ -378,8 +378,8 @@ public function testDataGetFirstLastDirectives() $this->assertEquals(['LHR', 'LGW'], data_get($array, 'flights.*.segments.{first}.from')); $this->assertEquals(['PKX', 'PEK'], data_get($array, 'flights.*.segments.{last}.to')); - $this->assertEquals('Not found', data_get($array, 'empty.{first}', 'Not found')); - $this->assertEquals('Not found', data_get($array, 'empty.{last}', 'Not found')); + $this->assertSame('Not found', data_get($array, 'empty.{first}', 'Not found')); + $this->assertSame('Not found', data_get($array, 'empty.{last}', 'Not found')); } public function testDataGetFirstLastDirectivesOnArrayAccessIterable() @@ -402,13 +402,13 @@ public function testDataGetFirstLastDirectivesOnArrayAccessIterable() 'empty' => new SupportTestArrayAccessIterable([]), ]; - $this->assertEquals('LHR', data_get($arrayAccessIterable, 'flights.0.segments.{first}.from')); - $this->assertEquals('PKX', data_get($arrayAccessIterable, 'flights.0.segments.{last}.to')); + $this->assertSame('LHR', data_get($arrayAccessIterable, 'flights.0.segments.{first}.from')); + $this->assertSame('PKX', data_get($arrayAccessIterable, 'flights.0.segments.{last}.to')); - $this->assertEquals('LHR', data_get($arrayAccessIterable, 'flights.{first}.segments.{first}.from')); - $this->assertEquals('PEK', data_get($arrayAccessIterable, 'flights.{last}.segments.{last}.to')); - $this->assertEquals('PKX', data_get($arrayAccessIterable, 'flights.{first}.segments.{last}.to')); - $this->assertEquals('LGW', data_get($arrayAccessIterable, 'flights.{last}.segments.{first}.from')); + $this->assertSame('LHR', data_get($arrayAccessIterable, 'flights.{first}.segments.{first}.from')); + $this->assertSame('PEK', data_get($arrayAccessIterable, 'flights.{last}.segments.{last}.to')); + $this->assertSame('PKX', data_get($arrayAccessIterable, 'flights.{first}.segments.{last}.to')); + $this->assertSame('LGW', data_get($arrayAccessIterable, 'flights.{last}.segments.{first}.from')); $this->assertEquals(['LHR', 'IST'], data_get($arrayAccessIterable, 'flights.{first}.segments.*.from')); $this->assertEquals(['SAW', 'PEK'], data_get($arrayAccessIterable, 'flights.{last}.segments.*.to')); @@ -416,8 +416,8 @@ public function testDataGetFirstLastDirectivesOnArrayAccessIterable() $this->assertEquals(['LHR', 'LGW'], data_get($arrayAccessIterable, 'flights.*.segments.{first}.from')); $this->assertEquals(['PKX', 'PEK'], data_get($arrayAccessIterable, 'flights.*.segments.{last}.to')); - $this->assertEquals('Not found', data_get($arrayAccessIterable, 'empty.{first}', 'Not found')); - $this->assertEquals('Not found', data_get($arrayAccessIterable, 'empty.{last}', 'Not found')); + $this->assertSame('Not found', data_get($arrayAccessIterable, 'empty.{first}', 'Not found')); + $this->assertSame('Not found', data_get($arrayAccessIterable, 'empty.{last}', 'Not found')); } public function testDataGetFirstLastDirectivesOnKeyedArrays() @@ -435,11 +435,11 @@ public function testDataGetFirstLastDirectivesOnKeyedArrays() ], ]; - $this->assertEquals('second', data_get($array, 'numericKeys.0')); - $this->assertEquals('first', data_get($array, 'numericKeys.{first}')); - $this->assertEquals('last', data_get($array, 'numericKeys.{last}')); - $this->assertEquals('first', data_get($array, 'stringKeys.{first}')); - $this->assertEquals('last', data_get($array, 'stringKeys.{last}')); + $this->assertSame('second', data_get($array, 'numericKeys.0')); + $this->assertSame('first', data_get($array, 'numericKeys.{first}')); + $this->assertSame('last', data_get($array, 'numericKeys.{last}')); + $this->assertSame('first', data_get($array, 'stringKeys.{first}')); + $this->assertSame('last', data_get($array, 'stringKeys.{last}')); } public function testDataGetEscapedSegmentKeys() @@ -452,12 +452,12 @@ public function testDataGetEscapedSegmentKeys() ], ]; - $this->assertEquals('caret', data_get($array, 'symbols.\{first}.description')); - $this->assertEquals('dollar', data_get($array, 'symbols.{first}.description')); - $this->assertEquals('asterisk', data_get($array, 'symbols.\*.description')); + $this->assertSame('caret', data_get($array, 'symbols.\{first}.description')); + $this->assertSame('dollar', data_get($array, 'symbols.{first}.description')); + $this->assertSame('asterisk', data_get($array, 'symbols.\*.description')); $this->assertEquals(['dollar', 'asterisk', 'caret'], data_get($array, 'symbols.*.description')); - $this->assertEquals('dollar', data_get($array, 'symbols.\{last}.description')); - $this->assertEquals('caret', data_get($array, 'symbols.{last}.description')); + $this->assertSame('dollar', data_get($array, 'symbols.\{last}.description')); + $this->assertSame('caret', data_get($array, 'symbols.{last}.description')); } public function testDataGetStar() @@ -1557,7 +1557,7 @@ public function testRequiredEnvReturnsValue(): void public function testLiteral(): void { $this->assertEquals(1, literal(1)); - $this->assertEquals('taylor', literal('taylor')); + $this->assertSame('taylor', literal('taylor')); $this->assertEquals((object) ['name' => 'Taylor', 'role' => 'Developer'], literal(name: 'Taylor', role: 'Developer')); } diff --git a/tests/Support/SupportHtmlStringTest.php b/tests/Support/SupportHtmlStringTest.php index f349a8a1ad2a..6c06c7ad377d 100644 --- a/tests/Support/SupportHtmlStringTest.php +++ b/tests/Support/SupportHtmlStringTest.php @@ -26,7 +26,7 @@ public function testToHtml(): void // Check if HtmlString correctly handles an empty string $emptyHtml = new HtmlString(''); - $this->assertEquals('', $emptyHtml->toHtml()); + $this->assertSame('', $emptyHtml->toHtml()); // Check if HtmlString correctly converts a plain text string $str = 'foo bar'; diff --git a/tests/Support/SupportJsTest.php b/tests/Support/SupportJsTest.php index 2bd875df5c0a..648ab94e6d3d 100644 --- a/tests/Support/SupportJsTest.php +++ b/tests/Support/SupportJsTest.php @@ -24,7 +24,7 @@ public function testScalars() $this->assertSame('null', (string) Js::from(null)); $this->assertSame("'Hello world'", (string) Js::from('Hello world')); $this->assertSame("'Hèlló world'", (string) Js::from('Hèlló world')); - $this->assertEquals( + $this->assertSame( "'\\u003Cdiv class=\\u0022foo\\u0022\\u003E\\u0027quoted html\\u0027\\u003C\\/div\\u003E'", (string) Js::from('
\'quoted html\'
') ); @@ -32,12 +32,12 @@ public function testScalars() public function testArrays() { - $this->assertEquals( + $this->assertSame( "JSON.parse('[\\u0022hello\\u0022,\\u0022world\\u0022]')", (string) Js::from(['hello', 'world']) ); - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from(['foo' => 'hello', 'bar' => 'world']) ); @@ -45,7 +45,7 @@ public function testArrays() public function testObjects() { - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from((object) ['foo' => 'hello', 'bar' => 'world']) ); @@ -72,7 +72,7 @@ public function toArray() } }; - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from($data) ); @@ -104,7 +104,7 @@ public function toArray() } }; - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from($data) ); @@ -124,7 +124,7 @@ public function toArray() } }; - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from($data) ); @@ -140,7 +140,7 @@ public function toHtml() } }; - $this->assertEquals("'\u003Cp\u003EHello, World!\u003C\/p\u003E'", (string) Js::from($data)); + $this->assertSame("'\u003Cp\u003EHello, World!\u003C\/p\u003E'", (string) Js::from($data)); $data = new class implements Htmlable, Arrayable { @@ -155,7 +155,7 @@ public function toArray() } }; - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from($data) ); @@ -173,7 +173,7 @@ public function toJson($options = 0) } }; - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from($data) ); @@ -191,7 +191,7 @@ public function jsonSerialize(): mixed } }; - $this->assertEquals( + $this->assertSame( "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", (string) Js::from($data) ); diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index 43f0339c21c5..d729a516fc47 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -22,7 +22,7 @@ public function testMakeWithClosureIsLazy() LazyCollection::make($closure); - $this->assertEquals([], $recorder->all()); + $this->assertSame([], $recorder->all()); } public function testMakeWithLazyCollectionIsLazy() diff --git a/tests/Support/SupportMailTest.php b/tests/Support/SupportMailTest.php index 146a3ca331a5..709c17bbec61 100644 --- a/tests/Support/SupportMailTest.php +++ b/tests/Support/SupportMailTest.php @@ -15,7 +15,7 @@ public function testItRegisterAndCallMacros() : 'it failed.', ); - $this->assertEquals('it works!', Mail::test('foo')); + $this->assertSame('it works!', Mail::test('foo')); } public function testItRegisterAndCallMacrosWhenFaked() @@ -27,7 +27,7 @@ public function testItRegisterAndCallMacrosWhenFaked() Mail::fake(); - $this->assertEquals('it works!', Mail::test('foo')); + $this->assertSame('it works!', Mail::test('foo')); } public function testEmailSent() diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 6bfec03405af..bab5090239b3 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -573,8 +573,8 @@ public function testFinish() public function testWrap() { - $this->assertEquals('"value"', Str::wrap('value', '"')); - $this->assertEquals('foo-bar-baz', Str::wrap('-bar-', 'foo', 'baz')); + $this->assertSame('"value"', Str::wrap('value', '"')); + $this->assertSame('foo-bar-baz', Str::wrap('-bar-', 'foo', 'baz')); } public function testWrapEdgeCases() @@ -590,11 +590,11 @@ public function testWrapEdgeCases() public function testUnwrap() { - $this->assertEquals('value', Str::unwrap('"value"', '"')); - $this->assertEquals('value', Str::unwrap('"value', '"')); - $this->assertEquals('value', Str::unwrap('value"', '"')); - $this->assertEquals('bar', Str::unwrap('foo-bar-baz', 'foo-', '-baz')); - $this->assertEquals('some: "json"', Str::unwrap('{some: "json"}', '{', '}')); + $this->assertSame('value', Str::unwrap('"value"', '"')); + $this->assertSame('value', Str::unwrap('"value', '"')); + $this->assertSame('value', Str::unwrap('value"', '"')); + $this->assertSame('bar', Str::unwrap('foo-bar-baz', 'foo-', '-baz')); + $this->assertSame('some: "json"', Str::unwrap('{some: "json"}', '{', '}')); } public function testIs() @@ -1244,10 +1244,10 @@ public function testCamel(): void public function testCharAt() { - $this->assertEquals('р', Str::charAt('Привет, мир!', 1)); - $this->assertEquals('ち', Str::charAt('「こんにちは世界」', 4)); - $this->assertEquals('w', Str::charAt('Привет, world!', 8)); - $this->assertEquals('界', Str::charAt('「こんにちは世界」', -2)); + $this->assertSame('р', Str::charAt('Привет, мир!', 1)); + $this->assertSame('ち', Str::charAt('「こんにちは世界」', 4)); + $this->assertSame('w', Str::charAt('Привет, world!', 8)); + $this->assertSame('界', Str::charAt('「こんにちは世界」', -2)); $this->assertEquals(null, Str::charAt('「こんにちは世界」', -200)); $this->assertEquals(null, Str::charAt('Привет, мир!', 100)); } @@ -1440,10 +1440,10 @@ public function testWordCount() public function testWordWrap() { - $this->assertEquals('Hello
World', Str::wordWrap('Hello World', 3, '
')); - $this->assertEquals('Hel
lo
Wor
ld', Str::wordWrap('Hello World', 3, '
', true)); + $this->assertSame('Hello
World', Str::wordWrap('Hello World', 3, '
')); + $this->assertSame('Hel
lo
Wor
ld', Str::wordWrap('Hello World', 3, '
', true)); - $this->assertEquals('❤Multi
Byte☆❤☆❤☆❤', Str::wordWrap('❤Multi Byte☆❤☆❤☆❤', 3, '
')); + $this->assertSame('❤Multi
Byte☆❤☆❤☆❤', Str::wordWrap('❤Multi Byte☆❤☆❤☆❤', 3, '
')); } public static function validUuidList() diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index f31df4b921da..09c3c2fcc1f0 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -1202,10 +1202,10 @@ public function testCamel() public function testCharAt() { - $this->assertEquals('р', $this->stringable('Привет, мир!')->charAt(1)); - $this->assertEquals('ち', $this->stringable('「こんにちは世界」')->charAt(4)); - $this->assertEquals('w', $this->stringable('Привет, world!')->charAt(8)); - $this->assertEquals('界', $this->stringable('「こんにちは世界」')->charAt(-2)); + $this->assertSame('р', $this->stringable('Привет, мир!')->charAt(1)); + $this->assertSame('ち', $this->stringable('「こんにちは世界」')->charAt(4)); + $this->assertSame('w', $this->stringable('Привет, world!')->charAt(8)); + $this->assertSame('界', $this->stringable('「こんにちは世界」')->charAt(-2)); $this->assertEquals(null, $this->stringable('「こんにちは世界」')->charAt(-200)); $this->assertEquals(null, $this->stringable('Привет, мир!')->charAt('Привет, мир!', 100)); } @@ -1345,8 +1345,8 @@ public function testPipe() public function testMarkdown() { - $this->assertEquals("

hello world

\n", $this->stringable('*hello world*')->markdown()); - $this->assertEquals("

hello world

\n", $this->stringable('# hello world')->markdown()); + $this->assertSame("

hello world

\n", (string) $this->stringable('*hello world*')->markdown()); + $this->assertSame("

hello world

\n", (string) $this->stringable('# hello world')->markdown()); $extension = new class implements ExtensionInterface { @@ -1363,8 +1363,8 @@ public function register(EnvironmentBuilderInterface $environment): void public function testInlineMarkdown() { - $this->assertEquals("hello world\n", $this->stringable('*hello world*')->inlineMarkdown()); - $this->assertEquals("Laravel\n", $this->stringable('[**Laravel**](https://laravel.com)')->inlineMarkdown()); + $this->assertSame("hello world\n", (string) $this->stringable('*hello world*')->inlineMarkdown()); + $this->assertSame("Laravel\n", (string) $this->stringable('[**Laravel**](https://laravel.com)')->inlineMarkdown()); $extension = new class implements ExtensionInterface { @@ -1415,15 +1415,15 @@ public function testWordCount() public function testWrap() { - $this->assertEquals('This is me!', $this->stringable('is')->wrap('This ', ' me!')); - $this->assertEquals('"value"', $this->stringable('value')->wrap('"')); + $this->assertSame('This is me!', (string) $this->stringable('is')->wrap('This ', ' me!')); + $this->assertSame('"value"', (string) $this->stringable('value')->wrap('"')); } public function testUnwrap() { - $this->assertEquals('value', $this->stringable('"value"')->unwrap('"')); - $this->assertEquals('bar', $this->stringable('foo-bar-baz')->unwrap('foo-', '-baz')); - $this->assertEquals('some: "json"', $this->stringable('{some: "json"}')->unwrap('{', '}')); + $this->assertSame('value', (string) $this->stringable('"value"')->unwrap('"')); + $this->assertSame('bar', (string) $this->stringable('foo-bar-baz')->unwrap('foo-', '-baz')); + $this->assertSame('some: "json"', (string) $this->stringable('{some: "json"}')->unwrap('{', '}')); } public function testToHtmlString() diff --git a/tests/Support/SupportTestingMailFakeTest.php b/tests/Support/SupportTestingMailFakeTest.php index 547931f91507..42c2ce9942ce 100644 --- a/tests/Support/SupportTestingMailFakeTest.php +++ b/tests/Support/SupportTestingMailFakeTest.php @@ -381,7 +381,7 @@ public function testMissingMethodsAreForwarded() { $this->mailManager->shouldReceive('foo')->andReturn('bar'); - $this->assertEquals('bar', $this->fake->foo()); + $this->assertSame('bar', $this->fake->foo()); } public function testAssertMailer() diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 9e15c23c28ae..8eb9f70414d8 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -12,37 +12,37 @@ public function test_can_build_special_urls() { Uri::setUrlGeneratorResolver(fn () => new CustomUrlGeneratorResolver); - $this->assertEquals('https://laravel.com/to', Uri::to('')->value()); - $this->assertEquals('https://laravel.com/route', Uri::route('')->value()); - $this->assertEquals('https://laravel.com/signed-route', Uri::signedRoute('')->value()); - $this->assertEquals('https://laravel.com/signed-route', Uri::temporarySignedRoute('', '')->value()); - $this->assertEquals('https://laravel.com/action', Uri::action('')->value()); + $this->assertSame('https://laravel.com/to', Uri::to('')->value()); + $this->assertSame('https://laravel.com/route', Uri::route('')->value()); + $this->assertSame('https://laravel.com/signed-route', Uri::signedRoute('')->value()); + $this->assertSame('https://laravel.com/signed-route', Uri::temporarySignedRoute('', '')->value()); + $this->assertSame('https://laravel.com/action', Uri::action('')->value()); } public function test_basic_uri_interactions() { $uri = Uri::of($originalUri = 'https://laravel.com/docs/installation'); - $this->assertEquals('https', $uri->scheme()); + $this->assertSame('https', $uri->scheme()); $this->assertNull($uri->user()); $this->assertNull($uri->password()); - $this->assertEquals('laravel.com', $uri->host()); + $this->assertSame('laravel.com', $uri->host()); $this->assertNull($uri->port()); - $this->assertEquals('docs/installation', $uri->path()); - $this->assertEquals([], $uri->query()->toArray()); - $this->assertEquals('', (string) $uri->query()); - $this->assertEquals('', $uri->query()->decode()); + $this->assertSame('docs/installation', $uri->path()); + $this->assertSame([], $uri->query()->toArray()); + $this->assertSame('', (string) $uri->query()); + $this->assertSame('', $uri->query()->decode()); $this->assertNull($uri->fragment()); $this->assertEquals($originalUri, (string) $uri); $uri = Uri::of('https://taylor:password@laravel.com/docs/installation?version=1#hello'); - $this->assertEquals('taylor', $uri->user()); - $this->assertEquals('password', $uri->password()); - $this->assertEquals('hello', $uri->fragment()); + $this->assertSame('taylor', $uri->user()); + $this->assertSame('password', $uri->password()); + $this->assertSame('hello', $uri->fragment()); $this->assertEquals(['version' => 1], $uri->query()->all()); $this->assertEquals(1, $uri->query()->integer('version')); - $this->assertEquals('taylor:password@laravel.com', $uri->authority()); + $this->assertSame('taylor:password@laravel.com', $uri->authority()); } public function test_is_empty_and_is_not_empty() @@ -58,15 +58,15 @@ public function test_without_fragment() { $uri = Uri::of('https://laravel.com/docs/installation#introduction'); - $this->assertEquals('introduction', $uri->fragment()); + $this->assertSame('introduction', $uri->fragment()); $withoutFragment = $uri->withoutFragment(); $this->assertNull($withoutFragment->fragment()); - $this->assertEquals('https://laravel.com/docs/installation', $withoutFragment->value()); + $this->assertSame('https://laravel.com/docs/installation', $withoutFragment->value()); // Original URI should be unchanged (immutability). - $this->assertEquals('introduction', $uri->fragment()); + $this->assertSame('introduction', $uri->fragment()); } public function test_without_fragment_on_uri_without_fragment() @@ -76,7 +76,7 @@ public function test_without_fragment_on_uri_without_fragment() $withoutFragment = $uri->withoutFragment(); $this->assertNull($withoutFragment->fragment()); - $this->assertEquals('https://laravel.com/docs', $withoutFragment->value()); + $this->assertSame('https://laravel.com/docs', $withoutFragment->value()); } public function test_complicated_query_string_parsing() @@ -107,7 +107,7 @@ public function test_complicated_query_string_parsing() 'flag_value' => '', ], $uri->query()->all()); - $this->assertEquals('key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&key.6=value&flag_value', $uri->query()->decode()); + $this->assertSame('key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&key.6=value&flag_value', $uri->query()->decode()); } public function test_uri_building() @@ -147,8 +147,8 @@ public function test_complicated_query_string_manipulation() 'flag' => '', ])->withoutQuery(['name']); - $this->assertEquals('age=38&role[title]=Developer&role[focus]=PHP&tags[0]=person&tags[1]=employee&flag=', $uri->query()->decode()); - $this->assertEquals('name=Taylor', $uri->replaceQuery(['name' => 'Taylor'])->query()->decode()); + $this->assertSame('age=38&role[title]=Developer&role[focus]=PHP&tags[0]=person&tags[1]=employee&flag=', $uri->query()->decode()); + $this->assertSame('name=Taylor', $uri->replaceQuery(['name' => 'Taylor'])->query()->decode()); // Push onto multi-value and missing items... $uri = Uri::of('https://laravel.com?tags[]=foo'); @@ -167,22 +167,22 @@ public function test_query_strings_with_dots_can_be_replaced_or_merged_consisten { $uri = Uri::of('https://dot.test/?foo.bar=baz'); - $this->assertEquals('foo.bar=baz&foo[bar]=zab', $uri->withQuery(['foo.bar' => 'zab'])->query()->decode()); - $this->assertEquals('foo[bar]=zab', $uri->replaceQuery(['foo.bar' => 'zab'])->query()->decode()); + $this->assertSame('foo.bar=baz&foo[bar]=zab', $uri->withQuery(['foo.bar' => 'zab'])->query()->decode()); + $this->assertSame('foo[bar]=zab', $uri->replaceQuery(['foo.bar' => 'zab'])->query()->decode()); } public function test_decoding_the_entire_uri() { $uri = Uri::of('https://laravel.com/docs/11.x/installation')->withQuery(['tags' => ['first', 'second']]); - $this->assertEquals('https://laravel.com/docs/11.x/installation?tags[0]=first&tags[1]=second', $uri->decode()); + $this->assertSame('https://laravel.com/docs/11.x/installation?tags[0]=first&tags[1]=second', $uri->decode()); } public function test_decoding_the_entire_uri_preserves_the_fragment() { $uri = Uri::of('https://laravel.com/docs/11.x/routing?q=laravel%20docs#route-model-binding'); - $this->assertEquals('https://laravel.com/docs/11.x/routing?q=laravel docs#route-model-binding', $uri->decode()); + $this->assertSame('https://laravel.com/docs/11.x/routing?q=laravel docs#route-model-binding', $uri->decode()); } public function test_with_query_if_missing() @@ -195,7 +195,7 @@ public function test_with_query_if_missing() 'existing' => 'new_value', ]); - $this->assertEquals('existing=value&new=parameter', $uri->query()->decode()); + $this->assertSame('existing=value&new=parameter', $uri->query()->decode()); // Test adding complex nested arrays to empty query string $uri = Uri::of('https://laravel.com'); @@ -212,7 +212,7 @@ public function test_with_query_if_missing() ], ]); - $this->assertEquals('name=Taylor&role[title]=Developer&role[focus]=PHP&tags[0]=person&tags[1]=employee', $uri->query()->decode()); + $this->assertSame('name=Taylor&role[title]=Developer&role[focus]=PHP&tags[0]=person&tags[1]=employee', $uri->query()->decode()); // Test partial array merging and preserving indexed arrays $uri = Uri::of('https://laravel.com?name=Taylor&tags[0]=person'); @@ -223,7 +223,7 @@ public function test_with_query_if_missing() 'tags' => ['should', 'not', 'change'], ]); - $this->assertEquals('name=Taylor&tags[0]=person&age=38', $uri->query()->decode()); + $this->assertSame('name=Taylor&tags[0]=person&age=38', $uri->query()->decode()); $this->assertEquals(['name' => 'Taylor', 'tags' => ['person'], 'age' => 38], $uri->query()->all()); $uri = Uri::of('https://laravel.com?user[name]=Taylor'); @@ -251,20 +251,20 @@ public function test_with_query_prevents_empty_query_string() { $uri = Uri::of('https://laravel.com'); - $this->assertEquals('https://laravel.com', (string) $uri); - $this->assertEquals('https://laravel.com', (string) $uri->withQuery([])); + $this->assertSame('https://laravel.com', (string) $uri); + $this->assertSame('https://laravel.com', (string) $uri->withQuery([])); } public function test_path_segments() { $uri = Uri::of('https://laravel.com'); - $this->assertEquals([], $uri->pathSegments()->toArray()); + $this->assertSame([], $uri->pathSegments()->toArray()); $uri = Uri::of('https://laravel.com/one/two/three'); $this->assertEquals(['one', 'two', 'three'], $uri->pathSegments()->toArray()); - $this->assertEquals('one', $uri->pathSegments()->first()); + $this->assertSame('one', $uri->pathSegments()->first()); $uri = Uri::of('https://laravel.com/one/two/three?foo=bar'); diff --git a/tests/Support/ValidatedInputTest.php b/tests/Support/ValidatedInputTest.php index af2e60d7352a..d6688d41f9be 100644 --- a/tests/Support/ValidatedInputTest.php +++ b/tests/Support/ValidatedInputTest.php @@ -556,6 +556,6 @@ public function test_except_method() $this->assertEquals(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null]], $input->except('foo.baz')); $this->assertEquals(['surname' => 'AYDIN'], $input->except('name', 'foo')); - $this->assertEquals([], $input->except('name', 'surname', 'foo')); + $this->assertSame([], $input->except('name', 'surname', 'foo')); } } diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 0da214569104..ba4bbf05713e 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -246,7 +246,7 @@ public function testViewData(): void 'gatherData' => ['foo' => 'bar', 'baz' => 'qux'], ]); - $this->assertEquals('bar', $response->viewData('foo')); + $this->assertSame('bar', $response->viewData('foo')); $this->assertEquals(['foo' => 'bar', 'baz' => 'qux'], $response->viewData()); } diff --git a/tests/Translation/TranslationFileLoaderTest.php b/tests/Translation/TranslationFileLoaderTest.php index 22e4e674b2d5..b0e3180da791 100755 --- a/tests/Translation/TranslationFileLoaderTest.php +++ b/tests/Translation/TranslationFileLoaderTest.php @@ -161,7 +161,7 @@ public function testEmptyArraysReturnedWhenFilesDontExist() $files->shouldReceive('exists')->once()->with(__DIR__.'/en/foo.php')->andReturn(false); $files->shouldReceive('getRequire')->never(); - $this->assertEquals([], $loader->load('en', 'foo', null)); + $this->assertSame([], $loader->load('en', 'foo', null)); } public function testEmptyArraysReturnedWhenFilesDontExistForNamespacedItems() @@ -169,7 +169,7 @@ public function testEmptyArraysReturnedWhenFilesDontExistForNamespacedItems() $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); $files->shouldReceive('getRequire')->never(); - $this->assertEquals([], $loader->load('en', 'foo', 'bar')); + $this->assertSame([], $loader->load('en', 'foo', 'bar')); } public function testLoadMethodForJSONProperlyCallsLoader() diff --git a/tests/Translation/TranslationMessageSelectorTest.php b/tests/Translation/TranslationMessageSelectorTest.php index ae9d8b6efe5d..ecccdd4da012 100755 --- a/tests/Translation/TranslationMessageSelectorTest.php +++ b/tests/Translation/TranslationMessageSelectorTest.php @@ -20,14 +20,14 @@ public function testChooseWithFloatDoesNotTriggerDeprecation() { $selector = new MessageSelector; - $this->assertEquals('many', $selector->choose('{0} zero|{1} one|[2,*] many', 2.75, 'pl')); + $this->assertSame('many', $selector->choose('{0} zero|{1} one|[2,*] many', 2.75, 'pl')); } public function testChoosePluralizesFloats() { $selector = new MessageSelector; - $this->assertEquals('plural', $selector->choose('singular|plural', 0.5, 'en')); + $this->assertSame('plural', $selector->choose('singular|plural', 0.5, 'en')); } public static function chooseTestData() diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php index 5ac17aad8ca8..d965e873aed2 100755 --- a/tests/Translation/TranslationTranslatorTest.php +++ b/tests/Translation/TranslationTranslatorTest.php @@ -189,7 +189,7 @@ public function testChoiceMethodProperlyUsesCustomCountReplacement() $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('{1} :count foos|[2,*] :count foos', 1234, 'en')->andReturn(':count foos'); - $this->assertEquals('1,234 foos', $t->choice(':count foos', 1234, ['count' => '1,234'])); + $this->assertSame('1,234 foos', $t->choice(':count foos', 1234, ['count' => '1,234'])); } public function testGetJson() diff --git a/tests/Validation/ValidationDateRuleTest.php b/tests/Validation/ValidationDateRuleTest.php index 04f99784839c..54f1766e961e 100644 --- a/tests/Validation/ValidationDateRuleTest.php +++ b/tests/Validation/ValidationDateRuleTest.php @@ -15,7 +15,7 @@ class ValidationDateRuleTest extends TestCase public function testDefaultDateRule() { $rule = Rule::date(); - $this->assertEquals('date', (string) $rule); + $this->assertSame('date', (string) $rule); $rule = new Date; $this->assertSame('date', (string) $rule); @@ -24,76 +24,76 @@ public function testDefaultDateRule() public function testDateFormatRule() { $rule = Rule::date()->format('d/m/Y'); - $this->assertEquals('date_format:d/m/Y', (string) $rule); + $this->assertSame('date_format:d/m/Y', (string) $rule); } public function testAfterTodayRule() { $rule = Rule::date()->afterToday(); - $this->assertEquals('date|after:today', (string) $rule); + $this->assertSame('date|after:today', (string) $rule); $rule = Rule::date()->todayOrAfter(); - $this->assertEquals('date|after_or_equal:today', (string) $rule); + $this->assertSame('date|after_or_equal:today', (string) $rule); } public function testBeforeTodayRule() { $rule = Rule::date()->beforeToday(); - $this->assertEquals('date|before:today', (string) $rule); + $this->assertSame('date|before:today', (string) $rule); $rule = Rule::date()->todayOrBefore(); - $this->assertEquals('date|before_or_equal:today', (string) $rule); + $this->assertSame('date|before_or_equal:today', (string) $rule); } public function testAfterSpecificDateRule() { $rule = Rule::date()->after(Carbon::parse('2024-01-01')); - $this->assertEquals('date|after:2024-01-01', (string) $rule); + $this->assertSame('date|after:2024-01-01', (string) $rule); $rule = Rule::date()->format('d/m/Y')->after(Carbon::parse('2024-01-01')); - $this->assertEquals('date_format:d/m/Y|after:01/01/2024', (string) $rule); + $this->assertSame('date_format:d/m/Y|after:01/01/2024', (string) $rule); } public function testBeforeSpecificDateRule() { $rule = Rule::date()->before(Carbon::parse('2024-01-01')); - $this->assertEquals('date|before:2024-01-01', (string) $rule); + $this->assertSame('date|before:2024-01-01', (string) $rule); $rule = Rule::date()->format('d/m/Y')->before(Carbon::parse('2024-01-01')); - $this->assertEquals('date_format:d/m/Y|before:01/01/2024', (string) $rule); + $this->assertSame('date_format:d/m/Y|before:01/01/2024', (string) $rule); } public function testAfterOrEqualSpecificDateRule() { $rule = Rule::date()->afterOrEqual(Carbon::parse('2024-01-01')); - $this->assertEquals('date|after_or_equal:2024-01-01', (string) $rule); + $this->assertSame('date|after_or_equal:2024-01-01', (string) $rule); $rule = Rule::date()->format('d/m/Y')->afterOrEqual(Carbon::parse('2024-01-01')); - $this->assertEquals('date_format:d/m/Y|after_or_equal:01/01/2024', (string) $rule); + $this->assertSame('date_format:d/m/Y|after_or_equal:01/01/2024', (string) $rule); } public function testBeforeOrEqualSpecificDateRule() { $rule = Rule::date()->beforeOrEqual(Carbon::parse('2024-01-01')); - $this->assertEquals('date|before_or_equal:2024-01-01', (string) $rule); + $this->assertSame('date|before_or_equal:2024-01-01', (string) $rule); $rule = Rule::date()->format('d/m/Y')->beforeOrEqual(Carbon::parse('2024-01-01')); - $this->assertEquals('date_format:d/m/Y|before_or_equal:01/01/2024', (string) $rule); + $this->assertSame('date_format:d/m/Y|before_or_equal:01/01/2024', (string) $rule); } public function testBetweenDatesRule() { $rule = Rule::date()->between(Carbon::parse('2024-01-01'), Carbon::parse('2024-02-01')); - $this->assertEquals('date|after:2024-01-01|before:2024-02-01', (string) $rule); + $this->assertSame('date|after:2024-01-01|before:2024-02-01', (string) $rule); $rule = Rule::date()->format('d/m/Y')->between(Carbon::parse('2024-01-01'), Carbon::parse('2024-02-01')); - $this->assertEquals('date_format:d/m/Y|after:01/01/2024|before:01/02/2024', (string) $rule); + $this->assertSame('date_format:d/m/Y|after:01/01/2024|before:01/02/2024', (string) $rule); } public function testBetweenOrEqualDatesRule() { $rule = Rule::date()->betweenOrEqual('2024-01-01', '2024-02-01'); - $this->assertEquals('date|after_or_equal:2024-01-01|before_or_equal:2024-02-01', (string) $rule); + $this->assertSame('date|after_or_equal:2024-01-01|before_or_equal:2024-02-01', (string) $rule); } public function testChainedRules() @@ -102,7 +102,7 @@ public function testChainedRules() ->format('Y-m-d') ->after('2024-01-01 00:00:00') ->before('2025-01-01 00:00:00'); - $this->assertEquals('date_format:Y-m-d|after:2024-01-01 00:00:00|before:2025-01-01 00:00:00', (string) $rule); + $this->assertSame('date_format:Y-m-d|after:2024-01-01 00:00:00|before:2025-01-01 00:00:00', (string) $rule); $rule = Rule::date() ->format('Y-m-d') diff --git a/tests/Validation/ValidationExceptionTest.php b/tests/Validation/ValidationExceptionTest.php index 5061f9e07d11..5a059dba2dcf 100755 --- a/tests/Validation/ValidationExceptionTest.php +++ b/tests/Validation/ValidationExceptionTest.php @@ -137,7 +137,7 @@ public function testExceptionErrorBagOneError() $exception = $this->getException([], ['foo' => 'required']); $exception->errorBag('milwad'); - $this->assertEquals('milwad', $exception->errorBag); + $this->assertSame('milwad', $exception->errorBag); } public function testExceptionRedirectToOneError() @@ -145,7 +145,7 @@ public function testExceptionRedirectToOneError() $exception = $this->getException([], ['foo' => 'required']); $exception->redirectTo('https://google.com'); - $this->assertEquals('https://google.com', $exception->redirectTo); + $this->assertSame('https://google.com', $exception->redirectTo); } public function testExceptionGetResponseOneError() diff --git a/tests/Validation/ValidationExcludeIfTest.php b/tests/Validation/ValidationExcludeIfTest.php index a616f642cc13..6edba31b0509 100644 --- a/tests/Validation/ValidationExcludeIfTest.php +++ b/tests/Validation/ValidationExcludeIfTest.php @@ -47,7 +47,7 @@ public function testItValidatesCallableAndBooleanAreAcceptableArguments() new ExcludeIf($condition); $this->fail('The ExcludeIf constructor must not accept '.gettype($condition)); } catch (InvalidArgumentException $exception) { - $this->assertEquals('The provided condition must be a callable or boolean.', $exception->getMessage()); + $this->assertSame('The provided condition must be a callable or boolean.', $exception->getMessage()); } } } diff --git a/tests/Validation/ValidationInArrayKeysTest.php b/tests/Validation/ValidationInArrayKeysTest.php index dec89209e398..ee542b59c980 100644 --- a/tests/Validation/ValidationInArrayKeysTest.php +++ b/tests/Validation/ValidationInArrayKeysTest.php @@ -67,7 +67,7 @@ public function testInArrayKeysValidationErrorMessage() $v = new Validator($trans, ['foo' => ['wrong_key' => 'bar']], ['foo' => 'in_array_keys:first_key,second_key']); $this->assertFalse($v->passes()); - $this->assertEquals( + $this->assertSame( 'The foo field must contain at least one of the following keys: first_key, second_key.', $v->messages()->first('foo') ); diff --git a/tests/Validation/ValidationNumericRuleTest.php b/tests/Validation/ValidationNumericRuleTest.php index 85b74a6728c1..7d8e5e5108f6 100644 --- a/tests/Validation/ValidationNumericRuleTest.php +++ b/tests/Validation/ValidationNumericRuleTest.php @@ -14,7 +14,7 @@ class ValidationNumericRuleTest extends TestCase public function testDefaultNumericRule() { $rule = Rule::numeric(); - $this->assertEquals('numeric', (string) $rule); + $this->assertSame('numeric', (string) $rule); $rule = new Numeric(); $this->assertSame('numeric', (string) $rule); @@ -23,118 +23,118 @@ public function testDefaultNumericRule() public function testBetweenRule() { $rule = Rule::numeric()->between(1, 10); - $this->assertEquals('numeric|between:1,10', (string) $rule); + $this->assertSame('numeric|between:1,10', (string) $rule); $rule = Rule::numeric()->between(1.5, 10.5); - $this->assertEquals('numeric|between:1.5,10.5', (string) $rule); + $this->assertSame('numeric|between:1.5,10.5', (string) $rule); } public function testDecimalRule() { $rule = Rule::numeric()->decimal(2, 4); - $this->assertEquals('numeric|decimal:2,4', (string) $rule); + $this->assertSame('numeric|decimal:2,4', (string) $rule); $rule = Rule::numeric()->decimal(2); - $this->assertEquals('numeric|decimal:2', (string) $rule); + $this->assertSame('numeric|decimal:2', (string) $rule); } public function testDifferentRule() { $rule = Rule::numeric()->different('some_field'); - $this->assertEquals('numeric|different:some_field', (string) $rule); + $this->assertSame('numeric|different:some_field', (string) $rule); } public function testDigitsRule() { $rule = Rule::numeric()->digits(10); - $this->assertEquals('numeric|integer|digits:10', (string) $rule); + $this->assertSame('numeric|integer|digits:10', (string) $rule); } public function testDigitsBetweenRule() { $rule = Rule::numeric()->digitsBetween(2, 10); - $this->assertEquals('numeric|integer|digits_between:2,10', (string) $rule); + $this->assertSame('numeric|integer|digits_between:2,10', (string) $rule); } public function testGreaterThanRule() { $rule = Rule::numeric()->greaterThan('some_field'); - $this->assertEquals('numeric|gt:some_field', (string) $rule); + $this->assertSame('numeric|gt:some_field', (string) $rule); } public function testGreaterThanOrEqualRule() { $rule = Rule::numeric()->greaterThanOrEqualTo('some_field'); - $this->assertEquals('numeric|gte:some_field', (string) $rule); + $this->assertSame('numeric|gte:some_field', (string) $rule); } public function testIntegerRule() { $rule = Rule::numeric()->integer(); - $this->assertEquals('numeric|integer', (string) $rule); + $this->assertSame('numeric|integer', (string) $rule); $rule = Rule::numeric()->integer(strict: true); - $this->assertEquals('numeric|integer:strict', (string) $rule); + $this->assertSame('numeric|integer:strict', (string) $rule); } public function testLessThanRule() { $rule = Rule::numeric()->lessThan('some_field'); - $this->assertEquals('numeric|lt:some_field', (string) $rule); + $this->assertSame('numeric|lt:some_field', (string) $rule); } public function testLessThanOrEqualRule() { $rule = Rule::numeric()->lessThanOrEqualTo('some_field'); - $this->assertEquals('numeric|lte:some_field', (string) $rule); + $this->assertSame('numeric|lte:some_field', (string) $rule); } public function testMaxRule() { $rule = Rule::numeric()->max(10); - $this->assertEquals('numeric|max:10', (string) $rule); + $this->assertSame('numeric|max:10', (string) $rule); $rule = Rule::numeric()->max(10.5); - $this->assertEquals('numeric|max:10.5', (string) $rule); + $this->assertSame('numeric|max:10.5', (string) $rule); } public function testMaxDigitsRule() { $rule = Rule::numeric()->maxDigits(10); - $this->assertEquals('numeric|max_digits:10', (string) $rule); + $this->assertSame('numeric|max_digits:10', (string) $rule); } public function testMinRule() { $rule = Rule::numeric()->min(10); - $this->assertEquals('numeric|min:10', (string) $rule); + $this->assertSame('numeric|min:10', (string) $rule); $rule = Rule::numeric()->min(10.5); - $this->assertEquals('numeric|min:10.5', (string) $rule); + $this->assertSame('numeric|min:10.5', (string) $rule); } public function testMinDigitsRule() { $rule = Rule::numeric()->minDigits(10); - $this->assertEquals('numeric|min_digits:10', (string) $rule); + $this->assertSame('numeric|min_digits:10', (string) $rule); } public function testMultipleOfRule() { $rule = Rule::numeric()->multipleOf(10); - $this->assertEquals('numeric|multiple_of:10', (string) $rule); + $this->assertSame('numeric|multiple_of:10', (string) $rule); } public function testSameRule() { $rule = Rule::numeric()->same('some_field'); - $this->assertEquals('numeric|same:some_field', (string) $rule); + $this->assertSame('numeric|same:some_field', (string) $rule); } public function testSizeRule() { $rule = Rule::numeric()->exactly(10); - $this->assertEquals('numeric|integer|size:10', (string) $rule); + $this->assertSame('numeric|integer|size:10', (string) $rule); } public function testChainedRules() @@ -144,7 +144,7 @@ public function testChainedRules() ->multipleOf(10) ->lessThanOrEqualTo('some_field') ->max(100); - $this->assertEquals('numeric|integer|multiple_of:10|lte:some_field|max:100', (string) $rule); + $this->assertSame('numeric|integer|multiple_of:10|lte:some_field|max:100', (string) $rule); $rule = Rule::numeric() ->decimal(2) @@ -356,6 +356,6 @@ public function testNumericValidation() public function testUniquenessValidation() { $rule = Rule::numeric()->integer()->digits(2)->exactly(2); - $this->assertEquals('numeric|integer|digits:2|size:2', (string) $rule); + $this->assertSame('numeric|integer|digits:2|size:2', (string) $rule); } } diff --git a/tests/Validation/ValidationProhibitedIfTest.php b/tests/Validation/ValidationProhibitedIfTest.php index c68eb88e95eb..bd6879c3ab5d 100644 --- a/tests/Validation/ValidationProhibitedIfTest.php +++ b/tests/Validation/ValidationProhibitedIfTest.php @@ -47,7 +47,7 @@ public function testItValidatesCallableAndBooleanAreAcceptableArguments() new ProhibitedIf($condition); $this->fail('The ProhibitedIf constructor must not accept '.gettype($condition)); } catch (InvalidArgumentException $exception) { - $this->assertEquals('The provided condition must be a callable or boolean.', $exception->getMessage()); + $this->assertSame('The provided condition must be a callable or boolean.', $exception->getMessage()); } } } diff --git a/tests/Validation/ValidationRuleCanTest.php b/tests/Validation/ValidationRuleCanTest.php index 039b9438b2f9..ecf7074c8012 100644 --- a/tests/Validation/ValidationRuleCanTest.php +++ b/tests/Validation/ValidationRuleCanTest.php @@ -59,7 +59,7 @@ protected function tearDown(): void public function testValidationFails() { $this->gate()->define('update-company', function ($user, $value) { - $this->assertEquals('1', $value); + $this->assertSame('1', $value); return false; }); @@ -78,7 +78,7 @@ public function testValidationPasses() $this->gate()->define('update-company', function ($user, $class, $model, $value) { $this->assertEquals(\App\Models\Company::class, $class); $this->assertInstanceOf(stdClass::class, $model); - $this->assertEquals('1', $value); + $this->assertSame('1', $value); return true; }); diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php index 4f5cd7d51f69..564dc4d3f216 100644 --- a/tests/Validation/ValidationRuleParserTest.php +++ b/tests/Validation/ValidationRuleParserTest.php @@ -187,7 +187,7 @@ public function testExplodeGeneratesNestedRules() 'users.*.name' => Rule::forEach(function ($value, $attribute, $data, $context) { $this->assertSame('Taylor Otwell', $value); $this->assertSame('users.0.name', $attribute); - $this->assertEquals('Taylor Otwell', $data['users.0.name']); + $this->assertSame('Taylor Otwell', $data['users.0.name']); $this->assertEquals(['name' => 'Taylor Otwell', 'email' => 'taylor@laravel.com'], $context); return [Rule::requiredIf(true)]; @@ -217,7 +217,7 @@ public function testExplodeGeneratesNestedRulesForNonNestedData() ]); $this->assertEquals(['name' => ['required']], $results->rules); - $this->assertEquals([], $results->implicitAttributes); + $this->assertSame([], $results->implicitAttributes); } public function testExplodeHandlesForwardSlashesInWildcardRule() diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index eb8450c26b5a..a04905319707 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -4652,10 +4652,10 @@ public function testValidateGtMessagesAreCorrect() ]); $this->assertFalse($v->passes()); - $this->assertEquals('The numeric field must be greater than 10.', $v->messages()->first('numeric')); - $this->assertEquals('The string field must be greater than 5 characters.', $v->messages()->first('string')); - $this->assertEquals('The file field must be greater than 9 kilobytes.', $v->messages()->first('file')); - $this->assertEquals('The array field must have more than 4 items.', $v->messages()->first('array')); + $this->assertSame('The numeric field must be greater than 10.', $v->messages()->first('numeric')); + $this->assertSame('The string field must be greater than 5 characters.', $v->messages()->first('string')); + $this->assertSame('The file field must be greater than 9 kilobytes.', $v->messages()->first('file')); + $this->assertSame('The array field must have more than 4 items.', $v->messages()->first('array')); } public function testValidateGteMessagesAreCorrect() @@ -4692,10 +4692,10 @@ public function testValidateGteMessagesAreCorrect() ]); $this->assertFalse($v->passes()); - $this->assertEquals('The numeric field must be greater than or equal to 10.', $v->messages()->first('numeric')); - $this->assertEquals('The string field must be greater than or equal to 5 characters.', $v->messages()->first('string')); - $this->assertEquals('The file field must be greater than or equal to 9 kilobytes.', $v->messages()->first('file')); - $this->assertEquals('The array field must have 4 items or more.', $v->messages()->first('array')); + $this->assertSame('The numeric field must be greater than or equal to 10.', $v->messages()->first('numeric')); + $this->assertSame('The string field must be greater than or equal to 5 characters.', $v->messages()->first('string')); + $this->assertSame('The file field must be greater than or equal to 9 kilobytes.', $v->messages()->first('file')); + $this->assertSame('The array field must have 4 items or more.', $v->messages()->first('array')); } public function testValidateLtMessagesAreCorrect() @@ -4732,10 +4732,10 @@ public function testValidateLtMessagesAreCorrect() ]); $this->assertFalse($v->passes()); - $this->assertEquals('The numeric field must be less than 5.', $v->messages()->first('numeric')); - $this->assertEquals('The string field must be less than 3 characters.', $v->messages()->first('string')); - $this->assertEquals('The file field must be less than 8 kilobytes.', $v->messages()->first('file')); - $this->assertEquals('The array field must have less than 2 items.', $v->messages()->first('array')); + $this->assertSame('The numeric field must be less than 5.', $v->messages()->first('numeric')); + $this->assertSame('The string field must be less than 3 characters.', $v->messages()->first('string')); + $this->assertSame('The file field must be less than 8 kilobytes.', $v->messages()->first('file')); + $this->assertSame('The array field must have less than 2 items.', $v->messages()->first('array')); } public function testValidateLteMessagesAreCorrect() @@ -4772,10 +4772,10 @@ public function testValidateLteMessagesAreCorrect() ]); $this->assertFalse($v->passes()); - $this->assertEquals('The numeric field must be less than or equal to 5.', $v->messages()->first('numeric')); - $this->assertEquals('The string field must be less than or equal to 3 characters.', $v->messages()->first('string')); - $this->assertEquals('The file field must be less than or equal to 8 kilobytes.', $v->messages()->first('file')); - $this->assertEquals('The array field must not have more than 2 items.', $v->messages()->first('array')); + $this->assertSame('The numeric field must be less than or equal to 5.', $v->messages()->first('numeric')); + $this->assertSame('The string field must be less than or equal to 3 characters.', $v->messages()->first('string')); + $this->assertSame('The file field must be less than or equal to 8 kilobytes.', $v->messages()->first('file')); + $this->assertSame('The array field must not have more than 2 items.', $v->messages()->first('array')); } public function testValidateIp() @@ -6954,7 +6954,7 @@ public function testItemAwareSometimesAddingRules() $v->sometimes(['users'], 'array', function ($i, $item) { return (bool) $item; }); - $this->assertEquals([], $v->getRules()); + $this->assertSame([], $v->getRules()); // ['company.users'] -> if users is not empty it must be validated as array $trans = $this->getIlluminateArrayTranslator(); diff --git a/tests/View/Blade/BladeBoolTest.php b/tests/View/Blade/BladeBoolTest.php index 8fb87f6cb963..5ed0a47a9db6 100644 --- a/tests/View/Blade/BladeBoolTest.php +++ b/tests/View/Blade/BladeBoolTest.php @@ -34,28 +34,28 @@ public function testCompileBool(): void ob_start(); eval(substr($compiled, 6, -3)); - $this->assertEquals('true', ob_get_clean()); + $this->assertSame('true', ob_get_clean()); $someViewVarFalsey = '0'; $compiled = $this->compiler->compileString('@bool($someViewVarFalsey)'); ob_start(); eval(substr($compiled, 6, -3)); - $this->assertEquals('false', ob_get_clean()); + $this->assertSame('false', ob_get_clean()); $anotherSomeViewVarTruthy = new SomeClass(); $compiled = $this->compiler->compileString('@bool($anotherSomeViewVarTruthy)'); ob_start(); eval(substr($compiled, 6, -3)); - $this->assertEquals('true', ob_get_clean()); + $this->assertSame('true', ob_get_clean()); $anotherSomeViewVarFalsey = null; $compiled = $this->compiler->compileString('@bool($anotherSomeViewVarFalsey)'); ob_start(); eval(substr($compiled, 6, -3)); - $this->assertEquals('false', ob_get_clean()); + $this->assertSame('false', ob_get_clean()); } } diff --git a/tests/View/ViewComponentAttributeBagTest.php b/tests/View/ViewComponentAttributeBagTest.php index a2293e6e5f19..0a972ba60e4c 100644 --- a/tests/View/ViewComponentAttributeBagTest.php +++ b/tests/View/ViewComponentAttributeBagTest.php @@ -279,7 +279,7 @@ public function testWhenFilled() $result = $bag->whenFilled('name', function ($value) { return 'callback-'.$value; }); - $this->assertEquals('callback-test', $result); + $this->assertSame('callback-test', $result); $result = $bag->whenFilled('empty', function ($value) { return 'callback-'.$value; @@ -291,7 +291,7 @@ public function testWhenFilled() }, function () { return 'default-callback'; }); - $this->assertEquals('default-callback', $result); + $this->assertSame('default-callback', $result); } public function testWhenHas() @@ -301,7 +301,7 @@ public function testWhenHas() $result = $bag->whenHas('name', function ($value) { return 'callback-'.$value; }); - $this->assertEquals('callback-test', $result); + $this->assertSame('callback-test', $result); $result = $bag->whenHas('missing', function ($value) { return 'callback-'.$value; @@ -313,7 +313,7 @@ public function testWhenHas() }, function () { return 'default-callback'; }); - $this->assertEquals('default-callback', $result); + $this->assertSame('default-callback', $result); } public function testWhenMissing() @@ -328,14 +328,14 @@ public function testWhenMissing() $result = $bag->whenMissing('missing', function () { return 'callback'; }); - $this->assertEquals('callback', $result); + $this->assertSame('callback', $result); $result = $bag->whenMissing('name', function () { return 'callback'; }, function () { return 'default-callback'; }); - $this->assertEquals('default-callback', $result); + $this->assertSame('default-callback', $result); } public function testString() @@ -347,10 +347,10 @@ public function testString() ]); $this->assertInstanceOf(\Illuminate\Support\Stringable::class, $bag->string('name')); - $this->assertEquals('test', (string) $bag->string('name')); - $this->assertEquals('', (string) $bag->string('empty')); - $this->assertEquals('123', (string) $bag->string('number')); - $this->assertEquals('default', (string) $bag->string('missing', 'default')); + $this->assertSame('test', (string) $bag->string('name')); + $this->assertSame('', (string) $bag->string('empty')); + $this->assertSame('123', (string) $bag->string('number')); + $this->assertSame('default', (string) $bag->string('missing', 'default')); } public function testBoolean() From 461f6157b1199dfe7523edd3b9321727816f35c4 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:34:02 +0600 Subject: [PATCH 190/596] [13.x] Cast numeric values to string before preg_match in decimal, max_digits, and min_digits rules (#59739) --- .../Concerns/ValidatesAttributes.php | 6 +++--- tests/Validation/ValidationValidatorTest.php | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index e80392d5712d..ebd0616ec90d 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -673,7 +673,7 @@ public function validateDecimal($attribute, $value, $parameters) $matches = []; - if (preg_match('/^[+-]?\d*\.?(\d*)$/', $value, $matches) !== 1) { + if (preg_match('/^[+-]?\d*\.?(\d*)$/', (string) $value, $matches) !== 1) { return false; } @@ -1703,7 +1703,7 @@ public function validateMaxDigits($attribute, $value, $parameters) $length = strlen((string) $value); - return ! preg_match('/[^0-9]/', $value) && $length <= $parameters[0]; + return ! preg_match('/[^0-9]/', (string) $value) && $length <= $parameters[0]; } /** @@ -1813,7 +1813,7 @@ public function validateMinDigits($attribute, $value, $parameters) $length = strlen((string) $value); - return ! preg_match('/[^0-9]/', $value) && $length >= $parameters[0]; + return ! preg_match('/[^0-9]/', (string) $value) && $length >= $parameters[0]; } /** diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a04905319707..a43e7a2a126e 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -3256,6 +3256,12 @@ public function testValidateMaxDigitsDoesNotThrowOnNonStringValue() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => ['array']], ['x' => 'max_digits:5']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'max_digits:5']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 123456], ['x' => 'max_digits:5']); + $this->assertFalse($v->passes()); } public function testValidateMinDigitsDoesNotThrowOnNonStringValue() @@ -3263,6 +3269,12 @@ public function testValidateMinDigitsDoesNotThrowOnNonStringValue() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => ['array']], ['x' => 'min_digits:1']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'min_digits:2']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 1], ['x' => 'min_digits:2']); + $this->assertFalse($v->passes()); } public function testValidateDigitsBetweenDoesNotThrowOnNonStringValue() @@ -3725,6 +3737,12 @@ public function testValidateDecimal() $this->assertTrue($v->passes()); $v = new Validator($trans, ['foo' => '123.34'], ['foo' => 'Decimal:0,2']); $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => 123], ['foo' => 'Decimal:0']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => 1.23], ['foo' => 'Decimal:0,3']); + $this->assertTrue($v->passes()); } public function testValidateInt() From 6d067e531ea17b698a187727125d7dbb5bff1105 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:23:31 +0600 Subject: [PATCH 191/596] Ignore PHPUnit security advisory GHSA-qrr6-mg7r-m243 (#59761) --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 218e76f03a43..c991f797152f 100644 --- a/composer.json +++ b/composer.json @@ -215,6 +215,7 @@ }, "audit": { "ignore": { + "GHSA-qrr6-mg7r-m243": "Ensure testing features are compatible with affected PHPUnit versions", "GHSA-vvj3-c3rp-c85p": "Ensure testing features are compatible with affected PHPUnit versions" } }, From f15ba6dc7f169279883d5aad47936091555462b3 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sat, 18 Apr 2026 14:25:22 +0100 Subject: [PATCH 192/596] [13.x] Allow assertDatabase has & missing to accept arrays (#59752) * apply logic * tests * use brain * be explicit fam * drop count * suPp0rtsssss --- .../Concerns/InteractsWithDatabase.php | 16 ++++++++++ .../FoundationInteractsWithDatabaseTest.php | 30 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 73d5f9771b5b..115589b17936 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -33,6 +33,14 @@ protected function assertDatabaseHas($table, array $data = [], $connection = nul return $this; } + if (array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { + foreach ($data as $row) { + $this->assertDatabaseHas($table, $row, $connection); + } + + return $this; + } + if ($table instanceof Model) { $data = [ $table->getKeyName() => $table->getKey(), @@ -65,6 +73,14 @@ protected function assertDatabaseMissing($table, array $data = [], $connection = return $this; } + if (array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { + foreach ($data as $row) { + $this->assertDatabaseMissing($table, $row, $connection); + } + + return $this; + } + if ($table instanceof Model) { $data = [ $table->getKeyName() => $table->getKey(), diff --git a/tests/Foundation/FoundationInteractsWithDatabaseTest.php b/tests/Foundation/FoundationInteractsWithDatabaseTest.php index a0a39e0eb924..9dc92067debf 100644 --- a/tests/Foundation/FoundationInteractsWithDatabaseTest.php +++ b/tests/Foundation/FoundationInteractsWithDatabaseTest.php @@ -61,6 +61,21 @@ public function testAssertDatabaseHasConstrainsToModel() $this->assertDatabaseHas(new ProductStub(['id' => 1]), $data); } + public function testAssertDatabaseSupportsArrays() + { + $builder = m::mock(Builder::class); + $builder->shouldReceive('where')->with(['title' => 'Spark', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('where')->with(['title' => 'Forge', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('exists')->twice()->andReturn(true); + + $this->connection->shouldReceive('table')->with($this->table)->andReturn($builder); + + $this->assertDatabaseHas($this->table, [ + ['title' => 'Spark', 'name' => 'Laravel'], + ['title' => 'Forge', 'name' => 'Laravel'], + ]); + } + public function testSeeInDatabaseDoesNotFindResults() { $this->expectException(ExpectationFailedException::class); @@ -103,6 +118,21 @@ public function testSeeInDatabaseFindsManyNotMatchingResults() $this->assertDatabaseHas($this->table, $this->data); } + public function testAssertDatabaseMissingSupportsArrays() + { + $builder = m::mock(Builder::class); + $builder->shouldReceive('where')->with(['title' => 'Spark', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('where')->with(['title' => 'Forge', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('exists')->twice()->andReturn(false); + + $this->connection->shouldReceive('table')->with($this->table)->andReturn($builder); + + $this->assertDatabaseMissing($this->table, [ + ['title' => 'Spark', 'name' => 'Laravel'], + ['title' => 'Forge', 'name' => 'Laravel'], + ]); + } + public function testDontSeeInDatabaseDoesNotFindResults() { $this->mockCountBuilder(false); From 0cb390810c365640a4a909e3b25ee648b7c74da0 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Sat, 18 Apr 2026 14:27:05 +0100 Subject: [PATCH 193/596] Support named credential providers for SQS queue connections (#59754) Allow SQS queue connections to specify a credential provider by name (ecs, instance) via the credentials config key, and configure Cloud to automatically use ECS credentials for managed queues. Co-authored-by: Claude Opus 4.6 (1M context) --- src/Illuminate/Foundation/Cloud.php | 16 +++++++- .../Queue/Connectors/SqsConnector.php | 35 ++++++++++++++++- tests/Integration/Foundation/CloudTest.php | 39 ++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 07a7317c9f85..30107606ddcc 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -30,7 +30,7 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst static::configureDisks($app); static::configureUnpooledPostgresConnection($app); static::ensureMigrationsUseUnpooledConnection($app); - static::configureManagedQueues(); + static::configureManagedQueues($app); }, HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); @@ -117,10 +117,22 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): /** * Configure managed queues if applicable. */ - public static function configureManagedQueues(): void + public static function configureManagedQueues(Application $app): void { if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { Worker::$restartable = false; + + $app['config']->set( + 'queue.connections.sqs.credentials', + 'ecs' + ); + + if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { + $app['config']->set( + 'queue.connections.sqs.region', + $_SERVER['LARAVEL_CLOUD_REGION'] + ); + } } } diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index bb702f7536a0..70c90873d794 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -2,9 +2,11 @@ namespace Illuminate\Queue\Connectors; +use Aws\Credentials\CredentialProvider; use Aws\Sqs\SqsClient; use Illuminate\Queue\SqsQueue; use Illuminate\Support\Arr; +use InvalidArgumentException; class SqsConnector implements ConnectorInterface { @@ -18,7 +20,9 @@ public function connect(array $config) { $config = $this->getDefaultConfiguration($config); - if (! empty($config['key']) && ! empty($config['secret'])) { + if ($credentials = $this->resolveCredentialProvider($config)) { + $config['credentials'] = $credentials; + } elseif (! empty($config['key']) && ! empty($config['secret'])) { $config['credentials'] = Arr::only($config, ['key', 'secret']); if (! empty($config['token'])) { @@ -37,6 +41,35 @@ public function connect(array $config) ); } + /** + * Resolve a credential provider from the given config. + * + * @param array $config + * @return callable|null + * + * @throws \InvalidArgumentException + */ + protected function resolveCredentialProvider(array $config) + { + $credentials = $config['credentials'] ?? null; + + $provider = is_string($credentials) ? $credentials : ($credentials['provider'] ?? null); + + if (is_null($provider)) { + return null; + } + + $options = is_array($credentials) ? Arr::except($credentials, ['provider']) : []; + + return match ($provider) { + 'ecs' => CredentialProvider::ecsCredentials($options), + 'instance' => CredentialProvider::instanceProfile($options), + default => throw new InvalidArgumentException( + "Invalid credential provider [{$provider}]." + ), + }; + } + /** * Get the default configuration for SQS. * diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index ad6e35b55752..d79d31dfea09 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -60,7 +60,7 @@ public function test_it_disables_queue_restart_polling_for_managed_queues() $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; try { - Cloud::configureManagedQueues(); + Cloud::configureManagedQueues($this->app); $this->assertFalse(Worker::$restartable); } finally { @@ -69,6 +69,43 @@ public function test_it_disables_queue_restart_polling_for_managed_queues() } } + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function test_it_configures_managed_queue_credentials() + { + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + } + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function test_it_does_not_configure_managed_queues_when_not_enabled() + { + Cloud::configureManagedQueues($this->app); + + $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function test_it_configures_managed_queue_region() + { + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + } + } + public function test_it_respects_log_levels() { if (isset($_SERVER['LOG_LEVEL'])) { From 082d7d01e5b512be929daf39a7b57ae92772a83d Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:27:49 +0000 Subject: [PATCH 194/596] Update facade docblocks --- src/Illuminate/Support/Facades/App.php | 16 +++++++------- src/Illuminate/Support/Facades/Bus.php | 2 +- src/Illuminate/Support/Facades/Cache.php | 6 ++--- src/Illuminate/Support/Facades/Config.php | 6 ++--- src/Illuminate/Support/Facades/Context.php | 22 +++++++++---------- src/Illuminate/Support/Facades/DB.php | 4 ++-- src/Illuminate/Support/Facades/Exceptions.php | 8 +++---- src/Illuminate/Support/Facades/Http.php | 18 +++++++-------- src/Illuminate/Support/Facades/Process.php | 6 ++--- src/Illuminate/Support/Facades/Queue.php | 6 ++--- src/Illuminate/Support/Facades/Request.php | 8 +++---- src/Illuminate/Support/Facades/Schedule.php | 14 ++++++------ src/Illuminate/Support/Facades/Schema.php | 22 +++++++++---------- src/Illuminate/Support/Facades/Storage.php | 10 ++++----- 14 files changed, 74 insertions(+), 74 deletions(-) diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 541792f5f5e5..5bad0f492df4 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -53,7 +53,7 @@ * @method static void loadDeferredProviders() * @method static void loadDeferredProvider(string $service) * @method static void registerDeferredProvider(string $provider, string|null $service = null) - * @method static object|mixed make(string|string $abstract, array $parameters = []) + * @method static object|mixed make(string $abstract, array $parameters = []) * @method static bool bound(string $abstract) * @method static bool isBooted() * @method static void boot() @@ -79,7 +79,7 @@ * @method static never abort(int $code, string $message = '', array $headers = []) * @method static \Illuminate\Foundation\Application terminating(callable|string $callback) * @method static void terminate() - * @method static array getLoadedProviders() + * @method static array getLoadedProviders() * @method static bool providerIsLoaded(string $provider) * @method static array getDeferredServices() * @method static void setDeferredServices(array $services) @@ -119,11 +119,11 @@ * @method static mixed rebinding(string $abstract, \Closure $callback) * @method static mixed refresh(string $abstract, mixed $target, string $method) * @method static \Closure wrap(\Closure $callback, array $parameters = []) - * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) - * @method static \Closure|\Closure factory(string|string $abstract) - * @method static object|mixed makeWith(string|string|callable $abstract, array $parameters = []) - * @method static object|mixed get(string|string $id) - * @method static object build(\Closure|string $concrete) + * @method static mixed call(callable|string $callback, array $parameters = [], string|null $defaultMethod = null) + * @method static \Closure|\Closure factory(string $abstract) + * @method static object|mixed makeWith(string|callable $abstract, array $parameters = []) + * @method static object|mixed get(string $id) + * @method static object build(\Closure|string $concrete) * @method static mixed resolveFromAttribute(\ReflectionAttribute $attribute) * @method static void beforeResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void resolving(\Closure|string $abstract, \Closure|null $callback = null) @@ -138,7 +138,7 @@ * @method static void forgetInstances() * @method static void forgetScopedInstances() * @method static void resolveEnvironmentUsing(callable|string|null $callback) - * @method static bool currentEnvironmentIs(array|string $environments) + * @method static bool currentEnvironmentIs(array|string $environments) * @method static \Illuminate\Foundation\Application getInstance() * @method static \Illuminate\Contracts\Container\Container|\Illuminate\Foundation\Application setInstance(\Illuminate\Contracts\Container\Container|null $container = null) * @method static void macro(string $name, object|callable $macro) diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index 49d80828b37d..ab09857f2a6d 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -45,7 +45,7 @@ * @method static \Illuminate\Support\Collection dispatched(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedSync(string $command, callable|null $callback = null) * @method static \Illuminate\Support\Collection dispatchedAfterResponse(string $command, callable|null $callback = null) - * @method static \Illuminate\Support\Collection batched(callable $callback) + * @method static \Illuminate\Support\Collection batched(callable $callback) * @method static bool hasDispatched(string $command) * @method static bool hasDispatchedSync(string $command) * @method static bool hasDispatchedAfterResponse(string $command) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 68880ba704d6..5d98d4c980d6 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -22,13 +22,13 @@ * @method static bool missing(\UnitEnum|string $key) * @method static mixed get(\UnitEnum|array|string $key, mixed $default = null) * @method static array many(array $keys) - * @method static iterable getMultiple(iterable $keys, mixed $default = null) + * @method static iterable getMultiple(iterable $keys, mixed $default = null) * @method static mixed pull(\UnitEnum|array|string $key, mixed $default = null) * @method static string string(\UnitEnum|string $key, \Closure|string|null $default = null) * @method static int integer(\UnitEnum|string $key, \Closure|int|null $default = null) * @method static float float(\UnitEnum|string $key, \Closure|float|null $default = null) * @method static bool boolean(\UnitEnum|string $key, \Closure|bool|null $default = null) - * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) + * @method static array array(\UnitEnum|string $key, \Closure|array|null $default = null) * @method static bool put(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool set(\UnitEnum|array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) @@ -45,7 +45,7 @@ * @method static \Illuminate\Cache\Limiters\ConcurrencyLimiterBuilder funnel(\UnitEnum|string $name) * @method static bool forget(\UnitEnum|array|string $key) * @method static bool delete(\UnitEnum|array|string $key) - * @method static bool deleteMultiple(iterable $keys) + * @method static bool deleteMultiple(iterable $keys) * @method static bool clear() * @method static \Illuminate\Cache\TaggedCache tags(mixed $names) * @method static string|null getName() diff --git a/src/Illuminate/Support/Facades/Config.php b/src/Illuminate/Support/Facades/Config.php index 990e34739f68..09228769a306 100755 --- a/src/Illuminate/Support/Facades/Config.php +++ b/src/Illuminate/Support/Facades/Config.php @@ -5,13 +5,13 @@ /** * @method static bool has(string $key) * @method static mixed get(array|string $key, mixed $default = null) - * @method static array getMany(array $keys) + * @method static array getMany(array $keys) * @method static string string(string $key, \Closure|string|null $default = null) * @method static int integer(string $key, \Closure|int|null $default = null) * @method static float float(string $key, \Closure|float|null $default = null) * @method static bool boolean(string $key, \Closure|bool|null $default = null) - * @method static array array(string $key, \Closure|array|null $default = null) - * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) + * @method static array array(string $key, \Closure|array|null $default = null) + * @method static \Illuminate\Support\Collection collection(string $key, \Closure|array|null $default = null) * @method static void set(array|string $key, mixed $value = null) * @method static void prepend(string $key, mixed $value) * @method static void push(string $key, mixed $value) diff --git a/src/Illuminate/Support/Facades/Context.php b/src/Illuminate/Support/Facades/Context.php index 714ec2b6ddd8..be57f00fa6d0 100644 --- a/src/Illuminate/Support/Facades/Context.php +++ b/src/Illuminate/Support/Facades/Context.php @@ -7,22 +7,22 @@ * @method static bool missing(string $key) * @method static bool hasHidden(string $key) * @method static bool missingHidden(string $key) - * @method static array all() - * @method static array allHidden() + * @method static array all() + * @method static array allHidden() * @method static mixed get(string $key, mixed $default = null) * @method static mixed getHidden(string $key, mixed $default = null) * @method static mixed pull(string $key, mixed $default = null) * @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 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 mixed remember(string $key, mixed $value) * @method static mixed rememberHidden(string $key, mixed $value) - * @method static \Illuminate\Log\Context\Repository forget(string|array $key) - * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) + * @method static \Illuminate\Log\Context\Repository forget(string|array $key) + * @method static \Illuminate\Log\Context\Repository forgetHidden(string|array $key) * @method static \Illuminate\Log\Context\Repository addIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository addHiddenIf(string $key, mixed $value) * @method static \Illuminate\Log\Context\Repository push(string $key, mixed ...$values) @@ -33,7 +33,7 @@ * @method static \Illuminate\Log\Context\Repository decrement(string $key, int $amount = 1) * @method static bool stackContains(string $key, mixed $value, bool $strict = false) * @method static bool hiddenStackContains(string $key, mixed $value, bool $strict = false) - * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) + * @method static mixed scope(callable $callback, array $data = [], array $hidden = []) * @method static bool isEmpty() * @method static \Illuminate\Log\Context\Repository dehydrating(callable $callback) * @method static \Illuminate\Log\Context\Repository hydrated(callable $callback) diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 94f118cfad47..13e87d3923bf 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -23,7 +23,7 @@ * @method static string[] availableDrivers() * @method static void extend(string $name, callable $resolver) * @method static void forgetExtension(string $name) - * @method static array getConnections() + * @method static array getConnections() * @method static void setReconnector(callable $reconnector) * @method static \Illuminate\Database\DatabaseManager setApplication(\Illuminate\Contracts\Foundation\Application $app) * @method static void macro(string $name, object|callable $macro) @@ -42,7 +42,7 @@ * @method static array selectFromWriteConnection(string $query, array $bindings = []) * @method static array select(string $query, array $bindings = [], bool $useReadPdo = true) * @method static array selectResultSets(string $query, array $bindings = [], bool $useReadPdo = true) - * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true) + * @method static \Generator cursor(string $query, array $bindings = [], bool $useReadPdo = true) * @method static bool insert(string $query, array $bindings = []) * @method static int update(string $query, array $bindings = []) * @method static int delete(string $query, array $bindings = []) diff --git a/src/Illuminate/Support/Facades/Exceptions.php b/src/Illuminate/Support/Facades/Exceptions.php index 59b4b07ef2d7..263b95bd0418 100644 --- a/src/Illuminate/Support/Facades/Exceptions.php +++ b/src/Illuminate/Support/Facades/Exceptions.php @@ -15,7 +15,7 @@ * @method static \Illuminate\Foundation\Exceptions\Handler dontReportWhen(callable $dontReportWhen) * @method static \Illuminate\Foundation\Exceptions\Handler ignore(array|string $exceptions) * @method static \Illuminate\Foundation\Exceptions\Handler dontFlash(array|string $attributes) - * @method static \Illuminate\Foundation\Exceptions\Handler level(string<\Throwable> $type, string $level) + * @method static \Illuminate\Foundation\Exceptions\Handler level(string $type, string $level) * @method static void report(\Throwable $e) * @method static bool shouldReport(\Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler throttleUsing(callable $throttleUsing) @@ -26,14 +26,14 @@ * @method static \Illuminate\Foundation\Exceptions\Handler shouldRenderJsonWhen(callable $callback) * @method static \Illuminate\Foundation\Exceptions\Handler dontReportDuplicates() * @method static \Illuminate\Contracts\Debug\ExceptionHandler handler() - * @method static void assertReported(\Closure|string<\Throwable> $exception) + * @method static void assertReported(\Closure|string $exception) * @method static void assertReportedCount(int $count) - * @method static void assertNotReported(\Closure|string<\Throwable> $exception) + * @method static void assertNotReported(\Closure|string $exception) * @method static void assertNothingReported() * @method static void renderForConsole(\Symfony\Component\Console\Output\OutputInterface $output, \Throwable $e) * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwOnReport() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake throwFirstReported() - * @method static array<\Throwable> reported() + * @method static array reported() * @method static \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake setHandler(\Illuminate\Contracts\Debug\ExceptionHandler $handler) * * @see \Illuminate\Foundation\Exceptions\Handler diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 25e3478e183b..50bd17818fca 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -10,21 +10,21 @@ * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalOptions(\Closure|array $options) * @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\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 \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(array|null $only = null) + * @method static \Illuminate\Http\Client\Factory allowStrayRequests(array|null $only = null) * @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|\Closure $callback) - * @method static void assertSentInOrder(array $callbacks) + * @method static void assertSentInOrder(array $callbacks) * @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(\Closure|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() @@ -65,7 +65,7 @@ * @method static \Illuminate\Http\Client\PendingRequest withMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withResponseMiddleware(callable $middleware) - * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) + * @method static \Illuminate\Http\Client\PendingRequest withAttributes(array $attributes) * @method static \Illuminate\Http\Client\PendingRequest beforeSending(callable $callback) * @method static \Illuminate\Http\Client\PendingRequest afterResponse(callable|null $callback) * @method static \Illuminate\Http\Client\PendingRequest throw(callable|null $callback = null) @@ -79,7 +79,7 @@ * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface patch(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface put(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) * @method static \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface delete(string $url, array|\JsonSerializable|\Illuminate\Contracts\Support\Arrayable $data = []) - * @method static array pool(callable $callback, int|null $concurrency = null) + * @method static array pool(callable $callback, int|null $concurrency = null) * @method static \Illuminate\Http\Client\Batch batch(callable $callback) * @method static \Illuminate\Http\Client\Response|\Illuminate\Http\Client\Promises\LazyPromise send(string $method, string $url, array $options = []) * @method static \GuzzleHttp\Client buildClient() @@ -93,9 +93,9 @@ * @method static array mergeOptions(array ...$options) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) * @method static bool isAllowedRequestUrl(string $url) - * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) + * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) * @method static \GuzzleHttp\Promise\PromiseInterface|null getPromise() - * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) + * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) * @method static \Illuminate\Http\Client\PendingRequest dontTruncateExceptions() * @method static \Illuminate\Http\Client\PendingRequest setClient(\GuzzleHttp\Client $client) * @method static \Illuminate\Http\Client\PendingRequest setHandler(callable $handler) diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index ecaaae3b24a8..35dfdca79345 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -6,7 +6,7 @@ use Illuminate\Process\Factory; /** - * @method static \Illuminate\Process\PendingProcess command(array|string $command) + * @method static \Illuminate\Process\PendingProcess command(array|string $command) * @method static \Illuminate\Process\PendingProcess path(string $path) * @method static \Illuminate\Process\PendingProcess timeout(int $timeout) * @method static \Illuminate\Process\PendingProcess idleTimeout(int $timeout) @@ -16,8 +16,8 @@ * @method static \Illuminate\Process\PendingProcess quietly() * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) - * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) - * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) + * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) * @method static bool supportsTty() * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) * @method static \Illuminate\Process\PendingProcess|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 4c86d10e72b2..710365458658 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -61,12 +61,12 @@ * @method static void assertCount(int $expectedCount) * @method static void assertNothingPushed() * @method static \Illuminate\Support\Collection pushed(string $job, callable|null $callback = null) - * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) - * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) + * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) + * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) * @method static bool hasPushed(string $job) * @method static bool shouldFakeJob(object $job) * @method static array pushedJobs() - * @method static array rawPushes() + * @method static array rawPushes() * @method static \Illuminate\Support\Testing\Fakes\QueueFake serializeAndRestore(bool $serializeAndRestore = true) * @method static void releaseUniqueJobLocks() * diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 0ff5c7aac60c..2865715dcb98 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -152,9 +152,9 @@ * @method static string|array|null post(string|null $key = null, string|array|null $default = null) * @method static bool hasCookie(string $key) * @method static string|array|null cookie(string|null $key = null, string|array|null $default = null) - * @method static array allFiles() + * @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) @@ -175,8 +175,8 @@ * @method static float|int clamp(string $key, int|float $min, int|float $max, int|float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, \UnitEnum|string|null $tz = null) * @method static \Carbon\CarbonInterval|null interval(string $key, \Carbon\Unit|string|null $unit = null) - * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string<\BackedEnum> $enumClass, \BackedEnum|null $default = null) - * @method static \BackedEnum[] enums(string $key, string<\BackedEnum> $enumClass) + * @method static \BackedEnum|(\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) * @method static array only(mixed $keys) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index 6b2f0d261122..eabe3ef7cb3a 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -50,7 +50,7 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyFifteenMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThirtyMinutes() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes hourlyAt(array|string|int|int[] $offset) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyOddHour(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyTwoHours(array|string|int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes everyThreeHours(array|string|int $offset = 0) @@ -59,8 +59,8 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daily() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes at(string $time) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes dailyAt(string $time) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDaily(int $first = 1, int $second = 13) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekdays() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekends() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes mondays() @@ -73,14 +73,14 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weekly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes weeklyOn(mixed $dayOfWeek, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes monthlyOn(int $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes lastDayOfMonth(string $time = '0:0') - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array>|int ...$days) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes daysOfMonth(array|int ...$days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterly() * @method static \Illuminate\Console\Scheduling\PendingEventAttributes quarterlyOn(int $dayOfQuarter = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearly() - * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0') * @method static \Illuminate\Console\Scheduling\PendingEventAttributes days(mixed $days) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes timezone(\UnitEnum|\DateTimeZone|string $timezone) * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 523813f228ed..5c617687bb3a 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -10,31 +10,31 @@ * @method static void morphUsingUlids() * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) - * @method static array getSchemas() + * @method static array getSchemas() * @method static bool hasTable(string $table) * @method static bool hasView(string $view) - * @method static array getTables(string|string[]|null $schema = null) - * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) - * @method static array getViews(string|string[]|null $schema = null) - * @method static array getTypes(string|string[]|null $schema = null) + * @method static array getTables(string|string[]|null $schema = null) + * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) + * @method static array getViews(string|string[]|null $schema = null) + * @method static array getTypes(string|string[]|null $schema = null) * @method static bool hasColumn(string $table, string $column) - * @method static bool hasColumns(string $table, array $columns) + * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) * @method static void whenTableDoesntHaveColumn(string $table, string $column, \Closure $callback) * @method static void whenTableHasIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static void whenTableDoesntHaveIndex(string $table, string|array $index, \Closure $callback, string|null $type = null) * @method static string getColumnType(string $table, string $column, bool $fullDefinition = false) - * @method static array getColumnListing(string $table) - * @method static array getColumns(string $table) - * @method static array getIndexes(string $table) - * @method static array getIndexListing(string $table) + * @method static array getColumnListing(string $table) + * @method static array getColumns(string $table) + * @method static array getIndexes(string $table) + * @method static array getIndexListing(string $table) * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) * @method static void dropIfExists(string $table) - * @method static void dropColumns(string $table, string|array $columns) + * @method static void dropColumns(string $table, string|array $columns) * @method static void dropAllTables() * @method static void dropAllViews() * @method static void dropAllTypes() diff --git a/src/Illuminate/Support/Facades/Storage.php b/src/Illuminate/Support/Facades/Storage.php index bac3e2a814bd..6a12ab40e92b 100644 --- a/src/Illuminate/Support/Facades/Storage.php +++ b/src/Illuminate/Support/Facades/Storage.php @@ -40,10 +40,10 @@ * @method static bool move(string $from, string $to) * @method static int size(string $path) * @method static int lastModified(string $path) - * @method static array files(string|null $directory = null, bool $recursive = false) - * @method static array allFiles(string|null $directory = null) - * @method static array directories(string|null $directory = null, bool $recursive = false) - * @method static array allDirectories(string|null $directory = null) + * @method static array files(string|null $directory = null, bool $recursive = false) + * @method static array allFiles(string|null $directory = null) + * @method static array directories(string|null $directory = null, bool $recursive = false) + * @method static array allDirectories(string|null $directory = null) * @method static bool makeDirectory(string $path) * @method static bool deleteDirectory(string $directory) * @method static \Illuminate\Filesystem\FilesystemAdapter assertExists(string|array $path, string|null $content = null) @@ -81,7 +81,7 @@ * @method static mixed macroCall(string $method, array $parameters) * @method static bool has(string $location) * @method static string read(string $location) - * @method static \League\Flysystem\DirectoryListing<\League\Flysystem\StorageAttributes> listContents(string $location, bool $deep = false) + * @method static \League\Flysystem\DirectoryListing listContents(string $location, bool $deep = false) * @method static int fileSize(string $path) * @method static string visibility(string $path) * @method static void write(string $location, string $contents, array $config = []) From f083ac3f67861baa8244448d3561463ec32eaed5 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Sat, 18 Apr 2026 15:28:59 +0200 Subject: [PATCH 195/596] Normalize Carbon (#59750) Co-authored-by: Lucas Michot --- src/Illuminate/Bus/DebounceLock.php | 4 ++-- src/Illuminate/Collections/LazyCollection.php | 2 +- tests/Bus/BusBatchTest.php | 2 +- tests/Cache/CacheArrayStoreTest.php | 4 ++-- tests/Cache/CacheFileStoreTest.php | 2 +- tests/Cache/CacheMemcachedStoreTest.php | 4 ++-- tests/Cache/CacheRateLimiterTest.php | 2 +- tests/Cache/CacheRepositoryTest.php | 2 +- tests/Cache/CacheSessionStoreTest.php | 2 +- tests/Console/ConsoleScheduledEventTest.php | 2 +- tests/Console/Scheduling/FrequencyTest.php | 2 +- .../Database/DatabaseEloquentBuilderTest.php | 4 ++-- .../DatabaseEloquentIntegrationTest.php | 2 +- .../DatabaseEloquentIrregularPluralTest.php | 2 +- tests/Database/DatabaseEloquentModelTest.php | 2 +- ...baseEloquentSoftDeletesIntegrationTest.php | 2 +- .../DatabaseEloquentTimestampsTest.php | 2 +- tests/Database/QueryDurationThresholdTest.php | 2 +- tests/Events/QueuedEventsTest.php | 2 +- tests/Http/Middleware/CacheTest.php | 2 +- .../Console/CommandDurationThresholdTest.php | 14 +++++------ tests/Integration/Cookie/CookieTest.php | 2 +- .../Integration/Database/DatabaseLockTest.php | 2 +- .../Database/MariaDb/EloquentCastTest.php | 18 +++++++-------- .../Database/MySql/EloquentCastTest.php | 18 +++++++-------- .../Filesystem/ReceiveFileTest.php | 10 ++++---- .../Integration/Filesystem/ServeFileTest.php | 6 ++--- .../Http/RequestDurationThresholdTest.php | 16 ++++++------- .../Integration/Http/ThrottleRequestsTest.php | 14 +++++------ .../Mail/SendingMailWithLocaleTest.php | 2 +- .../SendingNotificationsWithLocaleTest.php | 2 +- tests/Integration/Queue/DebouncedJobTest.php | 23 ++++++++++--------- tests/Integration/Queue/DynamoBatchTest.php | 6 ++--- tests/Integration/Queue/RateLimitedTest.php | 2 +- .../Queue/ThrottlesExceptionsTest.php | 4 ++-- tests/Queue/DynamoDbFailedJobProviderTest.php | 2 +- tests/Queue/FileFailedJobProviderTest.php | 8 +++---- .../QueueDatabaseQueueIntegrationTest.php | 8 +++---- tests/Queue/QueueDatabaseQueueUnitTest.php | 2 +- tests/Session/ArraySessionHandlerTest.php | 2 +- tests/Support/SleepTest.php | 4 ++-- tests/Support/SupportCarbonTest.php | 6 ++--- tests/Validation/ValidationValidatorTest.php | 2 +- 43 files changed, 111 insertions(+), 110 deletions(-) diff --git a/src/Illuminate/Bus/DebounceLock.php b/src/Illuminate/Bus/DebounceLock.php index 31489d0bb6cb..d92057c5ff58 100644 --- a/src/Illuminate/Bus/DebounceLock.php +++ b/src/Illuminate/Bus/DebounceLock.php @@ -68,12 +68,12 @@ protected function maxWaitExceeded(Cache $cache, string $key, int $ttl, ?int $ma $timestampKey = $key.':first_dispatched_at'; if (! $cache->has($timestampKey)) { - $cache->put($timestampKey, Carbon::now()->timestamp, $ttl); + $cache->put($timestampKey, Carbon::now()->getTimestamp(), $ttl); return false; } - $elapsed = Carbon::now()->timestamp - $cache->get($timestampKey); + $elapsed = Carbon::now()->getTimestamp() - $cache->get($timestampKey); if ($elapsed >= $maxWait) { $cache->forget($timestampKey); diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index dc65a68d677e..f3205e95c088 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -1937,7 +1937,7 @@ protected function passthru($method, array $params) protected function now() { return class_exists(Carbon::class) - ? Carbon::now()->timestamp + ? Carbon::now()->getTimestamp() : time(); } diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 376bcd4c56c0..670d1073ad92 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -739,7 +739,7 @@ public function test_options_unserialize_on_postgres($serialize, $options) 'failed_jobs' => '', 'failed_job_ids' => '[]', 'options' => $serialize, - 'created_at' => Carbon::now()->timestamp, + 'created_at' => Carbon::now()->getTimestamp(), 'cancelled_at' => null, 'finished_at' => null, ]); diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 98a8cadceb77..01ede6b24eda 100755 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -11,7 +11,7 @@ class CacheArrayStoreTest extends TestCase { protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } @@ -248,7 +248,7 @@ public function testLockWithNoExpirationNeverExpires() $store = new ArrayStore; $lock = $store->lock('foo'); $lock->acquire(); - Carbon::setTestNow(Carbon::now()->addYears(100)); + Carbon::setTestNow(Carbon::now()->addCentury()); $this->assertFalse($lock->acquire()); } diff --git a/tests/Cache/CacheFileStoreTest.php b/tests/Cache/CacheFileStoreTest.php index 04fc171a23b1..32e0777370a5 100755 --- a/tests/Cache/CacheFileStoreTest.php +++ b/tests/Cache/CacheFileStoreTest.php @@ -16,7 +16,7 @@ class CacheFileStoreTest extends TestCase { protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Cache/CacheMemcachedStoreTest.php b/tests/Cache/CacheMemcachedStoreTest.php index 0afcfcc0f23f..2d92636c2154 100755 --- a/tests/Cache/CacheMemcachedStoreTest.php +++ b/tests/Cache/CacheMemcachedStoreTest.php @@ -53,11 +53,11 @@ public function testSetMethodProperlyCallsMemcache() { Carbon::setTestNow($now = Carbon::now()); $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['set'])->getMock(); - $memcache->expects($this->once())->method('set')->with('foo', 'bar', $now->timestamp + 60)->willReturn(true); + $memcache->expects($this->once())->method('set')->with('foo', 'bar', $now->addMinute()->getTimestamp())->willReturn(true); $store = new MemcachedStore($memcache); $result = $store->put('foo', 'bar', 60); $this->assertTrue($result); - Carbon::setTestNow(null); + Carbon::setTestNow(); } public function testTouchMethodProperlyCallsMemcache(): void diff --git a/tests/Cache/CacheRateLimiterTest.php b/tests/Cache/CacheRateLimiterTest.php index 58496b737b10..b5cc99e07848 100644 --- a/tests/Cache/CacheRateLimiterTest.php +++ b/tests/Cache/CacheRateLimiterTest.php @@ -121,7 +121,7 @@ public function testClearClearsTheCacheKeys() public function testAvailableInReturnsPositiveValues() { $cache = m::mock(Cache::class); - $cache->shouldReceive('get')->andReturn(Carbon::now()->subSeconds(60)->getTimestamp(), null); + $cache->shouldReceive('get')->andReturn(Carbon::now()->subMinute()->getTimestamp(), null); $cache->shouldReceive('getStore')->andReturn(new ArrayStore); $rateLimiter = new RateLimiter($cache); diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 716a42792459..b907d5babe99 100755 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -39,7 +39,7 @@ protected function setUp(): void protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); Repository::handleUnserializableClassUsing(null); parent::tearDown(); diff --git a/tests/Cache/CacheSessionStoreTest.php b/tests/Cache/CacheSessionStoreTest.php index 02dda73f6d6c..cd3f745b6231 100755 --- a/tests/Cache/CacheSessionStoreTest.php +++ b/tests/Cache/CacheSessionStoreTest.php @@ -13,7 +13,7 @@ class CacheSessionStoreTest extends TestCase { protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Console/ConsoleScheduledEventTest.php b/tests/Console/ConsoleScheduledEventTest.php index 5df90969ca33..0be6d04efbb1 100644 --- a/tests/Console/ConsoleScheduledEventTest.php +++ b/tests/Console/ConsoleScheduledEventTest.php @@ -27,7 +27,7 @@ protected function setUp(): void protected function tearDown(): void { date_default_timezone_set($this->defaultTimezone); - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Console/Scheduling/FrequencyTest.php b/tests/Console/Scheduling/FrequencyTest.php index ddb19369f950..98170e8364a2 100644 --- a/tests/Console/Scheduling/FrequencyTest.php +++ b/tests/Console/Scheduling/FrequencyTest.php @@ -124,7 +124,7 @@ public function testLastDayOfMonth() $this->assertSame('0 0 31 * *', $this->event->lastDayOfMonth()->getExpression()); - Carbon::setTestNow(null); + Carbon::setTestNow(); } public function testTwiceMonthly() diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 55f10f8bf0ba..48cf8bb8b67c 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -29,7 +29,7 @@ class DatabaseEloquentBuilderTest extends TestCase { protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } @@ -2679,7 +2679,7 @@ public function testUpdateWithAliasWithQualifiedTimestampValue() $result = $builder->from('table as alias')->update(['foo' => 'bar', 'alias.updated_at' => null]); $this->assertEquals(1, $result); - Carbon::setTestNow(null); + Carbon::setTestNow(); } public function testUpsert() diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index ab1c809914c3..5dc1e52f209b 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -205,7 +205,7 @@ protected function tearDown(): void Relation::morphMap([], false); Eloquent::unsetConnectionResolver(); - Carbon::setTestNow(null); + Carbon::setTestNow(); Str::createUuidsNormally(); DB::flushQueryLog(); diff --git a/tests/Database/DatabaseEloquentIrregularPluralTest.php b/tests/Database/DatabaseEloquentIrregularPluralTest.php index 3aaa504acd1a..a88bdf7e9c7c 100644 --- a/tests/Database/DatabaseEloquentIrregularPluralTest.php +++ b/tests/Database/DatabaseEloquentIrregularPluralTest.php @@ -59,7 +59,7 @@ protected function tearDown(): void $this->schema()->drop('irregular_plural_humans'); $this->schema()->drop('irregular_plural_human_irregular_plural_token'); - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 40f3f625ef4d..0930a9c07f24 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -72,7 +72,7 @@ class DatabaseEloquentModelTest extends TestCase protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); Model::unsetEventDispatcher(); Carbon::resetToStringFormat(); diff --git a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php index 56105e096b0f..ff7259c22689 100644 --- a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php +++ b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php @@ -93,7 +93,7 @@ public function createSchema() */ protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); $this->schema()->drop('users'); $this->schema()->drop('posts'); diff --git a/tests/Database/DatabaseEloquentTimestampsTest.php b/tests/Database/DatabaseEloquentTimestampsTest.php index df92de947a10..545d4a22876a 100644 --- a/tests/Database/DatabaseEloquentTimestampsTest.php +++ b/tests/Database/DatabaseEloquentTimestampsTest.php @@ -63,7 +63,7 @@ protected function tearDown(): void $this->schema()->drop('users'); $this->schema()->drop('users_created_at'); $this->schema()->drop('users_updated_at'); - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Database/QueryDurationThresholdTest.php b/tests/Database/QueryDurationThresholdTest.php index ddcba33a3e90..8f1db531aa51 100644 --- a/tests/Database/QueryDurationThresholdTest.php +++ b/tests/Database/QueryDurationThresholdTest.php @@ -19,7 +19,7 @@ class QueryDurationThresholdTest extends TestCase protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index c350b0f6a096..db6f526ed279 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -670,7 +670,7 @@ class TestDispatcherOptions implements ShouldQueue public function retryUntil() { - return Carbon::now()->addHour(1); + return Carbon::now()->addHour(); } public function tries() diff --git a/tests/Http/Middleware/CacheTest.php b/tests/Http/Middleware/CacheTest.php index 242991854a2b..c0f05db0a249 100644 --- a/tests/Http/Middleware/CacheTest.php +++ b/tests/Http/Middleware/CacheTest.php @@ -160,7 +160,7 @@ public function testLastModifiedStringDate() return new Response('some content'); }, "last_modified=$birthdate"); - $this->assertSame(Carbon::parse($birthdate)->timestamp, $response->getLastModified()->getTimestamp()); + $this->assertSame(Carbon::parse($birthdate)->getTimestamp(), $response->getLastModified()->getTimestamp()); } public function testTrailingDelimiterIgnored() diff --git a/tests/Integration/Console/CommandDurationThresholdTest.php b/tests/Integration/Console/CommandDurationThresholdTest.php index 1c8df434a2c6..81617413f416 100644 --- a/tests/Integration/Console/CommandDurationThresholdTest.php +++ b/tests/Integration/Console/CommandDurationThresholdTest.php @@ -27,7 +27,7 @@ public function testItCanHandleExceedingCommandDuration(): void $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); $kernel->terminate($input, 21); $this->assertTrue($called); @@ -48,7 +48,7 @@ public function testItDoesntCallWhenExactlyThresholdDuration(): void $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($input, 21); $this->assertFalse($called); @@ -66,7 +66,7 @@ public function testItProvidesArgsToHandler(): void Carbon::setTestNow($startedAt = Carbon::now()); $kernel->handle($input, new ConsoleOutput); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($input, 21); $this->assertCount(3, $args); @@ -90,7 +90,7 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds(): $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); $kernel->terminate($input, 21); $this->assertTrue($called); @@ -111,7 +111,7 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds( $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($input, 21); $this->assertFalse($called); @@ -135,7 +135,7 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime(): void $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMillisecond()); + Carbon::setTestNow(Carbon::now()->addSecond()->addMillisecond()); $kernel->terminate($input, 21); @@ -158,7 +158,7 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime(): v $this->assertFalse($called); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($input, 21); $this->assertFalse($called); diff --git a/tests/Integration/Cookie/CookieTest.php b/tests/Integration/Cookie/CookieTest.php index ed5223022a91..e351b5bfcbe7 100644 --- a/tests/Integration/Cookie/CookieTest.php +++ b/tests/Integration/Cookie/CookieTest.php @@ -39,7 +39,7 @@ public function test_cookie_is_sent_back_with_proper_expire_time_with_respect_to Carbon::setTestNow(Carbon::now()); $response = $this->get('/'); $this->assertCount(2, $response->headers->getCookies()); - $this->assertEquals(Carbon::now()->getTimestamp() + 60, $response->headers->getCookies()[1]->getExpiresTime()); + $this->assertEquals(Carbon::now()->addMinute()->getTimestamp(), $response->headers->getCookies()[1]->getExpiresTime()); } protected function defineEnvironment($app) diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php index d795b7835b1f..bac55dd30405 100644 --- a/tests/Integration/Database/DatabaseLockTest.php +++ b/tests/Integration/Database/DatabaseLockTest.php @@ -57,7 +57,7 @@ public function testExpiredLockCanBeRetrieved() { $lock = Cache::driver('database')->lock('foo'); $this->assertTrue($lock->get()); - DB::table('cache_locks')->update(['expiration' => Carbon::now()->subDays(1)->getTimestamp()]); + DB::table('cache_locks')->update(['expiration' => Carbon::now()->subDay()->getTimestamp()]); $otherLock = Cache::driver('database')->lock('foo'); $this->assertTrue($otherLock->get()); diff --git a/tests/Integration/Database/MariaDb/EloquentCastTest.php b/tests/Integration/Database/MariaDb/EloquentCastTest.php index 25ce7b141d3d..980c1399f235 100644 --- a/tests/Integration/Database/MariaDb/EloquentCastTest.php +++ b/tests/Integration/Database/MariaDb/EloquentCastTest.php @@ -37,7 +37,7 @@ protected function destroyDatabaseMigrations() public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() { Carbon::setTestNow(Carbon::now()); - $createdAt = Carbon::now()->timestamp; + $createdAt = Carbon::now()->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -80,7 +80,7 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() { Carbon::setTestNow(Carbon::now()); - $createdAt = Carbon::now()->timestamp; + $createdAt = Carbon::now()->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -100,7 +100,7 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); Carbon::setTestNow(Carbon::now()->addSecond()); - $updatedAt = Carbon::now()->timestamp; + $updatedAt = Carbon::now()->getTimestamp(); $castUser->update([ 'email' => fake()->unique()->email, @@ -134,7 +134,7 @@ public function testItCastTimestampsUpdatedByAMutator() $this->assertNull($mutatorUser->updated_at); Carbon::setTestNow(Carbon::now()->addSecond()); - $updatedAt = Carbon::now()->timestamp; + $updatedAt = Carbon::now()->getTimestamp(); $mutatorUser->update([ 'email' => fake()->unique()->email, @@ -166,7 +166,7 @@ public function get($model, string $key, $value, array $attributes) public function set($model, string $key, $value, array $attributes) { - return Carbon::parse($value)->timestamp; + return Carbon::parse($value)->getTimestamp(); } } @@ -180,7 +180,7 @@ protected function updatedAt(): Attribute { return Attribute::make( get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + set: fn ($value) => Carbon::parse($value)->getTimestamp(), ); } @@ -188,7 +188,7 @@ protected function createdAt(): Attribute { return Attribute::make( get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + set: fn ($value) => Carbon::parse($value)->getTimestamp(), ); } } @@ -206,7 +206,7 @@ protected function getUpdatedAtAttribute($value) protected function setUpdatedAtAttribute($value) { - $this->attributes['updated_at'] = Carbon::parse($value)->timestamp; + $this->attributes['updated_at'] = Carbon::parse($value)->getTimestamp(); } protected function getCreatedAtAttribute($value) @@ -216,7 +216,7 @@ protected function getCreatedAtAttribute($value) protected function setCreatedAtAttribute($value) { - $this->attributes['created_at'] = Carbon::parse($value)->timestamp; + $this->attributes['created_at'] = Carbon::parse($value)->getTimestamp(); } } diff --git a/tests/Integration/Database/MySql/EloquentCastTest.php b/tests/Integration/Database/MySql/EloquentCastTest.php index 131085d15077..3a2b80f8e4d4 100644 --- a/tests/Integration/Database/MySql/EloquentCastTest.php +++ b/tests/Integration/Database/MySql/EloquentCastTest.php @@ -37,7 +37,7 @@ protected function destroyDatabaseMigrations() public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() { Carbon::setTestNow(Carbon::now()); - $createdAt = Carbon::now()->timestamp; + $createdAt = Carbon::now()->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -80,7 +80,7 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed() public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() { Carbon::setTestNow(Carbon::now()); - $createdAt = Carbon::now()->timestamp; + $createdAt = Carbon::now()->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -100,7 +100,7 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed() $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); Carbon::setTestNow(Carbon::now()->addSecond()); - $updatedAt = Carbon::now()->timestamp; + $updatedAt = Carbon::now()->getTimestamp(); $castUser->update([ 'email' => fake()->unique()->email, @@ -134,7 +134,7 @@ public function testItCastTimestampsUpdatedByAMutator() $this->assertNull($mutatorUser->updated_at); Carbon::setTestNow(Carbon::now()->addSecond()); - $updatedAt = Carbon::now()->timestamp; + $updatedAt = Carbon::now()->getTimestamp(); $mutatorUser->update([ 'email' => fake()->unique()->email, @@ -166,7 +166,7 @@ public function get($model, string $key, $value, array $attributes) public function set($model, string $key, $value, array $attributes) { - return Carbon::parse($value)->timestamp; + return Carbon::parse($value)->getTimestamp(); } } @@ -180,7 +180,7 @@ protected function updatedAt(): Attribute { return Attribute::make( get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + set: fn ($value) => Carbon::parse($value)->getTimestamp(), ); } @@ -188,7 +188,7 @@ protected function createdAt(): Attribute { return Attribute::make( get: fn ($value) => Carbon::parse($value), - set: fn ($value) => Carbon::parse($value)->timestamp, + set: fn ($value) => Carbon::parse($value)->getTimestamp(), ); } } @@ -206,7 +206,7 @@ protected function getUpdatedAtAttribute($value) protected function setUpdatedAtAttribute($value) { - $this->attributes['updated_at'] = Carbon::parse($value)->timestamp; + $this->attributes['updated_at'] = Carbon::parse($value)->getTimestamp(); } protected function getCreatedAtAttribute($value) @@ -216,7 +216,7 @@ protected function getCreatedAtAttribute($value) protected function setCreatedAtAttribute($value) { - $this->attributes['created_at'] = Carbon::parse($value)->timestamp; + $this->attributes['created_at'] = Carbon::parse($value)->getTimestamp(); } } diff --git a/tests/Integration/Filesystem/ReceiveFileTest.php b/tests/Integration/Filesystem/ReceiveFileTest.php index 974ce1a28582..600b5506c465 100644 --- a/tests/Integration/Filesystem/ReceiveFileTest.php +++ b/tests/Integration/Filesystem/ReceiveFileTest.php @@ -21,7 +21,7 @@ protected function setUp(): void public function testItCanReceiveAFile() { - $result = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->addMinutes(1)); + $result = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->addMinute()); $response = $this->call('PUT', $result['url'], [], [], [], [], 'Hello World'); @@ -31,7 +31,7 @@ public function testItCanReceiveAFile() public function testItWill403OnWrongSignature() { - $result = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->addMinutes(1)); + $result = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->addMinute()); $url = $result['url'].'c'; @@ -43,7 +43,7 @@ public function testItWill403OnWrongSignature() public function testItWill403OnExpiredUrl() { - $result = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->subMinutes(1)); + $result = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->subMinute()); $response = $this->call('PUT', $result['url'], [], [], [], [], 'Hello World'); @@ -55,7 +55,7 @@ public function testDownloadUrlCannotBeUsedForUpload() { Storage::put('receive-file-test.txt', 'Original Content'); - $downloadUrl = Storage::temporaryUrl('receive-file-test.txt', Carbon::now()->addMinutes(1)); + $downloadUrl = Storage::temporaryUrl('receive-file-test.txt', Carbon::now()->addMinute()); $response = $this->call('PUT', $downloadUrl, [], [], [], [], 'Malicious Content'); @@ -67,7 +67,7 @@ public function testUploadUrlCannotBeUsedForDownload() { Storage::put('receive-file-test.txt', 'Secret Content'); - $uploadUrl = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->addMinutes(1)); + $uploadUrl = Storage::temporaryUploadUrl('receive-file-test.txt', Carbon::now()->addMinute()); $response = $this->get($uploadUrl['url']); diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php index 5b6a2f2859fd..616bffe42c12 100644 --- a/tests/Integration/Filesystem/ServeFileTest.php +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -25,7 +25,7 @@ protected function setUp(): void public function testItCanServeAnExistingFile() { - $url = Storage::temporaryUrl('serve-file-test.txt', Carbon::now()->addMinutes(1)); + $url = Storage::temporaryUrl('serve-file-test.txt', Carbon::now()->addMinute()); $response = $this->get($url); @@ -34,7 +34,7 @@ public function testItCanServeAnExistingFile() public function testItWill404OnMissingFile() { - $url = Storage::temporaryUrl('serve-missing-test.txt', Carbon::now()->addMinutes(1)); + $url = Storage::temporaryUrl('serve-missing-test.txt', Carbon::now()->addMinute()); $response = $this->get($url); @@ -43,7 +43,7 @@ public function testItWill404OnMissingFile() public function testItWill403OnWrongSignature() { - $url = Storage::temporaryUrl('serve-file-test.txt', Carbon::now()->addMinutes(1)); + $url = Storage::temporaryUrl('serve-file-test.txt', Carbon::now()->addMinute()); $url = $url.'c'; diff --git a/tests/Integration/Http/RequestDurationThresholdTest.php b/tests/Integration/Http/RequestDurationThresholdTest.php index 2aade582e458..4cb8d8a43b7f 100644 --- a/tests/Integration/Http/RequestDurationThresholdTest.php +++ b/tests/Integration/Http/RequestDurationThresholdTest.php @@ -27,7 +27,7 @@ public function testItCanHandleExceedingRequestDuration() Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); $kernel->terminate($request, $response); $this->assertTrue($called); @@ -47,7 +47,7 @@ public function testItDoesntCallWhenExactlyThresholdDuration() Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($request, $response); $this->assertFalse($called); @@ -106,7 +106,7 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds() Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); $kernel->terminate($request, $response); $this->assertTrue($called); @@ -126,7 +126,7 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds( Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($request, $response); $this->assertFalse($called); @@ -139,14 +139,14 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime() $response = new Response(); $called = false; $kernel = $this->app[Kernel::class]; - $kernel->whenRequestLifecycleIsLongerThan(Carbon::now()->addSeconds(1), function () use (&$called) { + $kernel->whenRequestLifecycleIsLongerThan(Carbon::now()->addSecond(), function () use (&$called) { $called = true; }); Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); $kernel->terminate($request, $response); $this->assertTrue($called); @@ -159,14 +159,14 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime() $response = new Response(); $called = false; $kernel = $this->app[Kernel::class]; - $kernel->whenRequestLifecycleIsLongerThan(Carbon::now()->addSeconds(1), function () use (&$called) { + $kernel->whenRequestLifecycleIsLongerThan(Carbon::now()->addSecond(), function () use (&$called) { $called = true; }); Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); $kernel->terminate($request, $response); $this->assertFalse($called); diff --git a/tests/Integration/Http/ThrottleRequestsTest.php b/tests/Integration/Http/ThrottleRequestsTest.php index 58f8444113a3..f80194af54cf 100644 --- a/tests/Integration/Http/ThrottleRequestsTest.php +++ b/tests/Integration/Http/ThrottleRequestsTest.php @@ -164,7 +164,7 @@ public function testItCanThrottlePerMinute(string $middleware) $response = $this->get('/'); $response->assertStatus(429); $response->assertHeader('Retry-After', 57); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSeconds(57)->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSeconds(57)->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 3); $response->assertHeader('X-RateLimit-Remaining', 0); @@ -176,7 +176,7 @@ public function testItCanThrottlePerMinute(string $middleware) $response = $this->get('/'); $response->assertStatus(429); $response->assertHeader('Retry-After', 1); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSeconds(1)->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 3); $response->assertHeader('X-RateLimit-Remaining', 0); @@ -222,7 +222,7 @@ public function testItCanThrottlePerSecond() $response = $this->get('/'); $response->assertStatus(429); $response->assertHeader('Retry-After', 1); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 3); $response->assertHeader('X-RateLimit-Remaining', 0); @@ -233,7 +233,7 @@ public function testItCanThrottlePerSecond() $response = $this->get('/'); $response->assertHeader('Retry-After', 1); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 3); $response->assertHeader('X-RateLimit-Remaining', 0); @@ -280,7 +280,7 @@ public function testItCanCombineRateLimitsWithoutSpecifyingUniqueKeys() $response = $this->get('/'); $response->assertStatus(429); $response->assertHeader('Retry-After', 1); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 3); $response->assertHeader('X-RateLimit-Remaining', 0); @@ -291,7 +291,7 @@ public function testItCanCombineRateLimitsWithoutSpecifyingUniqueKeys() $response = $this->get('/'); $response->assertHeader('Retry-After', 1); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSecond()->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 3); $response->assertHeader('X-RateLimit-Remaining', 0); @@ -319,7 +319,7 @@ public function testItCanCombineRateLimitsWithoutSpecifyingUniqueKeys() $response = $this->get('/'); $response->assertStatus(429); $response->assertHeader('Retry-After', 59); - $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSeconds(59)->timestamp); + $response->assertHeader('X-RateLimit-Reset', Carbon::now()->addSeconds(59)->getTimestamp()); $response->assertHeader('X-RateLimit-Limit', 5); $response->assertHeader('X-RateLimit-Remaining', 0); } diff --git a/tests/Integration/Mail/SendingMailWithLocaleTest.php b/tests/Integration/Mail/SendingMailWithLocaleTest.php index 06a6dc096e45..54a1d0d48342 100644 --- a/tests/Integration/Mail/SendingMailWithLocaleTest.php +++ b/tests/Integration/Mail/SendingMailWithLocaleTest.php @@ -79,7 +79,7 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled() $this->assertSame('en', Carbon::getLocale()); - Carbon::setTestNow(null); + Carbon::setTestNow(); } public function testLocaleIsSentWithModelPreferredLocale() diff --git a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php index 95a0f4e39f38..fc5f4fea5dc3 100644 --- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php +++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php @@ -143,7 +143,7 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled() $this->assertSame('en', Carbon::getLocale()); - Carbon::setTestNow(null); + Carbon::setTestNow(); } public function testLocaleIsSentWithNotifiablePreferredLocale() diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php index 6068f7fe35d1..c1bee70f4668 100644 --- a/tests/Integration/Queue/DebouncedJobTest.php +++ b/tests/Integration/Queue/DebouncedJobTest.php @@ -12,6 +12,7 @@ use Illuminate\Queue\Attributes\DebounceFor; use Illuminate\Queue\Events\JobDebounced; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache as CacheFacade; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; @@ -37,7 +38,7 @@ public function testDebouncedJobDispatchesAndExecutes() DebouncedTestJob::$handled = false; dispatch(new DebouncedTestJob('entity-1')); - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue(DebouncedTestJob::$handled); @@ -55,7 +56,7 @@ public function testSupersededDebouncedJobIsSkipped() dispatch(new DebouncedTestJob('entity-1')); // Advance time past the debounce window so jobs become available. - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); // Process both jobs from the queue. $this->runQueueWorkerCommand(['--once' => true], 2); @@ -71,7 +72,7 @@ public function testTokenPersistsAfterSuccessfulExecution() DebouncedTestJob::$handled = false; dispatch($job = new DebouncedTestJob('entity-1')); - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue($job::$handled); @@ -110,7 +111,7 @@ public function testJobDebouncedEventFiresForSupersededJob() dispatch(new DebouncedTestJob('entity-1')); dispatch(new DebouncedTestJob('entity-1')); - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); $this->runQueueWorkerCommand(['--once' => true], 2); $this->assertEquals(1, $firedCount); @@ -143,7 +144,7 @@ public function testDifferentDebounceIdsDoNotInterfere() dispatch(new DebouncedTestJob('entity-1')); dispatch(new DebouncedTestJob('entity-2')); - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); $this->runQueueWorkerCommand(['--once' => true], 2); // Both should execute — different identities. @@ -180,7 +181,7 @@ public function testJobExecutesWhenCacheTokenIsEvicted() // Simulate cache eviction by manually removing the debounce token. $this->app->get(Cache::class)->forget(DebounceLock::getKey($job)); - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); $this->runQueueWorkerCommand(['--once' => true]); // Job should execute (fail-open) even though token was evicted. @@ -221,7 +222,7 @@ public function testReleaseClearsMaxWaitTimestamp() $this->assertNull($cache->get(DebounceLock::getKey($job).':first_dispatched_at')); // If timestamp cleanup worked, max wait should not appear exceeded. - $this->travelTo(now()->addSeconds(61)); + $this->travelTo(Carbon::now()->addSeconds(61)); $second = $lock->acquire($job); @@ -241,7 +242,7 @@ public function testSupersededDebouncedJobDoesNotDispatchChain() // Second dispatch supersedes the first (no chain). dispatch(new DebouncedTestJob('entity-1')); - $this->travelTo(now()->addSeconds(31)); + $this->travelTo(Carbon::now()->addSeconds(31)); $this->runQueueWorkerCommand(['--once' => true], 3); // Only the second dispatch should have executed. @@ -278,11 +279,11 @@ public function testMaxDebounceWaitForcesImmediateExecution() dispatch(new DebouncedWithMaxWaitJob('entity-1')); // Second dispatch at t=50 (within maxWait of 60s). - $this->travelTo(now()->addSeconds(50)); + $this->travelTo(Carbon::now()->addSeconds(50)); dispatch(new DebouncedWithMaxWaitJob('entity-1')); // Third dispatch at t=61 — exceeds maxWait. - $this->travelTo(now()->addSeconds(11)); + $this->travelTo(Carbon::now()->addSeconds(11)); $job = new DebouncedWithMaxWaitJob('entity-1'); $pending = dispatch($job); unset($pending); @@ -303,7 +304,7 @@ public function testDebounceWithoutMaxWaitAllowsIndefiniteDelay() $this->assertEquals(30, $job1->delay); // Dispatch again much later — still gets the full delay. - $this->travelTo(now()->addSeconds(600)); + $this->travelTo(Carbon::now()->addMinutes(10)); $job2 = new DebouncedTestJob('entity-1'); $pending2 = dispatch($job2); unset($pending2); diff --git a/tests/Integration/Queue/DynamoBatchTest.php b/tests/Integration/Queue/DynamoBatchTest.php index 1816ad5720e9..d9c4dd7825c8 100644 --- a/tests/Integration/Queue/DynamoBatchTest.php +++ b/tests/Integration/Queue/DynamoBatchTest.php @@ -67,7 +67,7 @@ public function test_retrieve_batch_by_id() $retrieved = $repo->find($batch->id); $this->assertEquals(2, $retrieved->totalJobs); $this->assertEquals(0, $retrieved->failedJobs); - $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSecond(30), Carbon::now())); + $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSecond(), Carbon::now())); } public function test_retrieve_non_existent_batch() @@ -114,8 +114,8 @@ public function test_batch_with_failing_job() $retrieved = $repo->find($batch->id); $this->assertEquals(2, $retrieved->totalJobs); $this->assertEquals(1, $retrieved->failedJobs); - $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSecond(30), Carbon::now())); - $this->assertTrue($retrieved->cancelledAt->between(Carbon::now()->subSecond(30), Carbon::now())); + $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSecond(), Carbon::now())); + $this->assertTrue($retrieved->cancelledAt->between(Carbon::now()->subSecond(), Carbon::now())); } public function test_get_batches() diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index fd6671fe20d2..7317aca951a1 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -266,7 +266,7 @@ public function release() $this->assertSame($job, $result); $this->assertFalse($job->released); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); } $result = $middleware->handle($job = $jobFactory(), $next); diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index c6acdf10f68f..f9b205c3b8bc 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -186,7 +186,7 @@ public function release() $this->assertTrue($job->released); $this->assertTrue($job->handled); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); } $result = $middleware->handle($job = $jobFactory(), $next); @@ -294,7 +294,7 @@ public function release() $this->assertTrue($job->released); $this->assertTrue($job->handled); - Carbon::setTestNow(Carbon::now()->addSeconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()); } $result = $middleware->handle($job = $jobFactory(), $next); diff --git a/tests/Queue/DynamoDbFailedJobProviderTest.php b/tests/Queue/DynamoDbFailedJobProviderTest.php index ec6d74cd5d8c..1c9a4b188893 100644 --- a/tests/Queue/DynamoDbFailedJobProviderTest.php +++ b/tests/Queue/DynamoDbFailedJobProviderTest.php @@ -38,7 +38,7 @@ public function testCanProperlyLogFailedJob() 'payload' => ['S' => json_encode(['uuid' => (string) $uuid])], 'exception' => ['S' => (string) $exception], 'failed_at' => ['N' => (string) $now->getTimestamp()], - 'expires_at' => ['N' => (string) $now->addDays(7)->getTimestamp()], + 'expires_at' => ['N' => (string) $now->addWeek()->getTimestamp()], ], ]); diff --git a/tests/Queue/FileFailedJobProviderTest.php b/tests/Queue/FileFailedJobProviderTest.php index 096cdc67beb8..9f133c15d9fb 100644 --- a/tests/Queue/FileFailedJobProviderTest.php +++ b/tests/Queue/FileFailedJobProviderTest.php @@ -128,14 +128,14 @@ public function testCanPruneFailedJobs() $this->logFailedJob(); $this->logFailedJob(); - $this->provider->prune(Carbon::now()->addDay(1)); + $this->provider->prune(Carbon::now()->addDay()); $failedJobs = $this->provider->all(); $this->assertEmpty($failedJobs); $this->logFailedJob(); $this->logFailedJob(); - $this->provider->prune(Carbon::now()->subDay(1)); + $this->provider->prune(Carbon::now()->subDay()); $failedJobs = $this->provider->all(); $this->assertCount(2, $failedJobs); } @@ -145,14 +145,14 @@ public function testCanPruneFailedJobsWithRelativeHours() $this->logFailedJob(); $this->logFailedJob(); - $this->provider->prune(Carbon::now()->addHour(1)); + $this->provider->prune(Carbon::now()->addHour()); $failedJobs = $this->provider->all(); $this->assertEmpty($failedJobs); $this->logFailedJob(); $this->logFailedJob(); - $this->provider->prune(Carbon::now()->subHour(1)); + $this->provider->prune(Carbon::now()->subHour()); $failedJobs = $this->provider->all(); $this->assertCount(2, $failedJobs); } diff --git a/tests/Queue/QueueDatabaseQueueIntegrationTest.php b/tests/Queue/QueueDatabaseQueueIntegrationTest.php index ed8b6229bdbd..eb764f81dab0 100644 --- a/tests/Queue/QueueDatabaseQueueIntegrationTest.php +++ b/tests/Queue/QueueDatabaseQueueIntegrationTest.php @@ -121,7 +121,7 @@ public function testAvailableAndUnReservedJobsArePopped() 'payload' => 'mock_payload', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->subSeconds(1)->getTimestamp(), + 'available_at' => Carbon::now()->subSecond()->getTimestamp(), 'created_at' => Carbon::now()->getTimestamp(), ]); @@ -141,7 +141,7 @@ public function testPoppedJobsIncrementAttempts() 'payload' => 'mock_payload', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->subSeconds(1)->getTimestamp(), + 'available_at' => Carbon::now()->subSecond()->getTimestamp(), 'created_at' => Carbon::now()->getTimestamp(), ]; @@ -176,7 +176,7 @@ public function testThatQueueCanBeCleared() 'payload' => 'mock_payload 2', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->subSeconds(1)->getTimestamp(), + 'available_at' => Carbon::now()->subSecond()->getTimestamp(), 'created_at' => Carbon::now()->getTimestamp(), ]]); @@ -197,7 +197,7 @@ public function testUnavailableJobsAreNotPopped() 'payload' => 'mock_payload', 'attempts' => 0, 'reserved_at' => null, - 'available_at' => Carbon::now()->addSeconds(60)->getTimestamp(), + 'available_at' => Carbon::now()->addMinute()->getTimestamp(), 'created_at' => Carbon::now()->getTimestamp(), ]); diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 71f7222149ef..326594e6186a 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -256,7 +256,7 @@ public function testReservedJobs() $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('where')->with('queue', 'default')->andReturnSelf(); $query->shouldReceive('whereNotNull')->with('reserved_at')->andReturnSelf(); - $query->shouldReceive('get')->andReturn(collect([(object) ['id' => 1, 'queue' => 'default', 'payload' => $payload, 'attempts' => 1, 'reserved_at' => now()->timestamp]])); + $query->shouldReceive('get')->andReturn(collect([(object) ['id' => 1, 'queue' => 'default', 'payload' => $payload, 'attempts' => 1, 'reserved_at' => Carbon::now()->getTimestamp()]])); $jobs = $queue->reservedJobs(); diff --git a/tests/Session/ArraySessionHandlerTest.php b/tests/Session/ArraySessionHandlerTest.php index 68767fa8fbd3..00f977340a07 100644 --- a/tests/Session/ArraySessionHandlerTest.php +++ b/tests/Session/ArraySessionHandlerTest.php @@ -11,7 +11,7 @@ class ArraySessionHandlerTest extends TestCase { protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index 024ab4c256fe..2f6388af9613 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -246,7 +246,7 @@ public function testItCanSleepTillGivenTimestamp() Sleep::fake(); Carbon::setTestNow(Carbon::now()->startOfDay()); - Sleep::until(Carbon::now()->addMinute()->timestamp); + Sleep::until(Carbon::now()->addMinute()->getTimestamp()); Sleep::assertSequence([ Sleep::for(60)->seconds(), @@ -258,7 +258,7 @@ public function testItCanSleepTillGivenTimestampAsString() Sleep::fake(); Carbon::setTestNow(Carbon::now()->startOfDay()); - Sleep::until((string) Carbon::now()->addMinute()->timestamp); + Sleep::until((string) Carbon::now()->addMinute()->getTimestamp()); Sleep::assertSequence([ Sleep::for(60)->seconds(), diff --git a/tests/Support/SupportCarbonTest.php b/tests/Support/SupportCarbonTest.php index df035124e4e8..942d783d252e 100644 --- a/tests/Support/SupportCarbonTest.php +++ b/tests/Support/SupportCarbonTest.php @@ -25,7 +25,7 @@ protected function setUp(): void protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); Carbon::serializeUsing(null); parent::tearDown(); @@ -120,8 +120,8 @@ public function testSetTestNowWillPersistBetweenImmutableAndMutableInstance() public function testCarbonIsConditionable() { - $this->assertTrue(Carbon::now()->when(null, fn (Carbon $carbon) => $carbon->addDays(1))->isToday()); - $this->assertTrue(Carbon::now()->when(true, fn (Carbon $carbon) => $carbon->addDays(1))->isTomorrow()); + $this->assertTrue(Carbon::now()->when(null, fn (Carbon $carbon) => $carbon->addDay())->isToday()); + $this->assertTrue(Carbon::now()->when(true, fn (Carbon $carbon) => $carbon->addDay())->isTomorrow()); } public function testCreateFromUid() diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a43e7a2a126e..41e0ab102b2d 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -44,7 +44,7 @@ class ValidationValidatorTest extends TestCase { protected function tearDown(): void { - Carbon::setTestNow(null); + Carbon::setTestNow(); parent::tearDown(); } From 89c441517fe2fc63154bd8e5c0e537d9edd23f3c Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:30:58 +0600 Subject: [PATCH 196/596] [13.x] Implement CanFlushLocks on FailoverStore (#59738) --- src/Illuminate/Cache/FailoverStore.php | 35 +++++++++++++- tests/Cache/CacheFailoverStoreTest.php | 64 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/Cache/CacheFailoverStoreTest.php diff --git a/src/Illuminate/Cache/FailoverStore.php b/src/Illuminate/Cache/FailoverStore.php index e516458673d4..2d2952c2ffb6 100644 --- a/src/Illuminate/Cache/FailoverStore.php +++ b/src/Illuminate/Cache/FailoverStore.php @@ -3,12 +3,13 @@ namespace Illuminate\Cache; use Illuminate\Cache\Events\CacheFailedOver; +use Illuminate\Contracts\Cache\CanFlushLocks; use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Events\Dispatcher; use RuntimeException; use Throwable; -class FailoverStore extends TaggableStore implements LockProvider +class FailoverStore extends TaggableStore implements CanFlushLocks, LockProvider { /** * The caches which failed on the last action. @@ -199,6 +200,38 @@ public function flushStaleTags() } } + /** + * Flush all of the stale locks from every backing store. + * + * @return bool + */ + public function flushLocks(): bool + { + $result = true; + + foreach ($this->stores as $store) { + $underlyingStore = $this->store($store)->getStore(); + + if ($underlyingStore instanceof CanFlushLocks) { + if (! $underlyingStore->flushLocks()) { + $result = false; + } + } + } + + return $result; + } + + /** + * Determine if the lock store is separate from the cache store. + * + * @return bool + */ + public function hasSeparateLockStore(): bool + { + return true; + } + /** * Get the cache key prefix. * diff --git a/tests/Cache/CacheFailoverStoreTest.php b/tests/Cache/CacheFailoverStoreTest.php new file mode 100644 index 000000000000..4f1b12f284f6 --- /dev/null +++ b/tests/Cache/CacheFailoverStoreTest.php @@ -0,0 +1,64 @@ +makeFailoverStore([]); + + $this->assertInstanceOf(CanFlushLocks::class, $store); + } + + public function testFlushLocksCallsFlushLocksOnAllBackingStores() + { + $storeA = new ArrayStore; + $storeB = new ArrayStore; + + $storeA->lock('lock-a', 60)->get(); + $storeB->lock('lock-b', 60)->get(); + + $cache = m::mock(CacheManager::class); + $cache->shouldReceive('store')->with('store-a')->andReturn(new Repository($storeA)); + $cache->shouldReceive('store')->with('store-b')->andReturn(new Repository($storeB)); + + $failover = new FailoverStore($cache, m::mock(Dispatcher::class), ['store-a', 'store-b']); + + $result = $failover->flushLocks(); + + $this->assertTrue($result); + $this->assertEmpty($storeA->locks); + $this->assertEmpty($storeB->locks); + } + + public function testFlushLocksReturnsTrueWhenNoStoreSupportsIt() + { + $store = $this->makeFailoverStore([]); + + $this->assertTrue($store->flushLocks()); + } + + protected function makeFailoverStore(array $stores): FailoverStore + { + return new FailoverStore( + m::mock(CacheManager::class), + m::mock(Dispatcher::class), + $stores + ); + } +} From e20a33055ab88383bd5238c4a46b4c8b05d4b75d Mon Sep 17 00:00:00 2001 From: Golovin Max <65284135+ma32kc@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:35:25 +0300 Subject: [PATCH 197/596] [13.x] Validate MAC across all decryption keys (#59742) * [13.x] Validate MAC across all decryption keys * Update Encrypter.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Encryption/Encrypter.php | 23 ++++++++++++++++------- tests/Encryption/EncrypterTest.php | 11 +++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 3771c5b1b179..339ab5a8eb02 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -162,16 +162,19 @@ public function decrypt($payload, $unserialize = true) $tag = empty($payload['tag']) ? null : base64_decode($payload['tag']) ); - $foundValidMac = false; + [$keys, $validKey] = [$this->getAllKeys(), null]; // Here we will decrypt the value. If we are able to successfully decrypt it // we will then unserialize it and return it out to the caller. If we are // unable to decrypt this value we will throw out an exception message. - foreach ($this->getAllKeys() as $key) { - if ( - $this->shouldValidateMac() && - ! ($foundValidMac = $foundValidMac || $this->validMacForKey($payload, $key)) - ) { + foreach ($keys as $key) { + if ($this->shouldValidateMac()) { + $validMac = $this->validMacForKey($payload, $key); + + if ($validMac && $validKey === null) { + $validKey = $key; + } + continue; } @@ -184,10 +187,16 @@ public function decrypt($payload, $unserialize = true) } } - if ($this->shouldValidateMac() && ! $foundValidMac) { + if ($this->shouldValidateMac() && $validKey === null) { throw new DecryptException('The MAC is invalid.'); } + if ($this->shouldValidateMac()) { + $decrypted = \openssl_decrypt( + $payload['value'], strtolower($this->cipher), $validKey, 0, $iv, $tag ?? '' + ); + } + if (($decrypted ?? false) === false) { throw new DecryptException('Could not decrypt the data.'); } diff --git a/tests/Encryption/EncrypterTest.php b/tests/Encryption/EncrypterTest.php index 78b6b538247b..55f502ae650e 100755 --- a/tests/Encryption/EncrypterTest.php +++ b/tests/Encryption/EncrypterTest.php @@ -62,6 +62,17 @@ public function testItValidatesMacOnPerKeyBasis() $this->assertSame('foo', $new->decryptString($encrypted)); } + public function testItDecryptsUsingTheFirstMacValidatedKey() + { + $previous = new Encrypter(str_repeat('b', 16)); + $encrypted = $previous->encryptString('foo'); + + $new = new Encrypter(str_repeat('a', 16)); + $new->previousKeys([str_repeat('b', 16), str_repeat('c', 16)]); + + $this->assertSame('foo', $new->decryptString($encrypted)); + } + public function testEncryptionUsingBase64EncodedKey() { $e = new Encrypter(random_bytes(16)); From 812551f9bf32d6c327c7ea2156e0fdeff52f3b1e Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:06:04 +0200 Subject: [PATCH 198/596] Use generic TModel in additional places in Factory class (#59780) --- src/Illuminate/Database/Eloquent/Factories/Factory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 4d2278d6af3a..b43e7d2d8329 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -366,7 +366,7 @@ public function lazy(array $attributes = [], ?Model $parent = null) /** * Set the connection name on the results and store them. * - * @param \Illuminate\Support\Collection $results + * @param \Illuminate\Support\Collection $results * @return void */ protected function store(Collection $results) @@ -391,7 +391,7 @@ protected function store(Collection $results) /** * Create the children for the given model. * - * @param \Illuminate\Database\Eloquent\Model $model + * @param TModel $model * @return void */ protected function createChildren(Model $model) @@ -514,7 +514,7 @@ public function insert(array $attributes = [], ?Model $parent = null): void * Make an instance of the model with the given attributes. * * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Model + * @return TModel */ protected function makeInstance(?Model $parent) { From f8eaedfa41ed2548511f11e172563f913de7370d Mon Sep 17 00:00:00 2001 From: Alies Lapatsin <5278175+alies-dev@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:07:32 +0100 Subject: [PATCH 199/596] [12.x] Prevent array to string conversion in signature validation (#59778) * [12.x] Prevent array to string conversion in signature validation When a request reaches a signed-URL route with array-style query parameters (e.g. ?signature[]=foo&signature[]=bar), $request->query('signature', '') returns an array. The (string) cast on that array raises an 'Array to string conversion' warning, which surfaces as a critical-level error in error trackers. This validates the signature is a string before entering the verification loop, returning false early for any non-string value (an array signature can never validate anyway). * Add regression test for array signature query parameter --- src/Illuminate/Routing/UrlGenerator.php | 8 +++++- tests/Routing/RoutingUrlGeneratorTest.php | 30 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Routing/UrlGenerator.php b/src/Illuminate/Routing/UrlGenerator.php index 1402f3a08858..5e23a5d1ac01 100755 --- a/src/Illuminate/Routing/UrlGenerator.php +++ b/src/Illuminate/Routing/UrlGenerator.php @@ -475,10 +475,16 @@ public function hasCorrectSignature(Request $request, $absolute = true, Closure| $keys = is_array($keys) ? $keys : [$keys]; + $signature = $request->query('signature'); + + if (! is_string($signature)) { + return false; + } + foreach ($keys as $key) { if (hash_equals( hash_hmac('sha256', $original, $key), - (string) $request->query('signature', '') + $signature )) { return true; } diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index 54eab01a1cc8..768f37fd94b3 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -776,6 +776,36 @@ public function testSignedUrl() $this->assertTrue($url->hasValidSignature($request, ignoreQuery: fn ($parameter) => $parameter === 'tampered')); } + public function testSignedUrlWithArraySignatureReturnsFalseWithoutWarning() + { + $url = new UrlGenerator( + $routes = new RouteCollection, + Request::create('http://www.foo.com/') + ); + $url->setKeyResolver(function () { + return 'secret'; + }); + + $route = new Route(['GET'], 'foo', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + // ?signature[]=foo&signature[]=bar previously raised an + // "Array to string conversion" warning. + $request = Request::create('http://www.foo.com/foo?signature[]=foo&signature[]=bar'); + + set_error_handler(static function (int $errno, string $errstr) { + throw new \ErrorException($errstr, 0, $errno); + }, E_WARNING); + + try { + $this->assertFalse($url->hasValidSignature($request)); + } finally { + restore_error_handler(); + } + } + public function testSignedUrlImplicitModelBinding() { $url = new UrlGenerator( From 1f42653a84791ed7c5ccf622c730260da4125d70 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 20 Apr 2026 14:14:04 +0100 Subject: [PATCH 200/596] [13.x] Ensure assertModelMissing and assertModelExists dont silently pass (#59772) * Update InteractsWithDatabase.php * tests * dont count just do straight comparisonnnn --- .../Concerns/InteractsWithDatabase.php | 4 +-- .../FoundationInteractsWithDatabaseTest.php | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 115589b17936..237c53e429a3 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -33,7 +33,7 @@ protected function assertDatabaseHas($table, array $data = [], $connection = nul return $this; } - if (array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { + if ($data !== [] && array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { foreach ($data as $row) { $this->assertDatabaseHas($table, $row, $connection); } @@ -73,7 +73,7 @@ protected function assertDatabaseMissing($table, array $data = [], $connection = return $this; } - if (array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { + if ($data !== [] && array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { foreach ($data as $row) { $this->assertDatabaseMissing($table, $row, $connection); } diff --git a/tests/Foundation/FoundationInteractsWithDatabaseTest.php b/tests/Foundation/FoundationInteractsWithDatabaseTest.php index 9dc92067debf..b4f5a90a9924 100644 --- a/tests/Foundation/FoundationInteractsWithDatabaseTest.php +++ b/tests/Foundation/FoundationInteractsWithDatabaseTest.php @@ -234,6 +234,32 @@ public function testAssertModelMissingPassesWhenDoesNotFindModelResults() $this->assertModelMissing(new ProductStub($this->data)); } + public function testAssertModelMissingFailsWhenFindsModelResults() + { + $this->expectException(ExpectationFailedException::class); + + $this->data = ['id' => 1]; + + $builder = $this->mockCountBuilder(true); + + $builder->shouldReceive('get')->andReturn(collect([$this->data])); + + $this->assertModelMissing(new ProductStub($this->data)); + } + + public function testAssertModelExistsFailsWhenDoesNotFindModelResults() + { + $this->expectException(ExpectationFailedException::class); + + $this->data = ['id' => 1]; + + $builder = $this->mockCountBuilder(false); + + $builder->shouldReceive('get')->andReturn(collect()); + + $this->assertModelExists(new ProductStub($this->data)); + } + public function testAssertSoftDeletedInDatabaseFindsResults() { $this->mockCountBuilder(true); From 813e3921d35d94e01de54f84bc2e56aa1f419808 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:42:28 -0400 Subject: [PATCH 201/596] [13.x] Introduce `JsonFormatter` (#59756) * wip * opus comments * clean up * this is better * clean up * whoops * tests * clean up * oh opus, you write such sloppy tests * clean up tests * simplify the test * style * don't include the ExceptionHandler's `context()` * rename method * renaming * clean up test * fine * Update JsonFormatter.php * formatting --------- Co-authored-by: Taylor Otwell --- .../Foundation/Exceptions/Handler.php | 43 ++- .../Log/Formatters/JsonFormatter.php | 45 +++ tests/Log/JsonFormatterTest.php | 331 ++++++++++++++++++ 3 files changed, 414 insertions(+), 5 deletions(-) create mode 100644 src/Illuminate/Log/Formatters/JsonFormatter.php create mode 100644 tests/Log/JsonFormatterTest.php diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 6853ad076f5c..811dc10bedc2 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -107,6 +107,13 @@ class Handler implements ExceptionHandlerContract */ protected $contextCallbacks = []; + /** + * The exception currently being reported. + * + * @var \Throwable|null + */ + protected ?Throwable $currentlyReporting = null; + /** * The callbacks that should be used during rendering. * @@ -398,11 +405,27 @@ protected function reportThrowable(Throwable $e): void $level = $this->mapLogLevel($e); - $context = $this->buildExceptionContext($e); + $originallyReporting = $this->currentlyReporting; + + $this->currentlyReporting = $e; - method_exists($logger, $level) - ? $logger->{$level}($e->getMessage(), $context) - : $logger->log($level, $e->getMessage(), $context); + try { + $context = $this->buildExceptionContext($e); + + method_exists($logger, $level) + ? $logger->{$level}($e->getMessage(), $context) + : $logger->log($level, $e->getMessage(), $context); + } finally { + $this->currentlyReporting = $originallyReporting; + } + } + + /** + * Determine if a given exception is being reported. + */ + public function isReporting(Throwable $e): bool + { + return $this->currentlyReporting === $e; } /** @@ -534,12 +557,22 @@ public function stopIgnoring(array|string $exceptions) protected function buildExceptionContext(Throwable $e) { return array_merge( - $this->exceptionContext($e), + $this->buildContextForException($e), $this->context(), ['exception' => $e] ); } + /** + * Creates the context for an exception. + * + * @return array + */ + public function buildContextForException(Throwable $e) + { + return $this->exceptionContext($e); + } + /** * Get the default exception context variables for logging. * diff --git a/src/Illuminate/Log/Formatters/JsonFormatter.php b/src/Illuminate/Log/Formatters/JsonFormatter.php new file mode 100644 index 000000000000..36f5004a01e6 --- /dev/null +++ b/src/Illuminate/Log/Formatters/JsonFormatter.php @@ -0,0 +1,45 @@ +make(ExceptionHandler::class); + } catch (Throwable) { + return $response; + } + + if ((! method_exists($handler, 'isReporting')) || ! $handler->isReporting($e)) { + if (method_exists($handler, 'buildContextForException') + && (is_array($normalizedHandlerExceptionContext = $this->normalize($handler->buildContextForException($e), $depth + 1))) + ) { + $response = array_merge( + $normalizedHandlerExceptionContext, + $response + ); + } elseif (method_exists($e, 'context')) { + $exceptionContext = $this->normalize($e->context(), $depth + 1); + + if (is_array($exceptionContext)) { + $response = array_merge( + $exceptionContext, + $response + ); + } + } + } + + return $response; + } +} diff --git a/tests/Log/JsonFormatterTest.php b/tests/Log/JsonFormatterTest.php new file mode 100644 index 000000000000..266fb2010d05 --- /dev/null +++ b/tests/Log/JsonFormatterTest.php @@ -0,0 +1,331 @@ + 'testing']); + config(['logging.channels' => [ + 'testing' => [ + 'driver' => 'monolog', + 'handler' => TestHandler::class, + 'formatter' => JsonFormatter::class, + ], + ]]); + } + + public function testExceptionContextIsEnrichedOnDirectLogging() + { + Log::error('fail', ['exception' => new ContextProvidingException('Something went wrong')]); + + $formatted = $this->getFormattedJson(); + + $exceptionData = $formatted['context']['exception']; + self::assertSame('bar', $exceptionData['foo']); + self::assertSame(ContextProvidingException::class, $exceptionData['class']); + } + + public function testExceptionContextIsNotDuplicatedWhenGoingThroughReport() + { + $exception = new ContextProvidingException('Something went wrong'); + + $this->app->make(ExceptionHandlerContract::class)->report($exception); + + $formatted = $this->getFormattedJson(); + + // Context should be at the top level (from the handler) + self::assertSame('bar', $formatted['context']['foo']); + + // But NOT enriched inside the normalized exception (formatter should skip) + $exceptionData = $formatted['context']['exception']; + self::assertArrayNotHasKey('foo', $exceptionData); + } + + public function testStackDriverEnrichesBothHandlersOnDirectLogging() + { + $handlerA = new TestHandler(); + $handlerB = new TestHandler(); + + $monolog = new Monolog('test', [$handlerA, $handlerB]); + foreach ([$handlerA, $handlerB] as $h) { + $h->setFormatter(new JsonFormatter()); + } + + $exception = new ContextProvidingException('Stack test'); + + $monolog->error('fail', ['exception' => $exception]); + + foreach (['handlerA' => $handlerA, 'handlerB' => $handlerB] as $name => $h) { + $formatted = $this->getFormattedJson($h); + $exceptionData = $formatted['context']['exception']; + + self::assertSame('bar', $exceptionData['foo'], "Expected enriched context on {$name}"); + self::assertSame(ContextProvidingException::class, $exceptionData['class']); + } + } + + public function testStackDriverSkipsEnrichmentOnBothHandlersWhenReporting() + { + $handlerA = new TestHandler(); + $handlerB = new TestHandler(); + + $monolog = new Monolog('test', [$handlerA, $handlerB]); + foreach ([$handlerA, $handlerB] as $h) { + $h->setFormatter(new JsonFormatter()); + } + + $this->app->instance(LoggerInterface::class, new Logger($monolog)); + + $exceptionHandler = new Handler($this->app); + $this->app->instance(ExceptionHandlerContract::class, $exceptionHandler); + + $exception = new ContextProvidingException('Stack report test'); + + $exceptionHandler->report($exception); + + foreach (['handlerA' => $handlerA, 'handlerB' => $handlerB] as $name => $h) { + $formatted = $this->getFormattedJson($h); + + self::assertSame('bar', $formatted['context']['foo'], "Context should be at top level on {$name}"); + + $exceptionData = $formatted['context']['exception']; + self::assertArrayNotHasKey('foo', $exceptionData, "Formatter should not enrich on {$name}"); + } + } + + public function testPreviousExceptionContextIsAlsoEnriched() + { + $previous = new ContextProvidingException('Root cause'); + $outer = new RuntimeException('Wrapper', 0, $previous); + + Log::error('fail', ['exception' => $outer]); + + $formatted = $this->getFormattedJson(); + $exceptionData = $formatted['context']['exception']; + + self::assertSame(RuntimeException::class, $exceptionData['class']); + self::assertArrayHasKey('previous', $exceptionData); + + $previousData = $exceptionData['previous']; + self::assertSame(ContextProvidingException::class, $previousData['class']); + self::assertSame('bar', $previousData['foo']); + } + + public function testReportEnrichesPreviousExceptionContext() + { + $exception = new RuntimeException('Wrapper', 0, new ContextProvidingException('Root cause')); + + $this->app->make(ExceptionHandlerContract::class)->report($exception); + + $formatted = $this->getFormattedJson(); + + // The outer exception has no context() method, so nothing at the top level + self::assertArrayNotHasKey('foo', $formatted['context']); + + $exceptionData = $formatted['context']['exception']; + + // Outer exception should NOT be enriched (isReporting matches it) + self::assertArrayNotHasKey('foo', $exceptionData); + + // Previous exception SHOULD be enriched (isReporting does not match it) + self::assertArrayHasKey('previous', $exceptionData); + $previousData = $exceptionData['previous']; + self::assertSame(ContextProvidingException::class, $previousData['class']); + self::assertSame('bar', $previousData['foo']); + } + + public function testExceptionWithoutContextMethodIsNotEnriched() + { + Log::error('fail', ['exception' => new RuntimeException('Plain exception')]); + + $formatted = $this->getFormattedJson(); + $exceptionData = $formatted['context']['exception']; + + self::assertSame(RuntimeException::class, $exceptionData['class']); + self::assertSame('Plain exception', $exceptionData['message']); + self::assertArrayNotHasKey('foo', $exceptionData); + } + + public function testContextCallbacksAreIncludedInFormatterEnrichment() + { + $this->app->make(ExceptionHandlerContract::class)->buildContextUsing(function (Throwable $e) { + return ['callback_key' => 'callback_value']; + }); + + $exception = new ContextProvidingException('With callbacks'); + + Log::error('fail', ['exception' => $exception]); + + $formatted = $this->getFormattedJson(); + $exceptionData = $formatted['context']['exception']; + + self::assertSame('bar', $exceptionData['foo']); + self::assertSame('callback_value', $exceptionData['callback_key']); + } + + public function testGracefulFallbackWhenContainerCannotResolveHandler() + { + Container::setInstance(new Container()); + + $handler = new TestHandler(); + $monolog = new Monolog('test', [$handler]); + $handler->setFormatter(new JsonFormatter()); + + $exception = new ContextProvidingException('No handler bound'); + + $monolog->error('fail', ['exception' => $exception]); + + $formatted = $this->getFormattedJson($handler); + $exceptionData = $formatted['context']['exception']; + + self::assertSame(ContextProvidingException::class, $exceptionData['class']); + self::assertSame('No handler bound', $exceptionData['message']); + self::assertArrayNotHasKey('foo', $exceptionData); + } + + public function testNonScalarContextValuesAreNormalized() + { + $exception = new ObjectContextException('Has objects in context'); + + Log::error('fail', ['exception' => $exception]); + + $formatted = $this->getFormattedJson(); + $exceptionData = $formatted['context']['exception']; + + self::assertIsArray($exceptionData['nested']); + self::assertSame(ObjectContextException::class, $exceptionData['class']); + } + + public function testBothOuterAndPreviousContextEnrichedOnDirectLogging() + { + $previous = new ContextProvidingException('Root cause'); + $outer = new AnotherContextProvidingException('Wrapper', 0, $previous); + + Log::error('fail', ['exception' => $outer]); + + $formatted = $this->getFormattedJson(); + $exceptionData = $formatted['context']['exception']; + + // Outer exception should have its own context + self::assertSame('outer_value', $exceptionData['outer_key']); + self::assertSame(AnotherContextProvidingException::class, $exceptionData['class']); + + // Previous exception should have its own context + self::assertArrayHasKey('previous', $exceptionData); + $previousData = $exceptionData['previous']; + self::assertSame('bar', $previousData['foo']); + self::assertSame(ContextProvidingException::class, $previousData['class']); + + // Context keys should not bleed between exceptions + self::assertArrayNotHasKey('foo', $exceptionData); + self::assertArrayNotHasKey('outer_key', $previousData); + } + + public function testBothOuterAndPreviousContextOnReport() + { + $previous = new ContextProvidingException('Root cause'); + $outer = new AnotherContextProvidingException('Wrapper', 0, $previous); + + $this->app->make(ExceptionHandlerContract::class)->report($outer); + + $formatted = $this->getFormattedJson(); + + // Outer's context should be at the top level (from the handler) + self::assertSame('outer_value', $formatted['context']['outer_key']); + + $exceptionData = $formatted['context']['exception']; + + // Outer should NOT be enriched by the formatter (isReporting matches) + self::assertArrayNotHasKey('outer_key', $exceptionData); + + // Previous SHOULD be enriched by the formatter (isReporting does not match) + self::assertArrayHasKey('previous', $exceptionData); + $previousData = $exceptionData['previous']; + self::assertSame('bar', $previousData['foo']); + self::assertSame(ContextProvidingException::class, $previousData['class']); + } + + public function testFormatterHandlesNormalizationDepthLimit() + { + $formatter = new JsonFormatter(); + $formatter->setMaxNormalizeDepth(3); + + $handler = new TestHandler(); + $handler->setFormatter($formatter); + $monolog = new Monolog('test', [$handler]); + + $inner = new ContextProvidingException('inner'); + $outer = new ContextProvidingException('outer', 0, $inner); + + $monolog->error('fail', ['exception' => $outer]); + + $formatted = $this->getFormattedJson($handler); + $exceptionData = $formatted['context']['exception']; + + // Outermost exception at depth 2 — context normalize called at depth 3, + // within limit so the array is returned (values inside may be depth-truncated) + self::assertArrayHasKey('foo', $exceptionData); + + // Previous exception at depth 3 — context normalize called at depth 4, + // exceeds limit so normalize returns a string. is_array() guard skips enrichment. + self::assertArrayHasKey('previous', $exceptionData); + self::assertArrayNotHasKey('foo', $exceptionData['previous']); + self::assertSame(ContextProvidingException::class, $exceptionData['previous']['class']); + } + + private function getFormattedJson(?TestHandler $handler = null): array + { + $handler ??= $this->app->make('log')->driver()->getLogger()->getHandlers()[0]; + $records = $handler->getRecords(); + self::assertNotEmpty($records, 'Expected at least one log record'); + + $formatted = $records[0]['formatted']; + + return json_decode($formatted, true, 512, JSON_THROW_ON_ERROR); + } +} + +class ContextProvidingException extends Exception +{ + public function context(): array + { + return ['foo' => 'bar']; + } +} + +class AnotherContextProvidingException extends Exception +{ + public function context(): array + { + return ['outer_key' => 'outer_value']; + } +} + +class ObjectContextException extends Exception +{ + public function context(): array + { + return [ + 'nested' => new \stdClass(), + ]; + } +} From 056555b9acbb9f665ea543f257afe04b24e6974c Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:42:58 +0000 Subject: [PATCH 202/596] Update facade docblocks --- src/Illuminate/Support/Facades/Exceptions.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Support/Facades/Exceptions.php b/src/Illuminate/Support/Facades/Exceptions.php index 263b95bd0418..a12e74e4394d 100644 --- a/src/Illuminate/Support/Facades/Exceptions.php +++ b/src/Illuminate/Support/Facades/Exceptions.php @@ -17,9 +17,11 @@ * @method static \Illuminate\Foundation\Exceptions\Handler dontFlash(array|string $attributes) * @method static \Illuminate\Foundation\Exceptions\Handler level(string $type, string $level) * @method static void report(\Throwable $e) + * @method static bool isReporting(\Throwable $e) * @method static bool shouldReport(\Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler throttleUsing(callable $throttleUsing) * @method static \Illuminate\Foundation\Exceptions\Handler stopIgnoring(array|string $exceptions) + * @method static array buildContextForException(\Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler buildContextUsing(\Closure $contextCallback) * @method static \Symfony\Component\HttpFoundation\Response render(\Illuminate\Http\Request $request, \Throwable $e) * @method static \Illuminate\Foundation\Exceptions\Handler respondUsing(callable $callback) From 1684284e499b5dde1d81c4f4cfedc7610ad89108 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 20 Apr 2026 13:43:30 +0000 Subject: [PATCH 203/596] Apply fixes from StyleCI --- src/Illuminate/Log/Formatters/JsonFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Log/Formatters/JsonFormatter.php b/src/Illuminate/Log/Formatters/JsonFormatter.php index 36f5004a01e6..d25ce7cded43 100644 --- a/src/Illuminate/Log/Formatters/JsonFormatter.php +++ b/src/Illuminate/Log/Formatters/JsonFormatter.php @@ -22,7 +22,7 @@ protected function normalizeException(Throwable $e, int $depth = 0): array if ((! method_exists($handler, 'isReporting')) || ! $handler->isReporting($e)) { if (method_exists($handler, 'buildContextForException') - && (is_array($normalizedHandlerExceptionContext = $this->normalize($handler->buildContextForException($e), $depth + 1))) + && is_array($normalizedHandlerExceptionContext = $this->normalize($handler->buildContextForException($e), $depth + 1)) ) { $response = array_merge( $normalizedHandlerExceptionContext, From e2ffb2c4afe3b770a6c4ad2c620bfae04fc724b5 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Mon, 20 Apr 2026 15:47:53 +0100 Subject: [PATCH 204/596] [13.x] Add prefersJsonResponses() to the application builder (#59753) * Add prefersJsonResponses() to the application builder * Apply fixes from StyleCI * Drop deprecated setAccessible() call in builder test * formatting --------- Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Configuration/ApplicationBuilder.php | 20 ++ .../Http/Middleware/PrefersJsonResponses.php | 65 ++++++ .../FoundationApplicationBuilderTest.php | 57 +++++ .../Middleware/PrefersJsonResponsesTest.php | 210 ++++++++++++++++++ .../Auth/Middleware/RequirePasswordTest.php | 45 ++++ .../Configuration/PrefersJsonDisabledTest.php | 38 ++++ .../Configuration/PrefersJsonTest.php | 147 ++++++++++++ .../Foundation/ExceptionHandlerTest.php | 27 +++ 8 files changed, 609 insertions(+) create mode 100644 src/Illuminate/Http/Middleware/PrefersJsonResponses.php create mode 100644 tests/Http/Middleware/PrefersJsonResponsesTest.php create mode 100644 tests/Integration/Foundation/Configuration/PrefersJsonDisabledTest.php create mode 100644 tests/Integration/Foundation/Configuration/PrefersJsonTest.php diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index b67b52f72878..b7e189eb95ca 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -13,6 +13,7 @@ use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; use Illuminate\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider; +use Illuminate\Http\Middleware\PrefersJsonResponses; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Broadcast; @@ -459,6 +460,25 @@ public function withScopedSingletons(array $scopedSingletons) }); } + /** + * Globally prefer JSON responses when the incoming "Accept" header is broad. + * + * @param bool $prefer + * @return $this + */ + public function prefersJsonResponses(bool $prefer = true) + { + if (! $prefer) { + return $this; + } + + $this->app->booted(function () { + $this->app->make(HttpKernel::class)->prependMiddleware(PrefersJsonResponses::class); + }); + + return $this; + } + /** * Register a callback to be invoked when the application's service providers are registered. * diff --git a/src/Illuminate/Http/Middleware/PrefersJsonResponses.php b/src/Illuminate/Http/Middleware/PrefersJsonResponses.php new file mode 100644 index 000000000000..2a930d7fa890 --- /dev/null +++ b/src/Illuminate/Http/Middleware/PrefersJsonResponses.php @@ -0,0 +1,65 @@ +headers->get('Accept'); + + if ($this->acceptHeaderIsBroad($accept)) { + if ($accept !== null) { + $request->headers->set('X-Original-Accept', $accept); + } + + $request->headers->set('Accept', 'application/json'); + } + + return $next($request); + } + + /** + * Determine if the given "Accept" header value is broad enough to be treated as JSON. + * + * The header is broad when it's missing or every media-type listed is wildcard ("*\/*" or "application/*"). + * + * @param string|null $accept + * @return bool + */ + protected function acceptHeaderIsBroad($accept) + { + if ($accept === null || trim($accept) === '') { + return true; + } + + foreach (explode(',', $accept) as $value) { + $value = strtolower(trim($value)); + + if ($value === '') { + continue; + } + + $pos = strpos($value, ';'); + + if ($pos !== false) { + $value = trim(substr($value, 0, $pos)); + } + + if (! in_array($value, ['*/*', 'application/*'], true)) { + return false; + } + } + + return true; + } +} diff --git a/tests/Foundation/FoundationApplicationBuilderTest.php b/tests/Foundation/FoundationApplicationBuilderTest.php index 5ae2c5886b60..ed210b38e825 100644 --- a/tests/Foundation/FoundationApplicationBuilderTest.php +++ b/tests/Foundation/FoundationApplicationBuilderTest.php @@ -2,7 +2,10 @@ namespace Illuminate\Tests\Foundation; +use Illuminate\Contracts\Http\Kernel as HttpKernelContract; use Illuminate\Foundation\Application; +use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Illuminate\Http\Middleware\PrefersJsonResponses; use PHPUnit\Framework\TestCase; class FoundationApplicationBuilderTest extends TestCase @@ -82,4 +85,58 @@ public function testStoragePathCanBeCustomized() $this->assertSame(__DIR__.'/custom-storage', $app->storagePath()); } + + public function testPrefersJsonResponsesIsFluent() + { + $builder = Application::configure(); + + $this->assertSame($builder, $builder->prefersJsonResponses()); + $this->assertSame($builder, $builder->prefersJsonResponses(false)); + } + + public function testPrefersJsonResponsesRegistersMiddlewareWhenEnabled() + { + $app = Application::configure()->prefersJsonResponses()->create(); + + $this->assertTrue($this->bootAndResolveKernel($app)->hasMiddleware(PrefersJsonResponses::class)); + } + + public function testPrefersJsonResponsesDefaultsToDisabled() + { + $app = Application::configure()->create(); + + $this->assertFalse($this->bootAndResolveKernel($app)->hasMiddleware(PrefersJsonResponses::class)); + } + + public function testPrefersJsonResponsesIsIdempotentWhenCalledMultipleTimes() + { + $app = Application::configure()->prefersJsonResponses()->prefersJsonResponses()->create(); + + $this->assertTrue($this->bootAndResolveKernel($app)->hasMiddleware(PrefersJsonResponses::class)); + } + + public function testPrefersJsonResponsesFalseDoesNotRegisterMiddleware() + { + $app = Application::configure()->prefersJsonResponses(false)->create(); + + $this->assertFalse($this->bootAndResolveKernel($app)->hasMiddleware(PrefersJsonResponses::class)); + } + + protected function bootAndResolveKernel(Application $app): HttpKernel + { + $app->singleton(HttpKernelContract::class, HttpKernel::class); + + // The builder registers its wiring inside $app->booted() callbacks. + // We can't call $app->boot() from a unit test — it runs the full + // provider chain which expects a real application — so invoke the + // booted callbacks directly. Real boot behavior is covered by the + // PrefersJson integration tests. + $property = (new \ReflectionClass(Application::class))->getProperty('bootedCallbacks'); + + foreach ($property->getValue($app) as $callback) { + $callback($app); + } + + return $app->make(HttpKernelContract::class); + } } diff --git a/tests/Http/Middleware/PrefersJsonResponsesTest.php b/tests/Http/Middleware/PrefersJsonResponsesTest.php new file mode 100644 index 000000000000..0cd948cbe579 --- /dev/null +++ b/tests/Http/Middleware/PrefersJsonResponsesTest.php @@ -0,0 +1,210 @@ +headers->remove('Accept'); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + $this->assertFalse($request->headers->has('X-Original-Accept')); + $this->assertTrue($request->wantsJson()); + $this->assertTrue($request->expectsJson()); + } + + public function testItRewritesEmptyAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => '']); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + } + + public function testItRewritesStarSlashStarAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => '*/*']); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + $this->assertSame('*/*', $request->headers->get('X-Original-Accept')); + $this->assertTrue($request->wantsJson()); + } + + public function testItDoesNotRewriteBareStarAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => '*']); + + $this->runMiddleware($request); + + $this->assertSame('*', $request->headers->get('Accept')); + $this->assertFalse($request->headers->has('X-Original-Accept')); + } + + public function testItRewritesApplicationWildcardAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/*']); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + } + + public function testItRewritesBroadAcceptHeaderWithQualityParameter() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => '*/*;q=0.8']); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + } + + public function testItRewritesMultipleBroadMarkers() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/*, */*;q=0.5']); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + $this->assertSame('application/*, */*;q=0.5', $request->headers->get('X-Original-Accept')); + } + + public function testItInvokesNextAndReturnsItsResponse() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => '*/*']); + + $called = false; + $expected = new Response('ok'); + + $result = (new PrefersJsonResponses)->handle($request, function ($passed) use ($request, &$called, $expected) { + $called = true; + $this->assertSame($request, $passed); + + return $expected; + }); + + $this->assertTrue($called); + $this->assertSame($expected, $result); + } + + public function testItLeavesExplicitHtmlAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'text/html']); + + $this->runMiddleware($request); + + $this->assertSame('text/html', $request->headers->get('Accept')); + $this->assertFalse($request->wantsJson()); + } + + public function testItLeavesExplicitXmlAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/xml']); + + $this->runMiddleware($request); + + $this->assertSame('application/xml', $request->headers->get('Accept')); + } + + public function testItLeavesExplicitPlainTextAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'text/plain']); + + $this->runMiddleware($request); + + $this->assertSame('text/plain', $request->headers->get('Accept')); + } + + public function testItLeavesMultiValueExplicitAcceptList() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'text/html, text/plain']); + + $this->runMiddleware($request); + + $this->assertSame('text/html, text/plain', $request->headers->get('Accept')); + $this->assertFalse($request->wantsJson()); + } + + public function testItLeavesMixedBroadAndExplicitAcceptList() + { + $request = Request::create('/', 'GET', [], [], [], [ + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + ]); + + $this->runMiddleware($request); + + $this->assertSame( + 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + $request->headers->get('Accept') + ); + $this->assertFalse($request->headers->has('X-Original-Accept')); + } + + public function testItLeavesWildcardFirstMixedAcceptList() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => '*/*, text/html']); + + $this->runMiddleware($request); + + $this->assertSame('*/*, text/html', $request->headers->get('Accept')); + } + + public function testItLeavesExplicitJsonAcceptHeader() + { + $request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/json']); + + $this->runMiddleware($request); + + $this->assertSame('application/json', $request->headers->get('Accept')); + $this->assertTrue($request->wantsJson()); + } + + public function testItDoesNotMutateBodyMethodUriQueryOrOtherHeaders() + { + $request = Request::create( + '/resource?page=2', + 'POST', + ['foo' => 'bar'], + [], + [], + [ + 'HTTP_ACCEPT' => '*/*', + 'HTTP_X_CUSTOM' => 'value', + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', + ], + 'raw-body' + ); + + $originalMethod = $request->getMethod(); + $originalPath = $request->getPathInfo(); + $originalQuery = $request->query->all(); + $originalBody = $request->getContent(); + $originalCustomHeader = $request->headers->get('X-Custom'); + + $this->runMiddleware($request); + + $this->assertSame($originalMethod, $request->getMethod()); + $this->assertSame($originalPath, $request->getPathInfo()); + $this->assertSame($originalQuery, $request->query->all()); + $this->assertSame($originalBody, $request->getContent()); + $this->assertSame($originalCustomHeader, $request->headers->get('X-Custom')); + $this->assertSame('application/json', $request->headers->get('Accept')); + $this->assertSame('*/*', $request->headers->get('X-Original-Accept')); + } + + protected function runMiddleware(Request $request): void + { + (new PrefersJsonResponses)->handle($request, fn ($request) => new Response); + } +} diff --git a/tests/Integration/Auth/Middleware/RequirePasswordTest.php b/tests/Integration/Auth/Middleware/RequirePasswordTest.php index 0247a63234c7..8f49406ac799 100644 --- a/tests/Integration/Auth/Middleware/RequirePasswordTest.php +++ b/tests/Integration/Auth/Middleware/RequirePasswordTest.php @@ -4,8 +4,10 @@ use Illuminate\Auth\Middleware\RequirePassword; use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Http\Kernel as HttpKernel; use Illuminate\Contracts\Routing\Registrar; use Illuminate\Contracts\Routing\UrlGenerator; +use Illuminate\Http\Middleware\PrefersJsonResponses; use Illuminate\Http\Response; use Illuminate\Session\Middleware\StartSession; use Orchestra\Testbench\TestCase; @@ -83,6 +85,49 @@ public function testUserIsRedirectedToACustomRouteIfThePasswordWasNotRecentlyCon $response->assertRedirect($this->app->make(UrlGenerator::class)->route('my-password.confirm')); } + public function testPrefersJsonReturnsJsonResponseForWildcardAcceptInsteadOfRedirecting() + { + $this->withoutExceptionHandling(); + + $this->app->make(HttpKernel::class)->prependMiddleware(PrefersJsonResponses::class); + + /** @var \Illuminate\Contracts\Routing\Registrar $router */ + $router = $this->app->make(Registrar::class); + + $router->get('test-route', function (): Response { + return new Response('foobar'); + })->middleware([StartSession::class, RequirePassword::class]); + + $response = $this->withSession(['auth.password_confirmed_at' => time() - 10801]) + ->get('test-route', ['Accept' => '*/*']); + + $response->assertStatus(423); + $response->assertJson(['message' => 'Password confirmation required.']); + } + + public function testPrefersJsonStillRedirectsWhenAcceptIsExplicitHtml() + { + $this->withoutExceptionHandling(); + + $this->app->make(HttpKernel::class)->prependMiddleware(PrefersJsonResponses::class); + + /** @var \Illuminate\Contracts\Routing\Registrar $router */ + $router = $this->app->make(Registrar::class); + + $router->get('password-confirm', function (): Response { + return new Response('foo'); + })->name('password.confirm'); + + $router->get('test-route', function (): Response { + return new Response('foobar'); + })->middleware([StartSession::class, RequirePassword::class]); + + $response = $this->withSession(['auth.password_confirmed_at' => time() - 10801]) + ->get('test-route', ['Accept' => 'text/html']); + + $response->assertRedirect($this->app->make(UrlGenerator::class)->route('password.confirm')); + } + public function testAuthPasswordTimeoutIsConfigurable() { $this->withoutExceptionHandling(); diff --git a/tests/Integration/Foundation/Configuration/PrefersJsonDisabledTest.php b/tests/Integration/Foundation/Configuration/PrefersJsonDisabledTest.php new file mode 100644 index 000000000000..2daa7b86470b --- /dev/null +++ b/tests/Integration/Foundation/Configuration/PrefersJsonDisabledTest.php @@ -0,0 +1,38 @@ +withMiddleware() + ->create(); + } + + public function testPlainStringRouteReturnsHtmlUnderWildcardAcceptWhenDisabled() + { + Route::get('plain', fn () => 'hello'); + + $this->get('plain', ['Accept' => '*/*']) + ->assertOk() + ->assertSee('hello') + ->assertHeader('Content-Type', 'text/html; charset=UTF-8'); + } + + public function testUnauthenticatedWildcardStillRedirectsWhenDisabled() + { + Route::get('login', fn () => 'login page')->name('login'); + + Route::get('protected', fn () => 'secret')->middleware(Authenticate::class); + + $this->get('protected', ['Accept' => '*/*']) + ->assertRedirect(); + } +} diff --git a/tests/Integration/Foundation/Configuration/PrefersJsonTest.php b/tests/Integration/Foundation/Configuration/PrefersJsonTest.php new file mode 100644 index 000000000000..c55f860afb31 --- /dev/null +++ b/tests/Integration/Foundation/Configuration/PrefersJsonTest.php @@ -0,0 +1,147 @@ +prefersJsonResponses() + ->create(); + } + + public function testArrayRouteReturnsJsonUnderWildcardAccept() + { + Route::get('payload', fn () => ['message' => 'hello']); + + $this->get('payload', ['Accept' => '*/*']) + ->assertOk() + ->assertHeader('Content-Type', 'application/json') + ->assertExactJson(['message' => 'hello']); + } + + public function testThrownExceptionRendersAsJsonUnderWildcardAccept() + { + Route::get('boom', fn () => throw new Exception('boom')); + + $this->get('boom', ['Accept' => '*/*']) + ->assertInternalServerError() + ->assertHeader('Content-Type', 'application/json') + ->assertJsonStructure(['message']); + } + + public function testUnauthenticatedRouteReturnsJsonUnderWildcardAccept() + { + Route::get('protected', fn () => 'secret')->middleware(Authenticate::class); + + $this->get('protected', ['Accept' => '*/*']) + ->assertUnauthorized() + ->assertHeader('Content-Type', 'application/json') + ->assertJson(['message' => 'Unauthenticated.']); + } + + public function testRequirePasswordMiddlewareReturnsJsonUnderWildcardAccept() + { + Route::get('password-confirm', fn () => 'page')->name('password.confirm'); + + Route::get('protected', fn () => 'secret') + ->middleware([StartSession::class, RequirePassword::class]); + + $this->withSession(['auth.password_confirmed_at' => time() - 10801]) + ->get('protected', ['Accept' => '*/*']) + ->assertStatus(423) + ->assertJson(['message' => 'Password confirmation required.']); + } + + public function testEnsureEmailIsVerifiedMiddlewareReturnsJsonUnderWildcardAccept() + { + Route::get('verification-notice', fn () => 'page')->name('verification.notice'); + + $user = new UnverifiedUser; + Auth::setUser($user); + + Route::get('verified-only', fn () => 'secret') + ->middleware(EnsureEmailIsVerified::class); + + $this->actingAs($user) + ->get('verified-only', ['Accept' => '*/*']) + ->assertForbidden() + ->assertHeader('Content-Type', 'application/json'); + } + + public function testValidationExceptionRendersAsJsonUnderWildcardAccept() + { + Route::get('validate', function () { + throw ValidationException::withMessages(['email' => 'The email field is required.']); + }); + + $this->get('validate', ['Accept' => '*/*']) + ->assertStatus(422) + ->assertHeader('Content-Type', 'application/json') + ->assertJsonValidationErrors(['email' => 'The email field is required.']); + } + + public function testExplicitHtmlAcceptHeaderStillReceivesHtml() + { + Route::get('plain', fn () => 'hello'); + + $this->get('plain', ['Accept' => 'text/html']) + ->assertOk() + ->assertSee('hello') + ->assertHeader('Content-Type', 'text/html; charset=UTF-8'); + } +} + +class UnverifiedUser extends Authenticatable implements MustVerifyEmail +{ + protected $guarded = []; + + public function hasVerifiedEmail(): bool + { + return false; + } + + public function markEmailAsVerified(): bool + { + return false; + } + + public function sendEmailVerificationNotification(): void + { + // + } + + public function getEmailForVerification(): string + { + return 'test@example.com'; + } + + public function getAuthIdentifier() + { + return 1; + } + + public function getAuthIdentifierName() + { + return 'id'; + } + + public function getAuthPassword() + { + return 'secret'; + } +} diff --git a/tests/Integration/Foundation/ExceptionHandlerTest.php b/tests/Integration/Foundation/ExceptionHandlerTest.php index 900c03510069..5f6aa3c61736 100644 --- a/tests/Integration/Foundation/ExceptionHandlerTest.php +++ b/tests/Integration/Foundation/ExceptionHandlerTest.php @@ -7,10 +7,12 @@ use Illuminate\Auth\Access\Response; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Debug\ShouldntReport; +use Illuminate\Contracts\Http\Kernel as HttpKernel; use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\Client\RequestException; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Middleware\PrefersJsonResponses; use Illuminate\Routing\ResponseFactory; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Http; @@ -286,6 +288,31 @@ public function json($data = [], $status = 200, array $headers = [], $options = ]); } + public function testItRendersAuthorizationExceptionsAsJsonUnderPrefersJsonForBroadAccept() + { + $this->app->make(HttpKernel::class)->prependMiddleware(PrefersJsonResponses::class); + + Route::get('test-route', fn () => Response::deny('expected message', 321)->authorize()); + + $this->get('test-route', ['Accept' => '*/*']) + ->assertForbidden() + ->assertExactJson([ + 'message' => 'expected message', + ]); + } + + public function testItStillRendersAuthorizationExceptionsAsHtmlForExplicitHtmlAcceptUnderPrefersJson() + { + $this->app->make(HttpKernel::class)->prependMiddleware(PrefersJsonResponses::class); + + Route::get('test-route', fn () => Response::deny('expected message', 321)->authorize()); + + $this->get('test-route', ['Accept' => 'text/html']) + ->assertForbidden() + ->assertSeeText('expected message') + ->assertHeader('Content-Type', 'text/html; charset=UTF-8'); + } + public function test_it_reports_request_exceptions() { config(['logging.default' => 'test_log']); From 3d64382ca7bbd4c47365b7751fe947e06b86277c Mon Sep 17 00:00:00 2001 From: Dwight Watson Date: Tue, 21 Apr 2026 03:43:20 +1000 Subject: [PATCH 205/596] [13.x] Add support for Cloudflare Email Service (#59735) * Add Cloudflare email transport * Clean up and rename apiToken to key * Get config from services instead of mail * Style fixes * Clean up test * Support config override for consistency * Throw TransportException on non-2xx * Style fixes * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Mail/MailManager.php | 20 ++ .../Mail/Transport/CloudflareTransport.php | 178 ++++++++++++++++ tests/Mail/MailCloudflareTransportTest.php | 196 ++++++++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 src/Illuminate/Mail/Transport/CloudflareTransport.php create mode 100644 tests/Mail/MailCloudflareTransportTest.php diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index 14c5cd6d3b6b..b581e46e0f6b 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Mail\Factory as FactoryContract; use Illuminate\Log\LogManager; use Illuminate\Mail\Transport\ArrayTransport; +use Illuminate\Mail\Transport\CloudflareTransport; use Illuminate\Mail\Transport\LogTransport; use Illuminate\Mail\Transport\ResendTransport; use Illuminate\Mail\Transport\SesTransport; @@ -323,6 +324,25 @@ protected function createResendTransport(array $config) ); } + /** + * Create an instance of the Cloudflare Transport driver. + * + * @param array $config + * @return \Illuminate\Mail\Transport\CloudflareTransport + */ + protected function createCloudflareTransport(array $config) + { + return new CloudflareTransport( + $config['account_id'] ?? + $this->app['config']->get('services.cloudflare.account_id'), + $config['token'] ?? + $config['key'] ?? + $this->app['config']->get('services.cloudflare.token') ?? + $this->app['config']->get('services.cloudflare.key'), + $this->getHttpClient($config), + ); + } + /** * Create an instance of the Symfony Mail Transport driver. * diff --git a/src/Illuminate/Mail/Transport/CloudflareTransport.php b/src/Illuminate/Mail/Transport/CloudflareTransport.php new file mode 100644 index 000000000000..e96cb833e71e --- /dev/null +++ b/src/Illuminate/Mail/Transport/CloudflareTransport.php @@ -0,0 +1,178 @@ +client = $client ?? HttpClient::create(); + } + + /** + * {@inheritDoc} + * + * @throws TransportException + */ + protected function doSend(SentMessage $message): void + { + try { + $response = $this->client->request('POST', sprintf( + 'https://api.cloudflare.com/client/v4/accounts/%s/email/sending/send', + $this->accountId, + ), [ + 'auth_bearer' => $this->key, + 'headers' => ['Accept' => 'application/json'], + 'json' => $this->getPayload($message), + ]); + + $result = $response->toArray(false); + } catch (Exception $exception) { + throw new TransportException( + sprintf('Request to Cloudflare API failed. Reason: %s.', $exception->getMessage()), + is_int($exception->getCode()) ? $exception->getCode() : 0, + $exception, + ); + } + + throw_if( + $response->getStatusCode() !== Response::HTTP_OK, + TransportException::class, + $result['errors'][0]['message'] ?? 'Unknown error', + $response->getStatusCode(), + ); + } + + /** + * Get the Cloudflare payload for the given message. + */ + protected function getPayload(SentMessage $message): array + { + $email = MessageConverter::toEmail($message->getOriginalMessage()); + + $envelope = $message->getEnvelope(); + + return array_filter([ + 'from' => $this->formatAddress($envelope->getSender()), + 'to' => $this->stringifyAddresses($this->getRecipients($email, $envelope)), + 'cc' => $this->stringifyAddresses($email->getCc()), + 'bcc' => $this->stringifyAddresses($email->getBcc()), + 'reply_to' => ($replyTo = $email->getReplyTo()) ? $this->formatAddress($replyTo[0]) : null, + 'subject' => $email->getSubject(), + 'html' => $email->getHtmlBody(), + 'text' => $email->getTextBody(), + 'headers' => $this->getCustomHeaders($email), + 'attachments' => $this->getAttachments($email), + ], fn ($value) => $value !== null && $value !== [] && $value !== ''); + } + + /** + * Get the recipients without CC or BCC. + */ + protected function getRecipients(Email $email, Envelope $envelope): array + { + return array_filter($envelope->getRecipients(), function (Address $address) use ($email) { + return in_array($address, array_merge($email->getCc(), $email->getBcc()), true) === false; + }); + } + + /** + * Get the custom headers for the email, excluding the standard ones. + */ + protected function getCustomHeaders(Email $email): array + { + $headers = []; + + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'reply-to', 'sender', 'subject', 'content-type']; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (in_array($name, $headersToBypass, true)) { + continue; + } + + $headers[$header->getName()] = $header->getBodyAsString(); + } + + return $headers; + } + + /** + * Get the attachments formatted for the Cloudflare API. + */ + protected function getAttachments(Email $email): array + { + $attachments = []; + + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + + $attachments[] = [ + 'content' => str_replace("\r\n", '', $attachment->bodyToString()), + 'filename' => $headers->getHeaderParameter('Content-Disposition', 'filename'), + 'type' => $headers->get('Content-Type')->getBody(), + 'disposition' => $headers->getHeaderBody('Content-Disposition') ?: 'attachment', + ]; + } + + return $attachments; + } + + /** + * Get the address formatted for the Cloudflare API. + * + * @return string|array + */ + protected function formatAddress(Address $address) + { + if ($address->getName()) { + return [ + 'name' => $address->getName(), + 'address' => $address->getAddress(), + ]; + } + + return $address->getAddress(); + } + + /** + * Get multiple addresses formatted as strings for the Cloudflare API. + */ + protected function stringifyAddresses(array $addresses): array + { + return array_map(fn (Address $a) => $a->getAddress(), $addresses); + } + + /** + * Get the string representation of the transport. + */ + public function __toString(): string + { + return 'cloudflare'; + } +} diff --git a/tests/Mail/MailCloudflareTransportTest.php b/tests/Mail/MailCloudflareTransportTest.php new file mode 100644 index 000000000000..7bcb0af4703a --- /dev/null +++ b/tests/Mail/MailCloudflareTransportTest.php @@ -0,0 +1,196 @@ +singleton('config', function () { + return new Repository([ + 'services' => [ + 'cloudflare' => [ + 'account_id' => 'test-account-id', + 'token' => 'test-token', + ], + ], + ]); + }); + + $manager = new MailManager($container); + + $transport = $manager->createSymfonyTransport(['transport' => 'cloudflare']); + + $this->assertInstanceOf(CloudflareTransport::class, $transport); + $this->assertSame('cloudflare', (string) $transport); + } + + public function testSend(): void + { + $requestBody = null; + $requestUrl = null; + $requestHeaders = null; + + $client = new MockHttpClient(function ($method, $url, $options) use (&$requestBody, &$requestUrl, &$requestHeaders) { + $requestUrl = $url; + $requestBody = json_decode($options['body'], true); + $requestHeaders = $options['normalized_headers']; + + return new MockResponse(json_encode([ + 'success' => true, + 'errors' => [], + 'messages' => [], + 'result' => [ + 'delivered' => ['me@example.com'], + 'permanent_bounces' => [], + 'queued' => [], + ], + ]), ['http_code' => 200]); + }); + + $transport = new CloudflareTransport('test-account-id', 'test-key', $client); + + $message = new Email(); + $message->subject('Test subject'); + $message->html('

Hello

'); + $message->text('Hello'); + $message->sender('sender@example.com'); + $message->to('me@example.com'); + $message->cc('cc@example.com'); + $message->bcc('bcc@example.com'); + $message->replyTo('taylor@example.com'); + $message->getHeaders()->addTextHeader('X-Custom-Header', 'CustomValue'); + + $transport->send($message); + + $this->assertStringContainsString('test-account-id', $requestUrl); + $this->assertSame('https://api.cloudflare.com/client/v4/accounts/test-account-id/email/sending/send', $requestUrl); + $this->assertSame('sender@example.com', $requestBody['from']); + $this->assertSame(['me@example.com'], $requestBody['to']); + $this->assertSame(['cc@example.com'], $requestBody['cc']); + $this->assertSame(['bcc@example.com'], $requestBody['bcc']); + $this->assertSame('taylor@example.com', $requestBody['reply_to']); + $this->assertSame('Test subject', $requestBody['subject']); + $this->assertSame('

Hello

', $requestBody['html']); + $this->assertSame('Hello', $requestBody['text']); + $this->assertSame('CustomValue', $requestBody['headers']['X-Custom-Header']); + $this->assertArrayHasKey('authorization', $requestHeaders); + $this->assertSame(['Authorization: Bearer test-key'], $requestHeaders['authorization']); + } + + public function testSendWithNamedAddresses(): void + { + $requestBody = null; + + $client = new MockHttpClient(function ($method, $url, $options) use (&$requestBody) { + $requestBody = json_decode($options['body'], true); + + return new MockResponse(json_encode([ + 'success' => true, + 'errors' => [], + 'messages' => [], + 'result' => [ + 'delivered' => ['me@example.com'], + 'permanent_bounces' => [], + 'queued' => [], + ], + ]), ['http_code' => 200]); + }); + + $transport = new CloudflareTransport('test-account-id', 'test-key', $client); + + $message = new Email(); + $message->subject('Test subject'); + $message->text('Hello'); + $message->sender(new Address('sender@example.com', 'Taylor Otwell')); + $message->to('me@example.com'); + $message->replyTo(new Address('taylor@example.com', 'Taylor Otwell')); + + $transport->send($message); + + $this->assertSame([ + 'name' => 'Taylor Otwell', + 'address' => 'sender@example.com', + ], $requestBody['from']); + $this->assertSame([ + 'name' => 'Taylor Otwell', + 'address' => 'taylor@example.com', + ], $requestBody['reply_to']); + } + + public function testSendWithAttachment(): void + { + $requestBody = null; + + $client = new MockHttpClient(function ($method, $url, $options) use (&$requestBody) { + $requestBody = json_decode($options['body'], true); + + return new MockResponse(json_encode([ + 'success' => true, + 'errors' => [], + 'messages' => [], + 'result' => ['delivered' => ['me@example.com'], 'permanent_bounces' => [], 'queued' => []], + ]), ['http_code' => 200]); + }); + + $transport = new CloudflareTransport('test-account-id', 'test-key', $client); + + $message = new Email(); + $message->subject('With attachment'); + $message->text('See attached'); + $message->sender('sender@example.com'); + $message->to('me@example.com'); + $message->attach('file contents', 'document.txt', 'text/plain'); + + $transport->send($message); + + $this->assertCount(1, $requestBody['attachments']); + $this->assertSame('document.txt', $requestBody['attachments'][0]['filename']); + $this->assertSame('text/plain', $requestBody['attachments'][0]['type']); + $this->assertSame('attachment', $requestBody['attachments'][0]['disposition']); + $this->assertNotEmpty($requestBody['attachments'][0]['content']); + } + + public function testSendThrowsOnApiFailure(): void + { + $client = new MockHttpClient(function () { + return new MockResponse(json_encode([ + 'success' => false, + 'errors' => [ + [ + 'code' => 10001, + 'message' => 'invalid_request_schema', + ], + ], + 'messages' => [], + 'result' => null, + ]), ['http_code' => 400]); + }); + + $transport = new CloudflareTransport('test-account-id', 'test-key', $client); + + $message = new Email(); + $message->subject('Fail'); + $message->text('Body'); + $message->sender('sender@example.com'); + $message->to('me@example.com'); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('invalid_request_schema'); + + $transport->send($message); + } +} From 95891fa4a7dc34648d7e78695173148ba9a41299 Mon Sep 17 00:00:00 2001 From: yousef kadah Date: Tue, 21 Apr 2026 15:41:40 +0300 Subject: [PATCH 206/596] [13.x] Add enum support to NotificationChannelManager channel and driver methods (#59783) --- .../Notifications/ChannelManager.php | 13 +++++++- .../NotificationChannelManagerTest.php | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Notifications/ChannelManager.php b/src/Illuminate/Notifications/ChannelManager.php index c7f6de68fff0..b06ab629285c 100644 --- a/src/Illuminate/Notifications/ChannelManager.php +++ b/src/Illuminate/Notifications/ChannelManager.php @@ -64,7 +64,7 @@ public function sendNow($notifiables, $notification, ?array $channels = null) /** * Get a channel instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return mixed */ public function channel($name = null) @@ -72,6 +72,17 @@ public function channel($name = null) return $this->driver($name); } + /** + * Get a driver instance. + * + * @param \UnitEnum|string|null $driver + * @return mixed + */ + public function driver($driver = null) + { + return parent::driver($driver); + } + /** * Create an instance of the database driver. * diff --git a/tests/Notifications/NotificationChannelManagerTest.php b/tests/Notifications/NotificationChannelManagerTest.php index 8c1ecac0440d..6fe8677bc0c2 100644 --- a/tests/Notifications/NotificationChannelManagerTest.php +++ b/tests/Notifications/NotificationChannelManagerTest.php @@ -49,6 +49,27 @@ public function testNotificationCanBeDispatchedToDriver() $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); } + public function testChannelCanBeResolvedUsingBackedEnum() + { + $container = new Container; + $container->instance('config', ['app.name' => 'Name', 'app.logo' => 'Logo']); + + $manager = new ChannelManager($container); + $manager->extend('test', fn () => new NotificationChannelManagerTestCustomChannel); + + $this->assertInstanceOf(NotificationChannelManagerTestCustomChannel::class, $manager->channel(NotificationChannelManagerTestChannelEnum::Test)); + } + + public function testDriverCanBeResolvedUsingBackedEnum() + { + $container = new Container; + $container->instance('config', ['app.name' => 'Name', 'app.logo' => 'Logo']); + + $manager = new ChannelManager($container); + + $this->assertInstanceOf(NotificationChannelManagerTestCustomChannel::class, $manager->driver(NotificationChannelManagerTestChannelEnum::Custom)); + } + public function testNotificationNotSentOnHalt() { $container = new Container; @@ -685,3 +706,13 @@ public function afterSending($notifiable, $channel, $response) static::$afterSendingResponse = $response; } } + +enum NotificationChannelManagerTestChannelEnum: string +{ + case Test = 'test'; + case Custom = NotificationChannelManagerTestCustomChannel::class; +} + +class NotificationChannelManagerTestCustomChannel +{ +} From aaa5f50b94048b43d1204f9db24c86692447d5be Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:42:15 +0000 Subject: [PATCH 207/596] Update facade docblocks --- src/Illuminate/Support/Facades/Notification.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Facades/Notification.php b/src/Illuminate/Support/Facades/Notification.php index eb2e088b51ce..4eb0ab108fee 100644 --- a/src/Illuminate/Support/Facades/Notification.php +++ b/src/Illuminate/Support/Facades/Notification.php @@ -9,12 +9,12 @@ /** * @method static void send(\Illuminate\Support\Collection|mixed $notifiables, mixed $notification) * @method static void sendNow(\Illuminate\Support\Collection|mixed $notifiables, mixed $notification, array|null $channels = null) - * @method static mixed channel(string|null $name = null) + * @method static mixed channel(\UnitEnum|string|null $name = null) + * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static string getDefaultDriver() * @method static string deliversVia() * @method static void deliverVia(string $channel) * @method static \Illuminate\Notifications\ChannelManager locale(string $locale) - * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Notifications\ChannelManager extend(string $driver, \Closure $callback) * @method static array getDrivers() * @method static \Illuminate\Contracts\Container\Container getContainer() From 416a93ea9c53161e0d4b8a44045f447b65a7d2f1 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:32:11 +0000 Subject: [PATCH 208/596] Update version to v13.6.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 21f2efe330a8..dcd38cfb1dda 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 = '13.5.0'; + const VERSION = '13.6.0'; /** * The base path for the Laravel installation. From b198a547dc21c41929f5475a2a9da74d76fde7a7 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:33:59 +0000 Subject: [PATCH 209/596] Update CHANGELOG --- CHANGELOG.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a07f060a8c3..b3f20680c2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,46 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.5.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.6.0...13.x) + +## [v13.6.0](https://github.com/laravel/framework/compare/v13.5.0...v13.6.0) - 2026-04-21 + +* [13.x] Use `version_compare` function by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59687 +* [13.x] Flip misordered assertions arguments by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59691 +* [13.x] Remove unused variable in `catch()` by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59689 +* [13.x] Fix number abbreviation rollover between unit tiers by [@Button99](https://github.com/Button99) in https://github.com/laravel/framework/pull/59692 +* [13.x ]Use Null and Isset coalescing when possible by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59690 +* [13.x] Change `count` array comparison to empty array comparison to improve performance by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59688 +* [13.x] testsuite by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59702 +* [13.x] Enforce static calls by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59704 +* [13.x] Allow Table Attribute on child to override parent by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59701 +* [13.x] Return null from Cursor::fromEncoded for malformed payloads by [@bipinks](https://github.com/bipinks) in https://github.com/laravel/framework/pull/59699 +* [13.x] Port forward rate limiter fix by [@paulandroshchuk](https://github.com/paulandroshchuk) in https://github.com/laravel/framework/pull/59706 +* [13.x] Add debounceable queued jobs by [@matthewnessworthy](https://github.com/matthewnessworthy) in https://github.com/laravel/framework/pull/59507 +* [13.x] Support JSON responses for the built-in health route by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/59710 +* [13.x] Ensure Queue::route string defaults to queue only by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59711 +* [13.x] Fix failOnUnknownFields query parameter handling by [@cyrodjohn](https://github.com/cyrodjohn) in https://github.com/laravel/framework/pull/59728 +* [13.x] Fix flaky QueueWorkerTest by freezing time before computing retryUntil by [@bipinks](https://github.com/bipinks) in https://github.com/laravel/framework/pull/59727 +* [13.x] Allow array of pivot arrays to be passed to hasAttached by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59723 +* [13.x] Fix TypeError in digits_between validation rule on non-string values by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59717 +* [13.x] Add enum support to PasswordBrokerManager by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59714 +* [13.x] Add enum support to BroadcastManager by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59713 +* Change attempts column type from tiny to small integer by [@ju-gow](https://github.com/ju-gow) in https://github.com/laravel/framework/pull/59718 +* [13.x] Get rid of useless Mockery::close by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59730 +* [13.x] Fix Vite CSS not loaded from nested chunk imports by [@karim1999](https://github.com/karim1999) in https://github.com/laravel/framework/pull/59662 +* [13.x] Support named credential providers for SQS queue connections by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59733 +* [13.x] Enforce stricter assertions by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59749 +* [13.x] Cast to string before preg_match in decimal, max_digits, and min_digits rules by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59739 +* [13.x] Ignore PHPUnit security advisory GHSA-qrr6-mg7r-m243 by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59761 +* [13.x] Allow assertDatabase has & missing to accept arrays by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59752 +* [13.x ] Normalize Carbon by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59750 +* [13.x] Implement CanFlushLocks on FailoverStore by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59738 +* [13.x] Validate MAC across all decryption keys by [@ma32kc](https://github.com/ma32kc) in https://github.com/laravel/framework/pull/59742 +* [13.x] Use generic TModel in additional places in Factory class by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59780 +* [13.x] Ensure assertModelMissing and assertModelExists dont silently pass by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59772 +* [13.x] Introduce `JsonFormatter` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59756 +* [13.x] Add prefersJsonResponses() to the application builder by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/59753 +* [13.x] Add support for Cloudflare Email Service by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/59735 +* [13.x] Add enum support to NotificationChannelManager channel and driver methods by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/59783 ## [v13.5.0](https://github.com/laravel/framework/compare/v13.4.0...v13.5.0) - 2026-04-14 From 79d24c863438e322f45384c29421f7b0106f4560 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Tue, 21 Apr 2026 23:38:22 +0200 Subject: [PATCH 210/596] Apply rector fixes (#59787) Co-authored-by: Lucas Michot --- src/Illuminate/Bus/DebounceLock.php | 2 +- .../Database/Eloquent/Factories/Factory.php | 2 +- tests/Log/JsonFormatterTest.php | 88 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/Illuminate/Bus/DebounceLock.php b/src/Illuminate/Bus/DebounceLock.php index d92057c5ff58..ece76ae5e0a7 100644 --- a/src/Illuminate/Bus/DebounceLock.php +++ b/src/Illuminate/Bus/DebounceLock.php @@ -149,7 +149,7 @@ public function getMaxDebounceWait($job) { $attributes = (new ReflectionClass($job))->getAttributes(DebounceFor::class); - return count($attributes) > 0 + return $attributes !== [] ? $attributes[0]->newInstance()->maxWait : null; } diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index b43e7d2d8329..e0955ac85181 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -724,7 +724,7 @@ protected function guessRelationship(string $related) */ public function hasAttached($factory, $pivot = [], $relationship = null) { - if (is_array($pivot) && count($pivot) > 0 && array_all($pivot, fn ($p) => is_array($p))) { + if (is_array($pivot) && $pivot !== [] && array_all($pivot, fn ($p) => is_array($p))) { $factory = $factory instanceof Factory && $factory->count === null ? $factory->count(count($pivot)) : $factory; diff --git a/tests/Log/JsonFormatterTest.php b/tests/Log/JsonFormatterTest.php index 266fb2010d05..3c5e1d422eae 100644 --- a/tests/Log/JsonFormatterTest.php +++ b/tests/Log/JsonFormatterTest.php @@ -40,8 +40,8 @@ public function testExceptionContextIsEnrichedOnDirectLogging() $formatted = $this->getFormattedJson(); $exceptionData = $formatted['context']['exception']; - self::assertSame('bar', $exceptionData['foo']); - self::assertSame(ContextProvidingException::class, $exceptionData['class']); + $this->assertSame('bar', $exceptionData['foo']); + $this->assertSame(ContextProvidingException::class, $exceptionData['class']); } public function testExceptionContextIsNotDuplicatedWhenGoingThroughReport() @@ -53,11 +53,11 @@ public function testExceptionContextIsNotDuplicatedWhenGoingThroughReport() $formatted = $this->getFormattedJson(); // Context should be at the top level (from the handler) - self::assertSame('bar', $formatted['context']['foo']); + $this->assertSame('bar', $formatted['context']['foo']); // But NOT enriched inside the normalized exception (formatter should skip) $exceptionData = $formatted['context']['exception']; - self::assertArrayNotHasKey('foo', $exceptionData); + $this->assertArrayNotHasKey('foo', $exceptionData); } public function testStackDriverEnrichesBothHandlersOnDirectLogging() @@ -78,8 +78,8 @@ public function testStackDriverEnrichesBothHandlersOnDirectLogging() $formatted = $this->getFormattedJson($h); $exceptionData = $formatted['context']['exception']; - self::assertSame('bar', $exceptionData['foo'], "Expected enriched context on {$name}"); - self::assertSame(ContextProvidingException::class, $exceptionData['class']); + $this->assertSame('bar', $exceptionData['foo'], "Expected enriched context on {$name}"); + $this->assertSame(ContextProvidingException::class, $exceptionData['class']); } } @@ -105,10 +105,10 @@ public function testStackDriverSkipsEnrichmentOnBothHandlersWhenReporting() foreach (['handlerA' => $handlerA, 'handlerB' => $handlerB] as $name => $h) { $formatted = $this->getFormattedJson($h); - self::assertSame('bar', $formatted['context']['foo'], "Context should be at top level on {$name}"); + $this->assertSame('bar', $formatted['context']['foo'], "Context should be at top level on {$name}"); $exceptionData = $formatted['context']['exception']; - self::assertArrayNotHasKey('foo', $exceptionData, "Formatter should not enrich on {$name}"); + $this->assertArrayNotHasKey('foo', $exceptionData, "Formatter should not enrich on {$name}"); } } @@ -122,12 +122,12 @@ public function testPreviousExceptionContextIsAlsoEnriched() $formatted = $this->getFormattedJson(); $exceptionData = $formatted['context']['exception']; - self::assertSame(RuntimeException::class, $exceptionData['class']); - self::assertArrayHasKey('previous', $exceptionData); + $this->assertSame(RuntimeException::class, $exceptionData['class']); + $this->assertArrayHasKey('previous', $exceptionData); $previousData = $exceptionData['previous']; - self::assertSame(ContextProvidingException::class, $previousData['class']); - self::assertSame('bar', $previousData['foo']); + $this->assertSame(ContextProvidingException::class, $previousData['class']); + $this->assertSame('bar', $previousData['foo']); } public function testReportEnrichesPreviousExceptionContext() @@ -139,18 +139,18 @@ public function testReportEnrichesPreviousExceptionContext() $formatted = $this->getFormattedJson(); // The outer exception has no context() method, so nothing at the top level - self::assertArrayNotHasKey('foo', $formatted['context']); + $this->assertArrayNotHasKey('foo', $formatted['context']); $exceptionData = $formatted['context']['exception']; // Outer exception should NOT be enriched (isReporting matches it) - self::assertArrayNotHasKey('foo', $exceptionData); + $this->assertArrayNotHasKey('foo', $exceptionData); // Previous exception SHOULD be enriched (isReporting does not match it) - self::assertArrayHasKey('previous', $exceptionData); + $this->assertArrayHasKey('previous', $exceptionData); $previousData = $exceptionData['previous']; - self::assertSame(ContextProvidingException::class, $previousData['class']); - self::assertSame('bar', $previousData['foo']); + $this->assertSame(ContextProvidingException::class, $previousData['class']); + $this->assertSame('bar', $previousData['foo']); } public function testExceptionWithoutContextMethodIsNotEnriched() @@ -160,9 +160,9 @@ public function testExceptionWithoutContextMethodIsNotEnriched() $formatted = $this->getFormattedJson(); $exceptionData = $formatted['context']['exception']; - self::assertSame(RuntimeException::class, $exceptionData['class']); - self::assertSame('Plain exception', $exceptionData['message']); - self::assertArrayNotHasKey('foo', $exceptionData); + $this->assertSame(RuntimeException::class, $exceptionData['class']); + $this->assertSame('Plain exception', $exceptionData['message']); + $this->assertArrayNotHasKey('foo', $exceptionData); } public function testContextCallbacksAreIncludedInFormatterEnrichment() @@ -178,8 +178,8 @@ public function testContextCallbacksAreIncludedInFormatterEnrichment() $formatted = $this->getFormattedJson(); $exceptionData = $formatted['context']['exception']; - self::assertSame('bar', $exceptionData['foo']); - self::assertSame('callback_value', $exceptionData['callback_key']); + $this->assertSame('bar', $exceptionData['foo']); + $this->assertSame('callback_value', $exceptionData['callback_key']); } public function testGracefulFallbackWhenContainerCannotResolveHandler() @@ -197,9 +197,9 @@ public function testGracefulFallbackWhenContainerCannotResolveHandler() $formatted = $this->getFormattedJson($handler); $exceptionData = $formatted['context']['exception']; - self::assertSame(ContextProvidingException::class, $exceptionData['class']); - self::assertSame('No handler bound', $exceptionData['message']); - self::assertArrayNotHasKey('foo', $exceptionData); + $this->assertSame(ContextProvidingException::class, $exceptionData['class']); + $this->assertSame('No handler bound', $exceptionData['message']); + $this->assertArrayNotHasKey('foo', $exceptionData); } public function testNonScalarContextValuesAreNormalized() @@ -211,8 +211,8 @@ public function testNonScalarContextValuesAreNormalized() $formatted = $this->getFormattedJson(); $exceptionData = $formatted['context']['exception']; - self::assertIsArray($exceptionData['nested']); - self::assertSame(ObjectContextException::class, $exceptionData['class']); + $this->assertIsArray($exceptionData['nested']); + $this->assertSame(ObjectContextException::class, $exceptionData['class']); } public function testBothOuterAndPreviousContextEnrichedOnDirectLogging() @@ -226,18 +226,18 @@ public function testBothOuterAndPreviousContextEnrichedOnDirectLogging() $exceptionData = $formatted['context']['exception']; // Outer exception should have its own context - self::assertSame('outer_value', $exceptionData['outer_key']); - self::assertSame(AnotherContextProvidingException::class, $exceptionData['class']); + $this->assertSame('outer_value', $exceptionData['outer_key']); + $this->assertSame(AnotherContextProvidingException::class, $exceptionData['class']); // Previous exception should have its own context - self::assertArrayHasKey('previous', $exceptionData); + $this->assertArrayHasKey('previous', $exceptionData); $previousData = $exceptionData['previous']; - self::assertSame('bar', $previousData['foo']); - self::assertSame(ContextProvidingException::class, $previousData['class']); + $this->assertSame('bar', $previousData['foo']); + $this->assertSame(ContextProvidingException::class, $previousData['class']); // Context keys should not bleed between exceptions - self::assertArrayNotHasKey('foo', $exceptionData); - self::assertArrayNotHasKey('outer_key', $previousData); + $this->assertArrayNotHasKey('foo', $exceptionData); + $this->assertArrayNotHasKey('outer_key', $previousData); } public function testBothOuterAndPreviousContextOnReport() @@ -250,18 +250,18 @@ public function testBothOuterAndPreviousContextOnReport() $formatted = $this->getFormattedJson(); // Outer's context should be at the top level (from the handler) - self::assertSame('outer_value', $formatted['context']['outer_key']); + $this->assertSame('outer_value', $formatted['context']['outer_key']); $exceptionData = $formatted['context']['exception']; // Outer should NOT be enriched by the formatter (isReporting matches) - self::assertArrayNotHasKey('outer_key', $exceptionData); + $this->assertArrayNotHasKey('outer_key', $exceptionData); // Previous SHOULD be enriched by the formatter (isReporting does not match) - self::assertArrayHasKey('previous', $exceptionData); + $this->assertArrayHasKey('previous', $exceptionData); $previousData = $exceptionData['previous']; - self::assertSame('bar', $previousData['foo']); - self::assertSame(ContextProvidingException::class, $previousData['class']); + $this->assertSame('bar', $previousData['foo']); + $this->assertSame(ContextProvidingException::class, $previousData['class']); } public function testFormatterHandlesNormalizationDepthLimit() @@ -283,20 +283,20 @@ public function testFormatterHandlesNormalizationDepthLimit() // Outermost exception at depth 2 — context normalize called at depth 3, // within limit so the array is returned (values inside may be depth-truncated) - self::assertArrayHasKey('foo', $exceptionData); + $this->assertArrayHasKey('foo', $exceptionData); // Previous exception at depth 3 — context normalize called at depth 4, // exceeds limit so normalize returns a string. is_array() guard skips enrichment. - self::assertArrayHasKey('previous', $exceptionData); - self::assertArrayNotHasKey('foo', $exceptionData['previous']); - self::assertSame(ContextProvidingException::class, $exceptionData['previous']['class']); + $this->assertArrayHasKey('previous', $exceptionData); + $this->assertArrayNotHasKey('foo', $exceptionData['previous']); + $this->assertSame(ContextProvidingException::class, $exceptionData['previous']['class']); } private function getFormattedJson(?TestHandler $handler = null): array { $handler ??= $this->app->make('log')->driver()->getLogger()->getHandlers()[0]; $records = $handler->getRecords(); - self::assertNotEmpty($records, 'Expected at least one log record'); + $this->assertNotEmpty($records, 'Expected at least one log record'); $formatted = $records[0]['formatted']; From 63a6ced3db46582b3276e2d03770a6317a94d6e2 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:21:29 +0000 Subject: [PATCH 211/596] Update version to v12.57.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 113073116981..434a0f3d07f9 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.56.0'; + const VERSION = '12.57.0'; /** * The base path for the Laravel installation. From db528f2e3589295d68feaa65776d444fd868043a Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:23:17 +0000 Subject: [PATCH 212/596] Update CHANGELOG --- CHANGELOG.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e279ac75be19..43ee888cc260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.56.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.57.0...12.x) + +## [v12.57.0](https://github.com/laravel/framework/compare/v12.56.0...v12.57.0) - 2026-04-22 + +* Preserve types on partialMock() and spy() by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59384 +* Fix missing UnitEnum support in ModelNotFoundException by [@jtheuerkauf](https://github.com/jtheuerkauf) in https://github.com/laravel/framework/pull/59423 +* [12.x] Fix macros with static closures by [@FeBe95](https://github.com/FeBe95) in https://github.com/laravel/framework/pull/59449 +* Correct Storage::fake() return type by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59469 +* [12.x] Fix callable type for freezeTime, freezeSecond, and travelTo by [@nbayramberdiyev](https://github.com/nbayramberdiyev) in https://github.com/laravel/framework/pull/59466 +* [12.x] Support string abstract in mock/partialMock/spy PHPDoc by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/59477 +* Document thrown exceptions in FilesystemAdapter by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59534 +* Hint \Redis `@mixin` on Connection by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59532 +* [12.x] Use PDO subclass polyfill by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59640 +* [12.x] Fix infinite rate limiter TTL on custom increments by [@paulandroshchuk](https://github.com/paulandroshchuk) in https://github.com/laravel/framework/pull/59693 +* [12.x] Support named credential providers for SQS queue connections by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59754 +* [12.x] Prevent array to string conversion in signature validation by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/59778 ## [v12.56.0](https://github.com/laravel/framework/compare/v12.55.1...v12.56.0) - 2026-03-26 From befb6d15100d0d9023339ed852244899c27c7e42 Mon Sep 17 00:00:00 2001 From: Anatoly Elyutin <50419205+Back1ng@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:25:28 +0300 Subject: [PATCH 213/596] Mirror implementation of Collection keyBy for LazyCollection (#59809) --- src/Illuminate/Collections/LazyCollection.php | 4 ++++ tests/Support/SupportCollectionTest.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index f3205e95c088..4213a41e9e7a 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -567,6 +567,10 @@ public function keyBy($keyBy) foreach ($this as $key => $item) { $resolvedKey = $keyBy($item, $key); + if ($resolvedKey instanceof \UnitEnum) { + $resolvedKey = enum_value($resolvedKey); + } + if (is_object($resolvedKey)) { $resolvedKey = (string) $resolvedKey; } diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 70a430205408..98330e3aab63 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -3751,6 +3751,20 @@ public function testKeyByAttribute($collection) $this->assertEquals([2 => ['rating' => 1, 'name' => '1'], 4 => ['rating' => 2, 'name' => '2'], 6 => ['rating' => 3, 'name' => '3']], $result->all()); } + #[DataProvider('collectionClassProvider')] + public function testKeyByBackedEnum($collection) + { + $data = new $collection([ + ['id' => 1, 'status' => TestStringBackedEnum::A], + ['id' => 2, 'status' => TestStringBackedEnum::B], + ]); + + $this->assertEquals([ + TestStringBackedEnum::A->value => ['id' => 1, 'status' => TestStringBackedEnum::A], + TestStringBackedEnum::B->value => ['id' => 2, 'status' => TestStringBackedEnum::B], + ], $data->keyBy('status')->all()); + } + #[DataProvider('collectionClassProvider')] public function testKeyByClosure($collection) { From 7b943ac7eb565f66ea85b85cf8a68eb19a6b5e6c Mon Sep 17 00:00:00 2001 From: Maher El Gamil Date: Wed, 22 Apr 2026 15:26:44 +0200 Subject: [PATCH 214/596] Add enum support to ConcurrencyManager driver method (#59801) --- src/Illuminate/Concurrency/ConcurrencyManager.php | 6 ++++-- tests/Integration/Concurrency/ConcurrencyTest.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Concurrency/ConcurrencyManager.php b/src/Illuminate/Concurrency/ConcurrencyManager.php index 8a7823e33e63..b410e71304fe 100644 --- a/src/Illuminate/Concurrency/ConcurrencyManager.php +++ b/src/Illuminate/Concurrency/ConcurrencyManager.php @@ -7,6 +7,8 @@ use RuntimeException; use Spatie\Fork\Fork; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Contracts\Concurrency\Driver */ @@ -15,12 +17,12 @@ class ConcurrencyManager extends MultipleInstanceManager /** * Get a driver instance by name. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return mixed */ public function driver($name = null) { - return $this->instance($name); + return $this->instance(enum_value($name)); } /** diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index c58c50dbf55b..3f2615eba38f 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -4,6 +4,7 @@ use Exception; use Illuminate\Concurrency\ProcessDriver; +use Illuminate\Concurrency\SyncDriver; use Illuminate\Foundation\Application; use Illuminate\Process\Factory as ProcessFactory; use Illuminate\Support\Facades\Concurrency; @@ -88,6 +89,14 @@ public function testOutputIsMappedToArrayInput() // $this->assertEquals(4, $forkOutput['second']); } + public function testDriverCanBeResolvedUsingBackedEnum() + { + $this->assertInstanceOf( + SyncDriver::class, + Concurrency::driver(ConcurrencyDriverEnum::Sync), + ); + } + public function testRunHandlerProcessErrorWithDefaultExceptionWithoutParam() { $this->expectException(Exception::class); @@ -160,6 +169,11 @@ function () { } } +enum ConcurrencyDriverEnum: string +{ + case Sync = 'sync'; +} + class ExceptionWithoutParam extends Exception { } From ac9c047d8039c55300e9dfc8a0e32d6ee1a849ab Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:27:12 +0000 Subject: [PATCH 215/596] Update facade docblocks --- src/Illuminate/Support/Facades/Concurrency.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Concurrency.php b/src/Illuminate/Support/Facades/Concurrency.php index a79bf93c3f10..4fa32d502aba 100644 --- a/src/Illuminate/Support/Facades/Concurrency.php +++ b/src/Illuminate/Support/Facades/Concurrency.php @@ -5,7 +5,7 @@ use Illuminate\Concurrency\ConcurrencyManager; /** - * @method static mixed driver(string|null $name = null) + * @method static mixed driver(\UnitEnum|string|null $name = null) * @method static \Illuminate\Concurrency\ProcessDriver createProcessDriver() * @method static \Illuminate\Concurrency\ForkDriver createForkDriver() * @method static \Illuminate\Concurrency\SyncDriver createSyncDriver() From baf1bf38c6e8d0f80d77821f7076357a4163aae2 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 22 Apr 2026 14:27:48 +0100 Subject: [PATCH 216/596] [13.x] Allow arrays for assertSoftDeleted & assertNotSoftDeleted (#59796) * add da logic * add da test * Update InteractsWithDatabase.php --- .../Concerns/InteractsWithDatabase.php | 16 ++++++++++ .../FoundationInteractsWithDatabaseTest.php | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 237c53e429a3..2ab1d1e25213 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -158,6 +158,14 @@ protected function assertSoftDeleted($table, array $data = [], $connection = nul ); } + if ($data !== [] && array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { + foreach ($data as $row) { + $this->assertSoftDeleted($table, $row, $connection, $deletedAtColumn); + } + + return $this; + } + $this->assertThat( $this->getTable($table), new SoftDeletedInDatabase( @@ -198,6 +206,14 @@ protected function assertNotSoftDeleted($table, array $data = [], $connection = ); } + if ($data !== [] && array_is_list($data) && array_all($data, fn ($row) => is_array($row))) { + foreach ($data as $row) { + $this->assertNotSoftDeleted($table, $row, $connection, $deletedAtColumn); + } + + return $this; + } + $this->assertThat( $this->getTable($table), new NotSoftDeletedInDatabase( diff --git a/tests/Foundation/FoundationInteractsWithDatabaseTest.php b/tests/Foundation/FoundationInteractsWithDatabaseTest.php index b4f5a90a9924..86226b901e68 100644 --- a/tests/Foundation/FoundationInteractsWithDatabaseTest.php +++ b/tests/Foundation/FoundationInteractsWithDatabaseTest.php @@ -205,6 +205,38 @@ public function testAssertTableEntriesCountWrong() $this->assertDatabaseCount($this->table, 3); } + public function testAssertSoftDeletedSupportsArrays() + { + $builder = m::mock(Builder::class); + $builder->shouldReceive('where')->with(['title' => 'Spark', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('where')->with(['title' => 'Forge', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('whereNotNull')->with('deleted_at')->twice()->andReturnSelf(); + $builder->shouldReceive('exists')->twice()->andReturn(true); + + $this->connection->shouldReceive('table')->with($this->table)->andReturn($builder); + + $this->assertSoftDeleted($this->table, [ + ['title' => 'Spark', 'name' => 'Laravel'], + ['title' => 'Forge', 'name' => 'Laravel'], + ]); + } + + public function testAssertNotSoftDeletedSupportsArrays() + { + $builder = m::mock(Builder::class); + $builder->shouldReceive('where')->with(['title' => 'Spark', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('where')->with(['title' => 'Forge', 'name' => 'Laravel'])->once()->andReturnSelf(); + $builder->shouldReceive('whereNull')->with('deleted_at')->twice()->andReturnSelf(); + $builder->shouldReceive('exists')->twice()->andReturn(true); + + $this->connection->shouldReceive('table')->with($this->table)->andReturn($builder); + + $this->assertNotSoftDeleted($this->table, [ + ['title' => 'Spark', 'name' => 'Laravel'], + ['title' => 'Forge', 'name' => 'Laravel'], + ]); + } + public function testAssertDatabaseMissingPassesWhenDoesNotFindResults() { $this->mockCountBuilder(false); From 80a590d8a40fa7896f715236134c75357e80aff9 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:28:51 -0400 Subject: [PATCH 217/596] [13.x] Extract exception context in `JsonFormatter` when `ExceptionHandler` is not bound (#59799) * interface * graceful even moreso * Update JsonFormatter.php --- .../Contracts/Debug/ExceptionHandler.php | 4 +++ .../Log/Formatters/JsonFormatter.php | 31 ++++++++++++----- tests/Log/JsonFormatterTest.php | 34 ++++++++----------- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/Illuminate/Contracts/Debug/ExceptionHandler.php b/src/Illuminate/Contracts/Debug/ExceptionHandler.php index 3b6594a2ba29..380d2f860902 100644 --- a/src/Illuminate/Contracts/Debug/ExceptionHandler.php +++ b/src/Illuminate/Contracts/Debug/ExceptionHandler.php @@ -4,6 +4,10 @@ use Throwable; +/** + * @method bool isReporting(\Throwable $e) + * @method array buildContextForException() + */ interface ExceptionHandler { /** diff --git a/src/Illuminate/Log/Formatters/JsonFormatter.php b/src/Illuminate/Log/Formatters/JsonFormatter.php index d25ce7cded43..54b6f119a1e8 100644 --- a/src/Illuminate/Log/Formatters/JsonFormatter.php +++ b/src/Illuminate/Log/Formatters/JsonFormatter.php @@ -17,7 +17,7 @@ protected function normalizeException(Throwable $e, int $depth = 0): array try { $handler = Container::getInstance()->make(ExceptionHandler::class); } catch (Throwable) { - return $response; + return array_merge($this->getExceptionContext($e, $depth), $response); } if ((! method_exists($handler, 'isReporting')) || ! $handler->isReporting($e)) { @@ -29,17 +29,30 @@ protected function normalizeException(Throwable $e, int $depth = 0): array $response ); } elseif (method_exists($e, 'context')) { - $exceptionContext = $this->normalize($e->context(), $depth + 1); - - if (is_array($exceptionContext)) { - $response = array_merge( - $exceptionContext, - $response - ); - } + $response = array_merge($this->getExceptionContext($e, $depth), $response); } } return $response; } + + /** + * Extract the context from the exception if available. + * + * @return array + */ + protected function getExceptionContext(Throwable $e, int $depth): array + { + if (! method_exists($e, 'context')) { + return []; + } + + try { + $exceptionContext = $this->normalize($e->context(), $depth + 1); + } catch (Throwable) { + return []; + } + + return is_array($exceptionContext) ? $exceptionContext : []; + } } diff --git a/tests/Log/JsonFormatterTest.php b/tests/Log/JsonFormatterTest.php index 3c5e1d422eae..ff6f7f36eb66 100644 --- a/tests/Log/JsonFormatterTest.php +++ b/tests/Log/JsonFormatterTest.php @@ -182,26 +182,6 @@ public function testContextCallbacksAreIncludedInFormatterEnrichment() $this->assertSame('callback_value', $exceptionData['callback_key']); } - public function testGracefulFallbackWhenContainerCannotResolveHandler() - { - Container::setInstance(new Container()); - - $handler = new TestHandler(); - $monolog = new Monolog('test', [$handler]); - $handler->setFormatter(new JsonFormatter()); - - $exception = new ContextProvidingException('No handler bound'); - - $monolog->error('fail', ['exception' => $exception]); - - $formatted = $this->getFormattedJson($handler); - $exceptionData = $formatted['context']['exception']; - - $this->assertSame(ContextProvidingException::class, $exceptionData['class']); - $this->assertSame('No handler bound', $exceptionData['message']); - $this->assertArrayNotHasKey('foo', $exceptionData); - } - public function testNonScalarContextValuesAreNormalized() { $exception = new ObjectContextException('Has objects in context'); @@ -292,6 +272,20 @@ public function testFormatterHandlesNormalizationDepthLimit() $this->assertSame(ContextProvidingException::class, $exceptionData['previous']['class']); } + public function testNoHandlerSet_mergesExceptionContext() + { + $this->app->bind(ExceptionHandlerContract::class, function () { + throw new Exception('this never works'); + }); + Log::warning('fail', ['exception' => new ContextProvidingException('Oh no!')]); + + $formatted = $this->getFormattedJson(); + + $exceptionData = $formatted['context']['exception']; + $this->assertSame('bar', $exceptionData['foo']); + $this->assertSame(ContextProvidingException::class, $exceptionData['class']); + } + private function getFormattedJson(?TestHandler $handler = null): array { $handler ??= $this->app->make('log')->driver()->getLogger()->getHandlers()[0]; From 4357ba5f2b40262bb199bc1fe1589b0715b5638f Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 22 Apr 2026 13:29:16 +0000 Subject: [PATCH 218/596] Apply fixes from StyleCI --- tests/Log/JsonFormatterTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Log/JsonFormatterTest.php b/tests/Log/JsonFormatterTest.php index ff6f7f36eb66..7d2807923740 100644 --- a/tests/Log/JsonFormatterTest.php +++ b/tests/Log/JsonFormatterTest.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Log; use Exception; -use Illuminate\Container\Container; use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract; use Illuminate\Foundation\Exceptions\Handler; use Illuminate\Log\Formatters\JsonFormatter; From 0869592120a53e9a97bcf000eabd7aa996034071 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 22 Apr 2026 14:29:47 +0100 Subject: [PATCH 219/596] [13.x] Add isLocked to the Lock class (#59791) * 13x add isLocked * test le tests * Revert "test le tests" This reverts commit c84f305176e4242701271e0e6f033d88d13340cc. * move ordering --- src/Illuminate/Cache/Lock.php | 10 ++++++++++ src/Illuminate/Cache/NoLock.php | 10 ++++++++++ tests/Integration/Cache/FileCacheLockTest.php | 12 ++++++++++++ .../Cache/MemcachedCacheLockTestCase.php | 14 ++++++++++++++ tests/Integration/Cache/RedisCacheLockTest.php | 14 ++++++++++++++ tests/Integration/Database/DatabaseLockTest.php | 12 ++++++++++++ 6 files changed, 72 insertions(+) diff --git a/src/Illuminate/Cache/Lock.php b/src/Illuminate/Cache/Lock.php index 6f8be3e2184a..c263d61d8026 100644 --- a/src/Illuminate/Cache/Lock.php +++ b/src/Illuminate/Cache/Lock.php @@ -147,6 +147,16 @@ public function owner() return $this->owner; } + /** + * Determine if the lock is currently held by any process. + * + * @return bool + */ + public function isLocked(): bool + { + return $this->getCurrentOwner() !== null; + } + /** * Determines whether this lock is allowed to release the lock in the driver. * diff --git a/src/Illuminate/Cache/NoLock.php b/src/Illuminate/Cache/NoLock.php index 68560f8f83d3..67da107dda9c 100644 --- a/src/Illuminate/Cache/NoLock.php +++ b/src/Illuminate/Cache/NoLock.php @@ -34,6 +34,16 @@ public function forceRelease() // } + /** + * Determine if the lock is currently held by any process. + * + * @return bool + */ + public function isLocked(): bool + { + return false; + } + /** * Returns the owner value written into the driver for this lock. * diff --git a/tests/Integration/Cache/FileCacheLockTest.php b/tests/Integration/Cache/FileCacheLockTest.php index c71c8aeab053..f82e3554855c 100644 --- a/tests/Integration/Cache/FileCacheLockTest.php +++ b/tests/Integration/Cache/FileCacheLockTest.php @@ -113,6 +113,18 @@ public function testCacheRememberReturnsValueWhenLockWithSameKeyExists() $lock->release(); } + public function testIsLocked() + { + $lock = Cache::lock('foo', 10); + $this->assertFalse($lock->isLocked()); + + $lock->get(); + $this->assertTrue($lock->isLocked()); + + $lock->release(); + $this->assertFalse($lock->isLocked()); + } + public function testOtherOwnerDoesNotOwnLockAfterRestore() { $firstLock = Cache::lock('foo', 10); diff --git a/tests/Integration/Cache/MemcachedCacheLockTestCase.php b/tests/Integration/Cache/MemcachedCacheLockTestCase.php index d819fb9fd73d..c3f88b3785da 100644 --- a/tests/Integration/Cache/MemcachedCacheLockTestCase.php +++ b/tests/Integration/Cache/MemcachedCacheLockTestCase.php @@ -92,6 +92,20 @@ public function testOwnerStatusCanBeCheckedAfterRestoringLock() $this->assertTrue($secondLock->isOwnedByCurrentProcess()); } + public function testIsLocked() + { + Cache::store('memcached')->lock('foo')->forceRelease(); + + $lock = Cache::store('memcached')->lock('foo', 10); + $this->assertFalse($lock->isLocked()); + + $lock->get(); + $this->assertTrue($lock->isLocked()); + + $lock->release(); + $this->assertFalse($lock->isLocked()); + } + public function testOtherOwnerDoesNotOwnLockAfterRestore() { Cache::store('memcached')->lock('foo')->forceRelease(); diff --git a/tests/Integration/Cache/RedisCacheLockTest.php b/tests/Integration/Cache/RedisCacheLockTest.php index 9ac22770b017..6ff3915e7056 100644 --- a/tests/Integration/Cache/RedisCacheLockTest.php +++ b/tests/Integration/Cache/RedisCacheLockTest.php @@ -121,6 +121,20 @@ public function testOwnerStatusCanBeCheckedAfterRestoringLock() $this->assertTrue($secondLock->isOwnedByCurrentProcess()); } + public function testIsLocked() + { + Cache::store('redis')->lock('foo')->forceRelease(); + + $lock = Cache::store('redis')->lock('foo', 10); + $this->assertFalse($lock->isLocked()); + + $lock->get(); + $this->assertTrue($lock->isLocked()); + + $lock->release(); + $this->assertFalse($lock->isLocked()); + } + public function testOtherOwnerDoesNotOwnLockAfterRestore() { Cache::store('redis')->lock('foo')->forceRelease(); diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php index bac55dd30405..2a670c01db8c 100644 --- a/tests/Integration/Database/DatabaseLockTest.php +++ b/tests/Integration/Database/DatabaseLockTest.php @@ -65,6 +65,18 @@ public function testExpiredLockCanBeRetrieved() $otherLock->release(); } + public function testIsLocked() + { + $lock = Cache::driver('database')->lock('foo'); + $this->assertFalse($lock->isLocked()); + + $lock->get(); + $this->assertTrue($lock->isLocked()); + + $lock->release(); + $this->assertFalse($lock->isLocked()); + } + public function testOtherOwnerDoesNotOwnLockAfterRestore() { $firstLock = Cache::store('database')->lock('foo'); From cf77f7af5a8e57dc12735c2a0097eb000707c7a4 Mon Sep 17 00:00:00 2001 From: Casper Bottelet Date: Wed, 22 Apr 2026 15:37:35 +0200 Subject: [PATCH 220/596] Fix route registration for domain-scoped routes (#59793) --- src/Illuminate/Routing/RouteCollection.php | 53 +++++++++++++++------- tests/Routing/RouteCollectionTest.php | 15 ++++++ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Illuminate/Routing/RouteCollection.php b/src/Illuminate/Routing/RouteCollection.php index 11d8e79b32ba..89ea9de08685 100644 --- a/src/Illuminate/Routing/RouteCollection.php +++ b/src/Illuminate/Routing/RouteCollection.php @@ -14,6 +14,13 @@ class RouteCollection extends AbstractRouteCollection */ protected $routes = []; + /** + * Domain routes keyed by method, used to maintain domain-first ordering. + * + * @var array + */ + protected $domainRoutes = []; + /** * A flattened array of all of the routes. * @@ -21,6 +28,13 @@ class RouteCollection extends AbstractRouteCollection */ protected $allRoutes = []; + /** + * Domain routes in the flattened array, used to maintain domain-first ordering. + * + * @var \Illuminate\Routing\Route[] + */ + protected $allDomainRoutes = []; + /** * A look-up table of routes by their names. * @@ -60,23 +74,20 @@ protected function addToCollections($route) { $methods = $route->methods(); $domainAndUri = $route->getDomain().$route->uri(); + $allRoutesKey = implode('|', $methods).$domainAndUri; - foreach ($methods as $method) { - if ($route->getDomain()) { - $domainRoutes = array_filter($this->routes[$method] ?? [], fn ($route) => $route->getDomain() !== null); + if ($route->getDomain()) { + foreach ($methods as $method) { + $this->domainRoutes[$method][$domainAndUri] = $route; + } - $this->routes[$method] = $domainRoutes + [$domainAndUri => $route] + ($this->routes[$method] ?? []); - } else { + $this->allDomainRoutes[$allRoutesKey] = $route; + } else { + foreach ($methods as $method) { $this->routes[$method][$domainAndUri] = $route; } - } - - if ($route->getDomain()) { - $domainRoutes = array_filter($this->allRoutes, fn ($route) => $route->getDomain() !== null); - $this->allRoutes = $domainRoutes + [implode('|', $methods).$domainAndUri => $route] + $this->allRoutes; - } else { - $this->allRoutes[implode('|', $methods).$domainAndUri] = $route; + $this->allRoutes[$allRoutesKey] = $route; } } @@ -150,7 +161,7 @@ public function refreshNameLookups() { $this->nameList = []; - foreach ($this->allRoutes as $route) { + foreach ($this->allDomainRoutes + $this->allRoutes as $route) { if (($name = $route->getName()) && ! $this->inNameLookup($name)) { $this->nameList[$name] = $route; } @@ -168,7 +179,7 @@ public function refreshActionLookups() { $this->actionList = []; - foreach ($this->allRoutes as $route) { + foreach ($this->allDomainRoutes + $this->allRoutes as $route) { if (($controller = $route->getAction()['controller'] ?? null) && ! $this->inActionLookup($controller)) { $this->addToActionList($route->getAction(), $route); } @@ -204,7 +215,9 @@ public function match(Request $request) */ public function get($method = null) { - return is_null($method) ? $this->getRoutes() : ($this->routes[$method] ?? []); + return is_null($method) + ? $this->getRoutes() + : ($this->domainRoutes[$method] ?? []) + ($this->routes[$method] ?? []); } /** @@ -247,7 +260,7 @@ public function getByAction($action) */ public function getRoutes() { - return array_values($this->allRoutes); + return array_values($this->allDomainRoutes + $this->allRoutes); } /** @@ -257,7 +270,13 @@ public function getRoutes() */ public function getRoutesByMethod() { - return $this->routes; + $result = $this->domainRoutes; + + foreach ($this->routes as $method => $routes) { + $result[$method] = ($result[$method] ?? []) + $routes; + } + + return $result; } /** diff --git a/tests/Routing/RouteCollectionTest.php b/tests/Routing/RouteCollectionTest.php index 1ab1dbfd7ddb..f84dae01a10e 100644 --- a/tests/Routing/RouteCollectionTest.php +++ b/tests/Routing/RouteCollectionTest.php @@ -419,4 +419,19 @@ public function testPrependsRoutesWithDomain() ], ], $this->routeCollection->getRoutesByMethod()); } + + public function testDomainRoutesAreMatchedBeforeNonDomainRoutes() + { + $this->routeCollection->add( + (new Route('GET', 'users', ['uses' => 'NoDomainController@index']))->name('no-domain') + ); + + $this->routeCollection->add( + (new Route('GET', 'users', ['uses' => 'DomainController@index']))->domain('api.test')->name('with-domain') + ); + + $request = Request::create('http://api.test/users', 'GET'); + + $this->assertSame('with-domain', $this->routeCollection->match($request)->getName()); + } } From 51d106d359b0d16df49013d1c35162fda05d2129 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:37:55 +0200 Subject: [PATCH 221/596] [13.x] Mark `Scope@apply` builder parameter as having covariant template (#59790) * Add type violation example for current setup * Mark builder parameter generic as covariant in Scope@apply * Old PHP syntax fixes --- src/Illuminate/Database/Eloquent/Scope.php | 2 +- types/Database/Eloquent/Scope.php | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Scope.php b/src/Illuminate/Database/Eloquent/Scope.php index 4eeeac0545b2..543d731e94b7 100644 --- a/src/Illuminate/Database/Eloquent/Scope.php +++ b/src/Illuminate/Database/Eloquent/Scope.php @@ -10,7 +10,7 @@ interface Scope /** * Apply the scope to a given Eloquent query builder. * - * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Builder $builder * @param TModel $model * @return void */ diff --git a/types/Database/Eloquent/Scope.php b/types/Database/Eloquent/Scope.php index b651be3a318c..751c0273abf5 100644 --- a/types/Database/Eloquent/Scope.php +++ b/types/Database/Eloquent/Scope.php @@ -15,7 +15,7 @@ class UserScope implements Scope { public function apply(Builder $builder, Model $model): void { - assertType('Illuminate\Database\Eloquent\Builder', $builder); + assertType('Illuminate\Database\Eloquent\Builder', $builder); assertType('Illuminate\Types\Scope\User', $model); } } @@ -27,7 +27,7 @@ class GenericScope implements Scope { public function apply(Builder $builder, Model $model): void { - assertType('Illuminate\Database\Eloquent\Builder', $builder); + assertType('Illuminate\Database\Eloquent\Builder', $builder); assertType('Illuminate\Database\Eloquent\Model', $model); } } @@ -35,3 +35,8 @@ public function apply(Builder $builder, Model $model): void class User extends Model { } + +$user = new User(); +$query = User::query(); +(new UserScope())->apply($query, $user); +(new GenericScope())->apply($query, $user); From ff94fce89f085931d20bc7b1d22c0410a6130d24 Mon Sep 17 00:00:00 2001 From: Tim Withers Date: Wed, 22 Apr 2026 06:52:54 -0700 Subject: [PATCH 222/596] [13.x] Allowing `DebounceFor` attribute to be inherited (#59795) * Allowing debounce to inherit attributes from parent * Styling changes --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/DebounceLock.php | 7 +--- .../Foundation/Bus/PendingDispatch.php | 14 +++---- .../Support/Traits/ReadsClassAttributes.php | 38 ++++++++++++----- tests/Integration/Queue/DebouncedJobTest.php | 42 +++++++++++++++++++ 4 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/Illuminate/Bus/DebounceLock.php b/src/Illuminate/Bus/DebounceLock.php index ece76ae5e0a7..95024f8b2c71 100644 --- a/src/Illuminate/Bus/DebounceLock.php +++ b/src/Illuminate/Bus/DebounceLock.php @@ -7,7 +7,6 @@ use Illuminate\Queue\Attributes\ReadsQueueAttributes; use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use ReflectionClass; class DebounceLock { @@ -147,11 +146,7 @@ public function getDebounceDelay($job) */ public function getMaxDebounceWait($job) { - $attributes = (new ReflectionClass($job))->getAttributes(DebounceFor::class); - - return $attributes !== [] - ? $attributes[0]->newInstance()->maxWait - : null; + return $this->getAttributeInstance($job, DebounceFor::class)?->maxWait ?? null; } /** diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index 92e2baa104dc..094c488d3abb 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -9,9 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Foundation\Queue\InteractsWithUniqueJobs; -use Illuminate\Queue\Attributes\DebounceFor; use LogicException; -use ReflectionClass; class PendingDispatch { @@ -218,11 +216,15 @@ protected function shouldDispatch() * * @return void * - * @throws \LogicException + * @throws LogicException */ protected function acquireDebounceLock() { - if (empty((new ReflectionClass($this->job))->getAttributes(DebounceFor::class))) { + $lock = new DebounceLock(Container::getInstance()->make(Cache::class)); + + $debounceFor = $lock->getDebounceDelay($this->job); + + if ($debounceFor === null) { return; } @@ -230,10 +232,8 @@ protected function acquireDebounceLock() throw new LogicException('A debounced job cannot also implement ShouldBeUnique.'); } - $lock = new DebounceLock(Container::getInstance()->make(Cache::class)); - $result = $lock->acquire( - $this->job, $debounceFor = $lock->getDebounceDelay($this->job) + $this->job, $debounceFor ); $this->job->debounceOwner = $result['owner']; diff --git a/src/Illuminate/Support/Traits/ReadsClassAttributes.php b/src/Illuminate/Support/Traits/ReadsClassAttributes.php index 9860f312ed2a..56f7800f313a 100644 --- a/src/Illuminate/Support/Traits/ReadsClassAttributes.php +++ b/src/Illuminate/Support/Traits/ReadsClassAttributes.php @@ -26,16 +26,8 @@ protected function getAttributeValue($target, string $attributeClass, ?string $p return $target->{$property}; } - try { - do { - $attributes = $reflection->getAttributes($attributeClass); - - if (count($attributes) > 0) { - return $this->extractAttributeValue($attributes[0]->newInstance()); - } - } while ($reflection = $reflection->getParentClass()); - } catch (Exception) { - // + if ($instance = $this->getAttributeInstance($target, $attributeClass)) { + return $this->extractAttributeValue($instance); } return $target->{$property} ?? $default; @@ -53,4 +45,30 @@ protected function extractAttributeValue($instance) return $properties === [] ? true : reset($properties); } + + /** + * Get an instance of the given attribute class from the target class or its parents. + * + * @param object $target + * @param class-string $attributeClass + * @return object|null + */ + protected function getAttributeInstance($target, string $attributeClass) + { + $reflection = new ReflectionClass($target); + + try { + do { + $attributes = $reflection->getAttributes($attributeClass); + + if (count($attributes) > 0) { + return $attributes[0]->newInstance(); + } + } while ($reflection = $reflection->getParentClass()); + } catch (Exception) { + // + } + + return null; + } } diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php index c1bee70f4668..4f0f82e33858 100644 --- a/tests/Integration/Queue/DebouncedJobTest.php +++ b/tests/Integration/Queue/DebouncedJobTest.php @@ -311,6 +311,27 @@ public function testDebounceWithoutMaxWaitAllowsIndefiniteDelay() $this->assertEquals(30, $job2->delay); } + + public function testChildDebouncedJobInheritsFromParent() + { + $this->markTestSkippedWhenUsingQueueDrivers(['sync', 'beanstalkd']); + + ChildOfDebouncedTestJob::$handleCount = 0; + + // Dispatch two jobs with the same debounce identity. + // The second dispatch supersedes the first. + dispatch(new ChildOfDebouncedTestJob('entity-1')); + dispatch(new ChildOfDebouncedTestJob('entity-1')); + + // Advance time past the debounce window so jobs become available. + $this->travelTo(Carbon::now()->addSeconds(31)); + + // Process both jobs from the queue. + $this->runQueueWorkerCommand(['--once' => true], 2); + + // Only the second (latest) dispatch should have executed. + $this->assertEquals(1, ChildOfDebouncedTestJob::$handleCount); + } } #[DebounceFor(30)] @@ -445,3 +466,24 @@ public function handle() static::$handleCount++; } } + +class ChildOfDebouncedTestJob extends DebouncedTestJob implements ShouldQueue +{ + use InteractsWithQueue, Queueable, Dispatchable; + + public static $handleCount = 0; + + public function __construct(public string $entityId) + { + } + + public function debounceId(): string + { + return $this->entityId; + } + + public function handle() + { + static::$handleCount++; + } +} From 9f3dc9f532eda23d62f62baf9a26dd6488d260c7 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:29:57 +0600 Subject: [PATCH 223/596] [13.x] Fix PendingDispatch resolving Cache for every dispatched job (#59821) * [13.x] Fix PendingDispatch resolving Cache when job has no debounce attribute * [13.x] Fix PendingDispatch resolving Cache when job has no debounce attribute --- src/Illuminate/Foundation/Bus/PendingDispatch.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index 094c488d3abb..110bd983f267 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -9,11 +9,14 @@ use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Foundation\Queue\InteractsWithUniqueJobs; +use Illuminate\Queue\Attributes\DebounceFor; +use Illuminate\Queue\Attributes\ReadsQueueAttributes; use LogicException; class PendingDispatch { use InteractsWithUniqueJobs; + use ReadsQueueAttributes; /** * The job. @@ -220,14 +223,14 @@ protected function shouldDispatch() */ protected function acquireDebounceLock() { - $lock = new DebounceLock(Container::getInstance()->make(Cache::class)); - - $debounceFor = $lock->getDebounceDelay($this->job); + $debounceFor = $this->getAttributeValue($this->job, DebounceFor::class, 'debounceFor'); if ($debounceFor === null) { return; } + $lock = new DebounceLock(Container::getInstance()->make(Cache::class)); + if ($this->job instanceof ShouldBeUnique) { throw new LogicException('A debounced job cannot also implement ShouldBeUnique.'); } From 912d9eecae51ce29fc367b8fde81ae6d841c121d Mon Sep 17 00:00:00 2001 From: "Cy(rod) John" Date: Thu, 23 Apr 2026 22:34:29 +0800 Subject: [PATCH 224/596] [13.x] Add bulk JSON path assertions to TestResponse (#59829) * feat: add bulk json path assertions * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Testing/TestResponse.php | 28 +++++++++++++ tests/Testing/TestResponseTest.php | 54 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 13a14fd9035d..e7c08da98430 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -875,6 +875,20 @@ public function assertJsonPath($path, $expect) return $this; } + /** + * Assert that the expected values and types exist at the given paths in the response. + * + * @return $this + */ + public function assertJsonPaths(array $paths) + { + foreach ($paths as $path => $expected) { + $this->assertJsonPath($path, $expected); + } + + return $this; + } + /** * Assert that the given path in the response contains all of the expected values without looking at the order. * @@ -983,6 +997,20 @@ public function assertJsonMissingPath(string $path) return $this; } + /** + * Assert that the response does not contain the given paths. + * + * @return $this + */ + public function assertJsonMissingPaths(array $paths) + { + foreach ($paths as $path) { + $this->assertJsonMissingPath($path); + } + + return $this; + } + /** * Assert that the response has a given JSON structure. * diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index ba4bbf05713e..ac7401d5ee8c 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1593,6 +1593,38 @@ public function testAssertJsonPathCanonicalizingCanFail(): void $response->assertJsonPathCanonicalizing('*.foo', ['foo 0', 'foo 2', 'foo 3']); } + public function testAssertJsonPaths(): void + { + $response = TestResponse::fromBaseResponse(new Response([ + 'data' => [ + 'id' => 1, + 'name' => 'Taylor', + ], + 'meta' => [ + 'count' => 3, + ], + ])); + + $response->assertJsonPaths([ + 'data.id' => 1, + 'data.name' => fn ($value) => $value === 'Taylor', + 'meta.count' => 3, + ]); + } + + public function testAssertJsonPathsCanFail(): void + { + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that 10 is identical to 11.'); + + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceWithIntegersStub)); + + $response->assertJsonPaths([ + '0.id' => 11, + '1.id' => 20, + ]); + } + public function testAssertJsonFragment(): void { $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); @@ -1854,6 +1886,28 @@ public function testAssertJsonMissingPathCanFail3(): void $response->assertJsonMissingPath('numeric_keys.3'); } + public function testAssertJsonMissingPaths(): void + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); + + $response->assertJsonMissingPaths([ + 'foobar.missing', + 'numeric_keys.0', + ]); + } + + public function testAssertJsonMissingPathsCanFail(): void + { + $this->expectException(AssertionFailedError::class); + + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); + + $response->assertJsonMissingPaths([ + 'foo', + 'foobar.missing', + ]); + } + public function testAssertJsonValidationErrors(): void { $data = [ From c06b4733762aa95f3a89b937218e8af1eedf5645 Mon Sep 17 00:00:00 2001 From: Christos Koumpis <56029580+Button99@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:59:35 +0300 Subject: [PATCH 225/596] [13.x] Fix false positives in LazyCollection::has() for duplicate keys (#59832) * fix LazyCollection::has() with duplicate generator keys * Update LazyCollection.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Collections/LazyCollection.php | 5 +++-- tests/Support/SupportLazyCollectionTest.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 4213a41e9e7a..b1ed2ca7d158 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -589,10 +589,11 @@ public function keyBy($keyBy) public function has($key) { $keys = array_flip(is_array($key) ? $key : func_get_args()); - $count = count($keys); foreach ($this as $key => $value) { - if (array_key_exists($key, $keys) && --$count == 0) { + unset($keys[$key]); + + if (empty($keys)) { return true; } } diff --git a/tests/Support/SupportLazyCollectionTest.php b/tests/Support/SupportLazyCollectionTest.php index c658d70ab231..c587210cdfeb 100644 --- a/tests/Support/SupportLazyCollectionTest.php +++ b/tests/Support/SupportLazyCollectionTest.php @@ -531,4 +531,17 @@ public function testRandomPreservesKeys() $this->assertContains($key, ['first', 'second', 'third']); } } + + public function testHasDoesNotCountDuplicateKeys() + { + $collection = LazyCollection::make(function () { + yield 'a' => 1; + yield 'a' => 2; + yield 'c' => 3; + }); + + $this->assertFalse($collection->has('a', 'b')); + $this->assertFalse($collection->has(['a', 'b'])); + $this->assertTrue($collection->has('a')); + } } From 27c76f88aac666307477f6f0580b08f3c22c1bfa Mon Sep 17 00:00:00 2001 From: Alberto Peripolli Date: Fri, 24 Apr 2026 16:06:17 +0200 Subject: [PATCH 226/596] Update constructor parameter type for limiterName (#59841) --- src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php index 8ac778f3179d..a0afebff1485 100644 --- a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php +++ b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php @@ -28,7 +28,7 @@ class RateLimitedWithRedis extends RateLimited /** * Create a new middleware instance. * - * @param string $limiterName + * @param \UnitEnum|string $limiterName */ public function __construct($limiterName, ?string $connection = null) { From 86cb6af75924838e97e2b426326324cd6b543c6c Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 24 Apr 2026 15:55:56 +0100 Subject: [PATCH 227/596] [13.x] Allow jobs to react to worker signals (#59833) * wip set after debounced oops Update Worker.php rename to make sense * add finally * formatting * add doc block --------- Co-authored-by: Taylor Otwell --- .../Contracts/Queue/Interruptible.php | 14 ++++++ src/Illuminate/Queue/CallQueuedHandler.php | 25 +++++++++- src/Illuminate/Queue/Worker.php | 48 +++++++++++++++++-- tests/Queue/QueueWorkerTest.php | 38 +++++++++++++++ 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/Illuminate/Contracts/Queue/Interruptible.php diff --git a/src/Illuminate/Contracts/Queue/Interruptible.php b/src/Illuminate/Contracts/Queue/Interruptible.php new file mode 100644 index 000000000000..81a924e6208c --- /dev/null +++ b/src/Illuminate/Contracts/Queue/Interruptible.php @@ -0,0 +1,14 @@ +deleteDebouncedJob($job, $command); } - $this->dispatchThroughMiddleware($job, $command); + $this->runningCommand = $command; + + try { + $this->dispatchThroughMiddleware($job, $command); + } finally { + $this->runningCommand = null; + } if (! $job->isReleased() && ! $this->commandShouldBeUniqueUntilProcessing($command)) { $this->ensureUniqueJobLockIsReleased($command); @@ -428,4 +441,14 @@ protected function ensureChainCatchCallbacksAreInvoked(string $uuid, $command, $ $command->invokeChainCatchCallbacks($e); } } + + /** + * Get the command currently being processed. + * + * @return mixed + */ + public function getRunningCommand() + { + return $this->runningCommand; + } } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index a39cc2a7cd07..2f02aae85ba4 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Queue\Factory as QueueManager; +use Illuminate\Contracts\Queue\Interruptible; use Illuminate\Database\DetectsLostConnections; use Illuminate\Queue\Events\JobAttempted; use Illuminate\Queue\Events\JobExceptionOccurred; @@ -78,6 +79,13 @@ class Worker */ protected $resetScope; + /** + * The job currently being processed. + * + * @var \Illuminate\Contracts\Queue\Job|null + */ + public $currentJob = null; + /** * Indicates if the worker should exit. * @@ -446,6 +454,8 @@ protected function queuePaused($connectionName, $queue) */ protected function runJob($job, $connectionName, WorkerOptions $options) { + $this->currentJob = $job; + try { return $this->process($connectionName, $job, $options); } catch (Throwable $e) { @@ -454,6 +464,8 @@ protected function runJob($job, $connectionName, WorkerOptions $options) } $this->stopWorkerIfLostConnection($e); + } finally { + $this->currentJob = null; } } @@ -808,13 +820,43 @@ protected function listenForSignals() { pcntl_async_signals(true); - pcntl_signal(SIGQUIT, fn () => $this->shouldQuit = true); - pcntl_signal(SIGTERM, fn () => $this->shouldQuit = true); - pcntl_signal(SIGINT, fn () => $this->shouldQuit = true); + foreach ([SIGQUIT, SIGTERM, SIGINT] as $signal) { + pcntl_signal($signal, function (int $signal) { + $this->shouldQuit = true; + + $this->notifyJobOfSignal($signal); + }); + } + pcntl_signal(SIGUSR2, fn () => $this->paused = true); pcntl_signal(SIGCONT, fn () => $this->paused = false); } + /** + * Passes the signal to the running job. + * + * @param int $signal + * @return void + */ + protected function notifyJobOfSignal(int $signal): void + { + if (! $this->currentJob) { + return; + } + + $handler = $this->currentJob->getResolvedJob(); + + if (! $handler instanceof CallQueuedHandler) { + return; + } + + $job = $handler->getRunningCommand(); + + if ($job instanceof Interruptible) { + $job->interrupted($signal); + } + } + /** * Determine if "async" signals are supported. * diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index f66766cdf2b7..c96d83db3659 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -6,7 +6,9 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Queue\Interruptible; use Illuminate\Contracts\Queue\Job as QueueJobContract; +use Illuminate\Queue\CallQueuedHandler; use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Events\JobPopped; use Illuminate\Queue\Events\JobPopping; @@ -507,6 +509,31 @@ public function testJobReleasedEvent() }))->once(); } + public function testInterruptibleJobIsNotifiedOnSignal() + { + $interruptible = new class implements Interruptible + { + public ?int $receivedSignal = null; + + public function interrupted(int $signal): void + { + $this->receivedSignal = $signal; + } + }; + + $handler = m::mock(CallQueuedHandler::class); + $handler->shouldReceive('getRunningCommand')->andReturn($interruptible); + + $worker = $this->getWorker('default', ['queue' => []]); + $job = new WorkerFakeJob; + $job->resolvedJob = $handler; + + $worker->currentJob = $job; + $worker->notifyJobOfSignal(15); + + $this->assertSame(15, $interruptible->receivedSignal); + } + /** * Helpers... */ @@ -554,6 +581,11 @@ public function sleep($seconds) $this->sleptFor = $seconds; } + public function notifyJobOfSignal(int $signal): void + { + parent::notifyJobOfSignal($signal); + } + public function stop($status = 0, $options = null, $reason = null) { return parent::stop($status, $options, $reason); @@ -649,6 +681,7 @@ class WorkerFakeJob implements QueueJobContract public $connectionName = ''; public $queue = ''; public $rawBody = ''; + public $resolvedJob = null; public function __construct($callback = null) { @@ -788,6 +821,11 @@ public function resolveQueuedJobClass() { return 'WorkerFakeJob'; } + + public function getResolvedJob() + { + return $this->resolvedJob; + } } class LoopBreakerException extends RuntimeException From b9fc87f68804ef69b2a8fc72109d84b29f846483 Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Fri, 24 Apr 2026 20:52:52 +0545 Subject: [PATCH 228/596] Honor empty JSON:API sparse fieldsets (#59813) --- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 9 ++++----- .../Http/Resources/JsonApi/JsonApiRequest.php | 12 ++++++++++++ .../Http/Resources/JsonApi/JsonApiRequestTest.php | 12 ++++++++++++ .../Resources/JsonApi/JsonApiResourceTest.php | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index dd7f5640cd79..2c4dc62de735 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -143,14 +143,13 @@ protected function resolveResourceAttributes(JsonApiRequest $request, string $re $data = $data->jsonSerialize(); } - $sparseFieldset = match ($this->usesRequestQueryString) { - true => $request->sparseFields($resourceType), - default => [], - }; + $usesSparseFieldset = $this->usesRequestQueryString && $request->hasSparseFieldset($resourceType); + + $sparseFieldset = $usesSparseFieldset ? $request->sparseFields($resourceType) : []; $data = (new Collection($data)) ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) - ->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset)) + ->when($usesSparseFieldset, fn ($attributes) => $attributes->only($sparseFieldset)) ->transform(fn ($value) => value($value, $request)) ->all(); diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index 51224a8ea9f3..ea220630e331 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -32,6 +32,18 @@ public function sparseFields(string $key): array return $this->cachedSparseFields[$key] ?? []; } + /** + * Determine if a sparse fieldset was provided for the given resource type. + */ + public function hasSparseFieldset(string $key): bool + { + if (is_null($this->cachedSparseFields)) { + $this->sparseFields($key); + } + + return array_key_exists($key, $this->cachedSparseFields); + } + /** * Get the request's included relationships. */ diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php index ff96ec1f7879..2f2f82a14f88 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php @@ -30,6 +30,18 @@ public function testItCanResolveEmptySparseFields() $this->assertSame([], $request->sparseFields('posts')); } + public function testItCanDetermineIfSparseFieldsetWasProvided() + { + $request = JsonApiRequest::create(uri: '/?'.http_build_query([ + 'fields' => [ + 'users' => '', + ], + ])); + + $this->assertTrue($request->hasSparseFieldset('users')); + $this->assertFalse($request->hasSparseFieldset('posts')); + } + public function testItCanResolveSparseIncluded() { $request = JsonApiRequest::create(uri: '/?'.http_build_query([ diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 956169738803..1ac7d71c8132 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -48,6 +48,21 @@ public function testItCanGenerateJsonApiResponseWithSparseFieldsets() ->assertJsonMissing(['jsonapi', 'included']); } + public function testItCanGenerateJsonApiResponseWithEmptySparseFieldsets() + { + $user = User::factory()->create(); + + $this->getJson("/users/{$user->getKey()}?".http_build_query(['fields' => ['users' => '']])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + ], + ]) + ->assertJsonMissing(['jsonapi', 'included']); + } + public function testItCanGenerateJsonApiResponseWithEmptyRelationshipsUsingSparseIncluded() { $user = User::factory()->create(); From 2457caf986357d0cb84e0e4c1d9815a36450a8dc Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Fri, 24 Apr 2026 23:27:15 +0600 Subject: [PATCH 229/596] [13.x] Fix flaky DynamoBatchTest timing assertions (#59844) --- tests/Integration/Queue/DynamoBatchTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Queue/DynamoBatchTest.php b/tests/Integration/Queue/DynamoBatchTest.php index d9c4dd7825c8..046059e51081 100644 --- a/tests/Integration/Queue/DynamoBatchTest.php +++ b/tests/Integration/Queue/DynamoBatchTest.php @@ -67,7 +67,7 @@ public function test_retrieve_batch_by_id() $retrieved = $repo->find($batch->id); $this->assertEquals(2, $retrieved->totalJobs); $this->assertEquals(0, $retrieved->failedJobs); - $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSecond(), Carbon::now())); + $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSeconds(3), Carbon::now())); } public function test_retrieve_non_existent_batch() @@ -114,8 +114,8 @@ public function test_batch_with_failing_job() $retrieved = $repo->find($batch->id); $this->assertEquals(2, $retrieved->totalJobs); $this->assertEquals(1, $retrieved->failedJobs); - $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSecond(), Carbon::now())); - $this->assertTrue($retrieved->cancelledAt->between(Carbon::now()->subSecond(), Carbon::now())); + $this->assertTrue($retrieved->finishedAt->between(Carbon::now()->subSeconds(3), Carbon::now())); + $this->assertTrue($retrieved->cancelledAt->between(Carbon::now()->subSeconds(3), Carbon::now())); } public function test_get_batches() From 98e99685580dd74b0f0587eb14367492d8a4caad Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Sun, 26 Apr 2026 17:36:27 +0100 Subject: [PATCH 230/596] [13.x] Memoize credentials in SqsConnector (#59866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a queue config sets `credentials.provider = ecs` (or `instance`), SqsConnector::resolveCredentialProvider returned a raw EcsCredentialProvider or InstanceProfileProvider. The AWS SDK's ClientResolver short-circuits any callable passed as `credentials` (no automatic memoize wrap), and the signer middleware invokes the provider on every signed request — so every SQS API call triggered a fresh HTTP fetch to the EKS Pod Identity Agent / EC2 metadata endpoint. Wrap the resolved provider in CredentialProvider::memoize so credentials are cached in-process for the lifetime of the worker, with the SDK's standard 60-second pre-expiry refresh window. This matches what the SDK's own defaultProvider() does and stops queue workers from saturating the Pod Identity Agent's rate limiter under steady-state polling. Co-authored-by: Claude Opus 4.7 (1M context) --- src/Illuminate/Queue/Connectors/SqsConnector.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index 70c90873d794..0bce9a654a95 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -61,13 +61,15 @@ protected function resolveCredentialProvider(array $config) $options = is_array($credentials) ? Arr::except($credentials, ['provider']) : []; - return match ($provider) { + $resolved = match ($provider) { 'ecs' => CredentialProvider::ecsCredentials($options), 'instance' => CredentialProvider::instanceProfile($options), default => throw new InvalidArgumentException( "Invalid credential provider [{$provider}]." ), }; + + return CredentialProvider::memoize($resolved); } /** From 94dafaafc8623098f2f1d211ede53410544147c3 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Sun, 26 Apr 2026 17:36:41 +0100 Subject: [PATCH 231/596] [12.x] Memoize credentials in SqsConnector (#59867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a queue config sets `credentials.provider = ecs` (or `instance`), SqsConnector::resolveCredentialProvider returned a raw EcsCredentialProvider or InstanceProfileProvider. The AWS SDK's ClientResolver short-circuits any callable passed as `credentials` (no automatic memoize wrap), and the signer middleware invokes the provider on every signed request — so every SQS API call triggered a fresh HTTP fetch to the EKS Pod Identity Agent / EC2 metadata endpoint. Wrap the resolved provider in CredentialProvider::memoize so credentials are cached in-process for the lifetime of the worker, with the SDK's standard 60-second pre-expiry refresh window. This matches what the SDK's own defaultProvider() does and stops queue workers from saturating the Pod Identity Agent's rate limiter under steady-state polling. Co-authored-by: Claude Opus 4.7 (1M context) --- src/Illuminate/Queue/Connectors/SqsConnector.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index 70c90873d794..0bce9a654a95 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -61,13 +61,15 @@ protected function resolveCredentialProvider(array $config) $options = is_array($credentials) ? Arr::except($credentials, ['provider']) : []; - return match ($provider) { + $resolved = match ($provider) { 'ecs' => CredentialProvider::ecsCredentials($options), 'instance' => CredentialProvider::instanceProfile($options), default => throw new InvalidArgumentException( "Invalid credential provider [{$provider}]." ), }; + + return CredentialProvider::memoize($resolved); } /** From 6172ae1f44ba5d89e111057ee4a4e7c27f5a610d Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:42:04 +0000 Subject: [PATCH 232/596] Update version to v12.58.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 434a0f3d07f9..5b420dbffcff 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.57.0'; + const VERSION = '12.58.0'; /** * The base path for the Laravel installation. From a08617bf1e2cc46e416707c156ef5876bd3d6fd4 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:43:39 +0000 Subject: [PATCH 233/596] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ee888cc260..76ba083a38f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.57.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.58.0...12.x) + +## [v12.58.0](https://github.com/laravel/framework/compare/v12.57.0...v12.58.0) - 2026-04-26 + +* [12.x] Memoize credentials in SqsConnector by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59867 ## [v12.57.0](https://github.com/laravel/framework/compare/v12.56.0...v12.57.0) - 2026-04-22 From d097521ad9dab52a37970e9ef18b5acea8ad6004 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Mon, 27 Apr 2026 14:31:44 +0100 Subject: [PATCH 234/596] [13.x] Disable pausing on managed queue workers (#59870) * [13.x] Disable pausing on managed queue workers Cloud-managed queue workers should not respond to pause signals, matching the existing behavior for restart signals. * Add test for managed queues disabling pause signal handling --- src/Illuminate/Foundation/Cloud.php | 1 + tests/Integration/Foundation/CloudTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 5cda82a7ae67..de30ca27302e 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -121,6 +121,7 @@ public static function configureManagedQueues(Application $app): void { if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { Worker::$restartable = false; + Worker::$pausable = false; $app['config']->set( 'queue.connections.sqs.credentials', diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index 1527f0baf1ee..e7d463474462 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -69,6 +69,21 @@ public function test_it_disables_queue_restart_polling_for_managed_queues() } } + public function test_it_disables_queue_pause_polling_for_managed_queues() + { + Worker::$pausable = true; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertFalse(Worker::$pausable); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + Worker::$pausable = true; + } + } + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] public function test_it_configures_managed_queue_credentials() { From 0a2aab7a742a2c96c670e4f3fa82aa87e3162d3a Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Mon, 27 Apr 2026 14:31:51 +0100 Subject: [PATCH 235/596] [12.x] Disable pausing on managed queue workers (#59871) * [12.x] Disable pausing on managed queue workers Cloud-managed queue workers should not respond to pause signals, matching the existing behavior for restart signals. * Add test for managed queues disabling pause signal handling --- src/Illuminate/Foundation/Cloud.php | 1 + tests/Integration/Foundation/CloudTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 30107606ddcc..c0c0e2e40350 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -121,6 +121,7 @@ public static function configureManagedQueues(Application $app): void { if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { Worker::$restartable = false; + Worker::$pausable = false; $app['config']->set( 'queue.connections.sqs.credentials', diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index d79d31dfea09..72feb5a55c29 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -69,6 +69,21 @@ public function test_it_disables_queue_restart_polling_for_managed_queues() } } + public function test_it_disables_queue_pause_polling_for_managed_queues() + { + Worker::$pausable = true; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertFalse(Worker::$pausable); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + Worker::$pausable = true; + } + } + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] public function test_it_configures_managed_queue_credentials() { From 715952e4385496f31e2e5e5f34fcf963bf3e7b01 Mon Sep 17 00:00:00 2001 From: Mahfuzur Rahman Date: Mon, 27 Apr 2026 19:33:44 +0600 Subject: [PATCH 236/596] [13.x] Fix getMigrationBatches return type annotation (#59876) The `pluck('batch', 'migration')->all()` call returns an associative array keyed by migration name (string) with batch number (int) as the value. The current annotation `array[]` has the keys and values swapped and an extraneous trailing `[]`. --- .../Database/Migrations/DatabaseMigrationRepository.php | 2 +- .../Database/Migrations/MigrationRepositoryInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php b/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php index 45c4389d27d6..17ea95f5aa21 100755 --- a/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php +++ b/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php @@ -99,7 +99,7 @@ public function getLast() /** * Get the completed migrations with their batch numbers. * - * @return array[] + * @return array */ public function getMigrationBatches() { diff --git a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php index 49cc08e21a8b..491261884f11 100755 --- a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php +++ b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php @@ -37,7 +37,7 @@ public function getLast(); /** * Get the completed migrations with their batch numbers. * - * @return array[] + * @return array */ public function getMigrationBatches(); From eb0bc37d4ac863f6534dc92c0e0ab4be5919b49b Mon Sep 17 00:00:00 2001 From: Mahfuzur Rahman Date: Mon, 27 Apr 2026 19:38:14 +0600 Subject: [PATCH 237/596] [13.x] Fix PHPDoc typo in MigrationRepositoryInterface (#59875) Fix `objectt` to `object` in the `@param` annotation for the `delete` method. --- .../Database/Migrations/MigrationRepositoryInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php index 491261884f11..57c400a3e24c 100755 --- a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php +++ b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php @@ -53,7 +53,7 @@ public function log($file, $batch); /** * Remove a migration from the log. * - * @param objectt{id?: int, migration: string, batch?: int} $migration + * @param object{id?: int, migration: string, batch?: int} $migration * @return void */ public function delete($migration); From 74f1c057b719e9a2b90c5fb0c0fbf9d57e857449 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 27 Apr 2026 21:42:10 +0800 Subject: [PATCH 238/596] [13.x] Add UnitEnum support to Cache Repository touch method (#59864) * [13.x] Add UnitEnum support to Cache Repository touch method * Allow UnitEnum keys in Cache::touch docblock --- src/Illuminate/Cache/Repository.php | 4 +++- src/Illuminate/Support/Facades/Cache.php | 2 +- tests/Cache/CacheRepositoryTest.php | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 229caa6517fa..35781779921a 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -662,12 +662,14 @@ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = fal /** * Set the expiration of a cached item. * - * @param string $key + * @param \UnitEnum|string $key * @param \DateTimeInterface|\DateInterval|int $ttl * @return bool */ public function touch($key, $ttl) { + $key = enum_value($key); + return $this->store->touch($this->itemKey($key), $this->getSeconds($ttl)); } diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 1aee186ee638..128d6b50fa3d 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -42,7 +42,7 @@ * @method static mixed sear(\UnitEnum|string $key, \Closure $callback) * @method static mixed rememberForever(\UnitEnum|string $key, \Closure $callback) * @method static mixed flexible(\UnitEnum|string $key, array $ttl, callable $callback, array|null $lock = null, bool $alwaysDefer = false) - * @method static bool touch(string $key, \DateTimeInterface|\DateInterval|int $ttl) + * @method static bool touch(\UnitEnum|string $key, \DateTimeInterface|\DateInterval|int $ttl) * @method static mixed withoutOverlapping(\UnitEnum|string $key, callable $callback, int $lockFor = 0, int $waitFor = 10, string|null $owner = null) * @method static \Illuminate\Cache\Limiters\ConcurrencyLimiterBuilder funnel(\UnitEnum|string $name) * @method static bool forget(\UnitEnum|array|string $key) diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index b907d5babe99..d76071a58b7e 100755 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -508,6 +508,15 @@ public function testTouchWithDateIntervalTtlCorrectlyProxiesToStore(): void $this->assertTrue($repo->touch($key, DateInterval::createFromDateString("$ttl seconds"))); } + public function testTouchWorksWithEnumKey(): void + { + $ttl = 60; + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('touch')->once()->with('foo', $ttl)->andReturn(true); + $this->assertTrue($repo->touch(TestCacheKey::FOO, $ttl)); + } + public function testAtomicExecutesCallbackAndReturnsResult() { $repo = new Repository(new ArrayStore); From 2ab5cf469e7e7448f78a603070e611f8718f4423 Mon Sep 17 00:00:00 2001 From: Maher El Gamil Date: Mon, 27 Apr 2026 16:44:06 +0300 Subject: [PATCH 239/596] [13.x] Prevent array query params from bypassing signed URL validation (#59860) * Prevent array to string conversion in signature validation * Also guard expires param against array bypass in signatureHasNotExpired --- src/Illuminate/Routing/UrlGenerator.php | 12 +++++- tests/Routing/RoutingUrlGeneratorTest.php | 47 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Routing/UrlGenerator.php b/src/Illuminate/Routing/UrlGenerator.php index 9354ee9830d5..2d156c333394 100755 --- a/src/Illuminate/Routing/UrlGenerator.php +++ b/src/Illuminate/Routing/UrlGenerator.php @@ -483,10 +483,16 @@ public function hasCorrectSignature(Request $request, $absolute = true, Closure| $keys = is_array($keys) ? $keys : [$keys]; + $signature = $request->query('signature'); + + if (! is_string($signature)) { + return false; + } + foreach ($keys as $key) { if (hash_equals( hash_hmac('sha256', $original, $key), - (string) $request->query('signature', '') + $signature )) { return true; } @@ -505,6 +511,10 @@ public function signatureHasNotExpired(Request $request) { $expires = $request->query('expires'); + if ($expires !== null && ! is_string($expires)) { + return false; + } + return ! ($expires && Carbon::now()->getTimestamp() > $expires); } diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index a93b74a22da2..d895629a3be1 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -966,6 +966,53 @@ public function testSignedUrlWithKeyResolver() $this->assertTrue($url3->hasValidSignature($secondRequest)); } + public function testSignedUrlWithArraySignatureReturnsFalseWithoutWarning() + { + $url = new UrlGenerator( + $routes = new RouteCollection, + Request::create('http://www.foo.com/') + ); + $url->setKeyResolver(function () { + return 'secret'; + }); + + $route = new Route(['GET'], 'foo', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + // ?signature[]=foo&signature[]=bar previously raised an + // "Array to string conversion" warning. + $request = Request::create('http://www.foo.com/foo?signature[]=foo&signature[]=bar'); + + set_error_handler(static function (int $errno, string $errstr) { + throw new \ErrorException($errstr, 0, $errno); + }, E_WARNING); + + try { + $this->assertFalse($url->hasValidSignature($request)); + } finally { + restore_error_handler(); + } + } + + public function testSignedUrlWithArrayExpiresReturnsFalse() + { + $url = new UrlGenerator( + new RouteCollection, + Request::create('http://www.foo.com/') + ); + $url->setKeyResolver(function () { + return 'secret'; + }); + + // ?expires[]=99999999999 is truthy but comparing timestamp > array is always + // false in PHP, so without the guard the URL would never appear expired. + $request = Request::create('http://www.foo.com/foo?expires[]=99999999999'); + + $this->assertFalse($url->signatureHasNotExpired($request)); + } + public function testMissingNamedRouteResolution() { $url = new UrlGenerator( From a2a9be1a6209e6e377d7f76d66eb50542808a419 Mon Sep 17 00:00:00 2001 From: Maher El Gamil Date: Mon, 27 Apr 2026 16:44:52 +0300 Subject: [PATCH 240/596] Add enum support to setDefaultDriver in QueueManager, LogManager, and SessionManager (#59861) --- src/Illuminate/Log/LogManager.php | 4 ++-- src/Illuminate/Queue/QueueManager.php | 4 ++-- src/Illuminate/Session/SessionManager.php | 6 +++-- tests/Log/LogManagerTest.php | 8 +++++++ tests/Queue/QueueManagerTest.php | 15 +++++++++++++ tests/Session/SessionManagerTest.php | 27 +++++++++++++++++++++++ 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 tests/Session/SessionManagerTest.php diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index f8d9ff1e8b34..059a55e2c150 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -583,12 +583,12 @@ public function getDefaultDriver() /** * Set the default log driver name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->app['config']['logging.default'] = $name; + $this->app['config']['logging.default'] = enum_value($name); } /** diff --git a/src/Illuminate/Queue/QueueManager.php b/src/Illuminate/Queue/QueueManager.php index 3a65246b5471..0cda8631b239 100755 --- a/src/Illuminate/Queue/QueueManager.php +++ b/src/Illuminate/Queue/QueueManager.php @@ -349,12 +349,12 @@ public function getDefaultDriver() /** * Set the name of the default queue connection. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->app['config']['queue.default'] = $name; + $this->app['config']['queue.default'] = enum_value($name); } /** diff --git a/src/Illuminate/Session/SessionManager.php b/src/Illuminate/Session/SessionManager.php index 0b176ef6d01a..a2ec07892f41 100755 --- a/src/Illuminate/Session/SessionManager.php +++ b/src/Illuminate/Session/SessionManager.php @@ -4,6 +4,8 @@ use Illuminate\Support\Manager; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Session\Store */ @@ -279,11 +281,11 @@ public function getDefaultDriver() /** * Set the default session driver name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ public function setDefaultDriver($name) { - $this->config->set('session.driver', $name); + $this->config->set('session.driver', enum_value($name)); } } diff --git a/tests/Log/LogManagerTest.php b/tests/Log/LogManagerTest.php index 8cf0b9391011..4ed22993962b 100755 --- a/tests/Log/LogManagerTest.php +++ b/tests/Log/LogManagerTest.php @@ -784,6 +784,14 @@ public function testLogManagerCanResolveBackedEnumDriver() $this->assertSame($logger1, $logger2); } + + public function testSetDefaultDriverAcceptsBackedEnum() + { + $manager = new LogManager($this->app); + $manager->setDefaultDriver(LogChannelName::Single); + + $this->assertSame('single', $this->app['config']['logging.default']); + } } class CustomizeFormatter diff --git a/tests/Queue/QueueManagerTest.php b/tests/Queue/QueueManagerTest.php index 639719852a49..153f97448095 100755 --- a/tests/Queue/QueueManagerTest.php +++ b/tests/Queue/QueueManagerTest.php @@ -125,6 +125,21 @@ public function testEnumConnectionCanBeChecked() $manager->connection(QueueConnectionName::Sync); $this->assertTrue($manager->connected(QueueConnectionName::Sync)); } + + public function testSetDefaultDriverAcceptsBackedEnum() + { + $app = [ + 'config' => [ + 'queue.default' => 'sync', + 'queue.connections.sync' => ['driver' => 'sync'], + ], + ]; + + $manager = new QueueManager($app); + $manager->setDefaultDriver(QueueConnectionName::Sync); + + $this->assertSame('sync', $app['config']['queue.default']); + } } enum QueueConnectionName: string diff --git a/tests/Session/SessionManagerTest.php b/tests/Session/SessionManagerTest.php new file mode 100644 index 000000000000..680a8e7b32e7 --- /dev/null +++ b/tests/Session/SessionManagerTest.php @@ -0,0 +1,27 @@ +singleton('config', fn () => new Config(['session' => ['driver' => 'array']])); + + $manager = new SessionManager($app); + $manager->setDefaultDriver(SessionDriverName::Array); + + $this->assertSame('array', $app['config']['session.driver']); + } +} + +enum SessionDriverName: string +{ + case Array = 'array'; +} From fdc1d9784d01da6f19231d3e8fc50b85e5f64841 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:45:21 +0000 Subject: [PATCH 241/596] Update facade docblocks --- src/Illuminate/Support/Facades/Log.php | 2 +- src/Illuminate/Support/Facades/Queue.php | 2 +- src/Illuminate/Support/Facades/Session.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Support/Facades/Log.php b/src/Illuminate/Support/Facades/Log.php index 923fef17d23f..4229bff55a00 100755 --- a/src/Illuminate/Support/Facades/Log.php +++ b/src/Illuminate/Support/Facades/Log.php @@ -12,7 +12,7 @@ * @method static \Illuminate\Log\LogManager withoutContext(string[]|null $keys = null) * @method static \Illuminate\Log\LogManager flushSharedContext() * @method static string|null getDefaultDriver() - * @method static void setDefaultDriver(string $name) + * @method static void setDefaultDriver(\UnitEnum|string $name) * @method static \Illuminate\Log\LogManager extend(string $driver, \Closure $callback) * @method static void forgetChannel(string|null $driver = null) * @method static array getChannels() diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 6adba39e69e6..e420c087bba6 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -24,7 +24,7 @@ * @method static void extend(string $driver, \Closure $resolver) * @method static void addConnector(string $driver, \Closure $resolver) * @method static string getDefaultDriver() - * @method static void setDefaultDriver(string $name) + * @method static void setDefaultDriver(\UnitEnum|string $name) * @method static string getName(string|null $connection = null) * @method static \Illuminate\Contracts\Foundation\Application getApplication() * @method static \Illuminate\Queue\QueueManager setApplication(\Illuminate\Contracts\Foundation\Application $app) diff --git a/src/Illuminate/Support/Facades/Session.php b/src/Illuminate/Support/Facades/Session.php index 7d23e6c2e9fd..9e9896832868 100755 --- a/src/Illuminate/Support/Facades/Session.php +++ b/src/Illuminate/Support/Facades/Session.php @@ -9,7 +9,7 @@ * @method static int defaultRouteBlockWaitSeconds() * @method static array getSessionConfig() * @method static string|null getDefaultDriver() - * @method static void setDefaultDriver(string $name) + * @method static void setDefaultDriver(\UnitEnum|string $name) * @method static mixed driver(\UnitEnum|string|null $driver = null) * @method static \Illuminate\Session\SessionManager extend(string $driver, \Closure $callback) * @method static array getDrivers() From 5fc352487e96a86d882c26cfa72d9ed56e692217 Mon Sep 17 00:00:00 2001 From: genius-asif-hub Date: Mon, 27 Apr 2026 19:15:29 +0530 Subject: [PATCH 242/596] [13.x] Add enum support to RedisManager purge method (#59857) * [13.x] Add enum support to RedisManager purge method * fix: styleci issue formatting * fix: styleci issue formatting --------- Co-authored-by: genius-asif-hub --- src/Illuminate/Redis/RedisManager.php | 4 ++-- tests/Redis/RedisManagerExtensionTest.php | 25 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index ffe5894a3988..5dfdf2afc63f 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -242,12 +242,12 @@ public function setDriver($driver) /** * Disconnect the given connection and remove from local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return void */ public function purge($name = null) { - $name = $name ?: 'default'; + $name = enum_value($name) ?: 'default'; unset($this->connections[$name]); } diff --git a/tests/Redis/RedisManagerExtensionTest.php b/tests/Redis/RedisManagerExtensionTest.php index 0b9250ff5971..d3ce84260fec 100644 --- a/tests/Redis/RedisManagerExtensionTest.php +++ b/tests/Redis/RedisManagerExtensionTest.php @@ -84,6 +84,26 @@ public function testParseConnectionConfigurationForCluster() $redis->resolve($name); } + + public function testPurgeAcceptsUnitEnum() + { + $redis = new RedisManager(new Application, 'my_custom_driver', [ + 'default' => [ + 'host' => 'some-host', + 'port' => 'some-port', + 'database' => 5, + 'timeout' => 0.5, + ], + ]); + + $property = new \ReflectionProperty($redis, 'connections'); + $property->setValue($redis, ['default' => 'fake-connection']); + + $this->assertCount(1, $redis->connections()); + + $redis->purge(FakeRedisConnectionName::Default); + $this->assertCount(0, $redis->connections()); + } } class FakeRedisConnector implements Connector @@ -113,3 +133,8 @@ public function connectToCluster(array $config, array $clusterOptions, array $op return 'my-redis-cluster-connection'; } } + +enum FakeRedisConnectionName: string +{ + case Default = 'default'; +} From 6d2f7192615e58b9a7918799755761acf196972f Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:46:05 +0000 Subject: [PATCH 243/596] Update facade docblocks --- src/Illuminate/Support/Facades/Redis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Redis.php b/src/Illuminate/Support/Facades/Redis.php index 044447e1b04e..6b67a0953950 100755 --- a/src/Illuminate/Support/Facades/Redis.php +++ b/src/Illuminate/Support/Facades/Redis.php @@ -9,7 +9,7 @@ * @method static void enableEvents() * @method static void disableEvents() * @method static void setDriver(string $driver) - * @method static void purge(string|null $name = null) + * @method static void purge(\UnitEnum|string|null $name = null) * @method static \Illuminate\Redis\RedisManager extend(string $driver, \Closure $callback) * @method static void createSubscription(array|string $channels, \Closure $callback, string $method = 'subscribe') * @method static \Illuminate\Redis\Limiters\ConcurrencyLimiterBuilder funnel(string $name) From 928b0e5851bac30b8b638daf0f22923c8c0beadb Mon Sep 17 00:00:00 2001 From: Rodolphe MOUTTE Date: Mon, 27 Apr 2026 15:47:57 +0200 Subject: [PATCH 244/596] [13.x] Fix factory hasAttached method pivot JSON attribute handling (#59856) * Fix Undefined array key 0 exception when setting pivot json columns with hasAttached factory method * Replace unnecessary import with native function call --------- Co-authored-by: Rodolphe Moutte --- .../Database/Eloquent/Factories/Factory.php | 2 +- .../Database/DatabaseEloquentFactoryTest.php | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index e0955ac85181..b2dcba24a34c 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -724,7 +724,7 @@ protected function guessRelationship(string $related) */ public function hasAttached($factory, $pivot = [], $relationship = null) { - if (is_array($pivot) && $pivot !== [] && array_all($pivot, fn ($p) => is_array($p))) { + if (is_array($pivot) && $pivot !== [] && array_is_list($pivot) && array_all($pivot, fn ($p) => is_array($p))) { $factory = $factory instanceof Factory && $factory->count === null ? $factory->count(count($pivot)) : $factory; diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index d1bc9bd7f45f..dd5177c1b665 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Database\Eloquent\Model as Eloquent; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Str; @@ -90,6 +91,7 @@ public function createSchema() $table->foreignId('role_id'); $table->foreignId('user_id'); $table->string('admin')->default('N'); + $table->json('meta')->nullable(); }); } @@ -556,15 +558,26 @@ public function test_belongs_to_many_relationship_with_existing_model_instances_ unset($_SERVER['__test.role.creating-role']); } + public function test_belongs_to_many_relationship_with_pivot_json_column() + { + $user = FactoryTestUserFactory::new() + ->hasAttached(FactoryTestRoleFactory::new(), ['meta' => ['foo' => 'bar']]) + ->create(); + + $this->assertCount(1, $user->factoryTestRoles); + $this->assertSame(['foo' => 'bar'], $user->factoryTestRoles[0]->pivot->meta); + } + public function test_belongs_to_many_relationship_with_pivot_arrays() { $user = FactoryTestUserFactory::new() - ->hasAttached(FactoryTestRoleFactory::new(), [['admin' => 'Y'], ['admin' => 'N']]) + ->hasAttached(FactoryTestRoleFactory::new(), [['admin' => 'Y'], ['admin' => 'N', 'meta' => ['foo' => 'bar']]]) ->create(); $this->assertCount(2, $user->factoryTestRoles); $this->assertSame('Y', $user->factoryTestRoles[0]->pivot->admin); $this->assertSame('N', $user->factoryTestRoles[1]->pivot->admin); + $this->assertSame(['foo' => 'bar'], $user->factoryTestRoles[1]->pivot->meta); } public function test_sequences() @@ -1194,7 +1207,7 @@ public function rolesWithFooBarBazAsName() public function factoryTestRoles() { - return $this->belongsToMany(FactoryTestRole::class, 'role_user', 'user_id', 'role_id')->withPivot('admin'); + return $this->belongsToMany(FactoryTestRole::class, 'role_user', 'user_id', 'role_id')->using(FactoryTestUserRolePivot::class)->withPivot(['admin', 'meta']); } } @@ -1356,6 +1369,15 @@ protected function casts() } } +class FactoryTestUserRolePivot extends Pivot +{ + protected $table = 'role_user'; + + public $timestamps = false; + + protected $casts = ['meta' => 'array']; +} + class FactoryTestUserWithArrayFactory extends Factory { protected $model = FactoryTestUserWithArray::class; From c500906471ad6b05212c1c30e10a603723d23f21 Mon Sep 17 00:00:00 2001 From: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:59:53 +0600 Subject: [PATCH 245/596] [13.x] Implement CanFlushLocks on NullStore and MemoizedStore (#59850) * [13.x] Implement CanFlushLocks on NullStore and MemoizedStore * Remove superfluous @return bool PHPDoc tags from native typed methods * Fix brace position style in anonymous class stub * Use Mockery instead of anonymous class stub in MemoizedStore test --- src/Illuminate/Cache/MemoizedStore.php | 30 ++++++++++++++++++++++-- src/Illuminate/Cache/NullStore.php | 19 ++++++++++++++- tests/Cache/CacheMemoizedStoreTest.php | 32 ++++++++++++++++++++++++++ tests/Cache/CacheNullStoreTest.php | 12 ++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Cache/MemoizedStore.php b/src/Illuminate/Cache/MemoizedStore.php index de41c015cf59..e802de7bd695 100644 --- a/src/Illuminate/Cache/MemoizedStore.php +++ b/src/Illuminate/Cache/MemoizedStore.php @@ -3,10 +3,11 @@ namespace Illuminate\Cache; use BadMethodCallException; +use Illuminate\Contracts\Cache\CanFlushLocks; use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Store; -class MemoizedStore implements LockProvider, Store +class MemoizedStore implements CanFlushLocks, LockProvider, Store { /** * The memoized cache values. @@ -105,7 +106,6 @@ public function put($key, $value, $seconds) /** * Store multiple items in the cache for a given number of seconds. * - * @param array $values * @param int $seconds * @return bool */ @@ -197,6 +197,32 @@ public function restoreLock($name, $owner) return $this->repository->getStore()->restoreLock(...func_get_args()); } + /** + * Flush all locks managed by the store. + * + * @throws \BadMethodCallException + */ + public function flushLocks(): bool + { + $store = $this->repository->getStore(); + + if (! $store instanceof CanFlushLocks) { + throw new BadMethodCallException('This cache store does not support flushing locks.'); + } + + return $store->flushLocks(); + } + + /** + * Determine if the lock store is separate from the cache store. + */ + public function hasSeparateLockStore(): bool + { + $store = $this->repository->getStore(); + + return $store instanceof CanFlushLocks && $store->hasSeparateLockStore(); + } + /** * Adjust the expiration time of a cached item. * diff --git a/src/Illuminate/Cache/NullStore.php b/src/Illuminate/Cache/NullStore.php index e00470159bb6..89f4b37c4c7e 100755 --- a/src/Illuminate/Cache/NullStore.php +++ b/src/Illuminate/Cache/NullStore.php @@ -2,9 +2,10 @@ namespace Illuminate\Cache; +use Illuminate\Contracts\Cache\CanFlushLocks; use Illuminate\Contracts\Cache\LockProvider; -class NullStore extends TaggableStore implements LockProvider +class NullStore extends TaggableStore implements CanFlushLocks, LockProvider { use RetrievesMultipleKeys; @@ -93,6 +94,22 @@ public function restoreLock($name, $owner) return $this->lock($name, 0, $owner); } + /** + * Flush all locks managed by the store. + */ + public function flushLocks(): bool + { + return true; + } + + /** + * Determine if the lock store is separate from the cache store. + */ + public function hasSeparateLockStore(): bool + { + return false; + } + /** * Adjust the expiration time of a cached item. * diff --git a/tests/Cache/CacheMemoizedStoreTest.php b/tests/Cache/CacheMemoizedStoreTest.php index 00b14c9f111b..d0f2f74ea9f6 100644 --- a/tests/Cache/CacheMemoizedStoreTest.php +++ b/tests/Cache/CacheMemoizedStoreTest.php @@ -2,10 +2,14 @@ namespace Illuminate\Tests\Cache; +use BadMethodCallException; use Illuminate\Cache\ArrayStore; use Illuminate\Cache\MemoizedStore; +use Illuminate\Cache\NullStore; use Illuminate\Cache\Repository; +use Illuminate\Contracts\Cache\Store; use Illuminate\Support\Carbon; +use Mockery as m; use PHPUnit\Framework\TestCase; class CacheMemoizedStoreTest extends TestCase @@ -23,4 +27,32 @@ public function testTouchExtendsTtl(): void $this->assertSame('bar', $store->get('foo')); } + + public function testLocksCanBeFlushedWhenUnderlyingStoreSupportsIt(): void + { + $store = new MemoizedStore('test', new Repository(new ArrayStore)); + $this->assertTrue($store->flushLocks()); + } + + public function testFlushLocksThrowsWhenUnderlyingStoreDoesNotSupportIt(): void + { + $this->expectException(BadMethodCallException::class); + + $stub = m::mock(Store::class); + (new MemoizedStore('test', new Repository($stub)))->flushLocks(); + } + + protected function tearDown(): void + { + m::close(); + } + + public function testHasSeparateLockStoreDelegatestoUnderlyingStore(): void + { + $withSeparate = new MemoizedStore('test', new Repository(new ArrayStore)); + $this->assertTrue($withSeparate->hasSeparateLockStore()); + + $withoutSeparate = new MemoizedStore('test', new Repository(new NullStore)); + $this->assertFalse($withoutSeparate->hasSeparateLockStore()); + } } diff --git a/tests/Cache/CacheNullStoreTest.php b/tests/Cache/CacheNullStoreTest.php index f30bedce39d0..1f9e2fbd5f26 100644 --- a/tests/Cache/CacheNullStoreTest.php +++ b/tests/Cache/CacheNullStoreTest.php @@ -38,4 +38,16 @@ public function testTouchReturnsFalse(): void { $this->assertFalse((new NullStore)->touch('foo', 30)); } + + public function testLocksCanBeFlushed(): void + { + $store = new NullStore; + $this->assertTrue($store->flushLocks()); + } + + public function testHasSeparateLockStore(): void + { + $store = new NullStore; + $this->assertFalse($store->hasSeparateLockStore()); + } } From 6e1783be15b16547ba78715b6cdeb1ebb3e39743 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 27 Apr 2026 15:00:47 +0100 Subject: [PATCH 246/596] [13.x] Introduce WorkerInterrupted event (#59848) * create the event * add new params, nullable ofc and dispatch! * add test to some degree * Update WorkerInterrupted.php * drop the test, not feeling it --- .../Queue/Events/WorkerInterrupted.php | 20 +++++++++++++++++++ src/Illuminate/Queue/Worker.php | 11 +++++++--- tests/Queue/QueueWorkerTest.php | 1 + 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/Illuminate/Queue/Events/WorkerInterrupted.php diff --git a/src/Illuminate/Queue/Events/WorkerInterrupted.php b/src/Illuminate/Queue/Events/WorkerInterrupted.php new file mode 100644 index 000000000000..474f9f79c79e --- /dev/null +++ b/src/Illuminate/Queue/Events/WorkerInterrupted.php @@ -0,0 +1,20 @@ +supportsAsyncSignals()) { - $this->listenForSignals(); + $this->listenForSignals($connectionName, $queue); } $lastRestart = $this->getTimestampOfLastQueueRestart(); @@ -814,16 +815,20 @@ protected function getTimestampOfLastQueueRestart() /** * Enable async signals for the process. * + * @param string|null $connectionName + * @param string|null $queue * @return void */ - protected function listenForSignals() + protected function listenForSignals($connectionName = null, $queue = null) { pcntl_async_signals(true); foreach ([SIGQUIT, SIGTERM, SIGINT] as $signal) { - pcntl_signal($signal, function (int $signal) { + pcntl_signal($signal, function (int $signal) use ($connectionName, $queue) { $this->shouldQuit = true; + $this->events->dispatch(new WorkerInterrupted($signal, $connectionName, $queue)); + $this->notifyJobOfSignal($signal); }); } diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index c96d83db3659..e3235845d0cd 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -581,6 +581,7 @@ public function sleep($seconds) $this->sleptFor = $seconds; } + public function notifyJobOfSignal(int $signal): void { parent::notifyJobOfSignal($signal); From f2434f0046a371dfb45ad9cdf69752eb53026105 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 27 Apr 2026 14:01:21 +0000 Subject: [PATCH 247/596] Apply fixes from StyleCI --- tests/Queue/QueueWorkerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index e3235845d0cd..c96d83db3659 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -581,7 +581,6 @@ public function sleep($seconds) $this->sleptFor = $seconds; } - public function notifyJobOfSignal(int $signal): void { parent::notifyJobOfSignal($signal); From 03831ebb63ca49883bfd651c7b80fc1608d1d0c7 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:25:58 +0600 Subject: [PATCH 248/596] [13.x] Fix MigrationRepositoryInterface return type docblocks (object vs array) (#59887) --- .../Database/Migrations/DatabaseMigrationRepository.php | 6 +++--- .../Database/Migrations/MigrationRepositoryInterface.php | 6 +++--- src/Illuminate/Database/Migrations/Migrator.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php b/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php index 17ea95f5aa21..22fd6ff76974 100755 --- a/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php +++ b/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php @@ -56,7 +56,7 @@ public function getRan() * Get the list of migrations. * * @param int $steps - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ public function getMigrations($steps) { @@ -73,7 +73,7 @@ public function getMigrations($steps) * Get the list of the migrations by batch number. * * @param int $batch - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ public function getMigrationsByBatch($batch) { @@ -87,7 +87,7 @@ public function getMigrationsByBatch($batch) /** * Get the last migration batch. * - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ public function getLast() { diff --git a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php index 57c400a3e24c..2e9edb5d516b 100755 --- a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php +++ b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php @@ -15,7 +15,7 @@ public function getRan(); * Get the list of migrations. * * @param int $steps - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ public function getMigrations($steps); @@ -23,14 +23,14 @@ public function getMigrations($steps); * Get the list of the migrations by batch. * * @param int $batch - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ public function getMigrationsByBatch($batch); /** * Get the last migration batch. * - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ public function getLast(); diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 47dc82916034..197799390c49 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -290,7 +290,7 @@ public function rollback($paths = [], array $options = []) * Get the migrations for a rollback operation. * * @param array $options - * @return array{id: int, migration: string, batch: int}[] + * @return object{id: int, migration: string, batch: int}[] */ protected function getMigrationsForRollback(array $options) { From b940896fd10cd59715f15e0aaeff505497fe0679 Mon Sep 17 00:00:00 2001 From: lorenzolosa <11164571+lorenzolosa@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:16:57 +0200 Subject: [PATCH 249/596] int argument for Collection::sortBy() (#59894) --- src/Illuminate/Collections/Collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 9ff0b9a6c520..d35a2a4e4880 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -1575,7 +1575,7 @@ public function sortDesc($options = SORT_REGULAR) /** * Sort the collection using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string $callback + * @param array|(callable(TValue, TKey): mixed)|string|int $callback * @param int $options * @param bool $descending * @return static From 299d1780e5b6c5ff45b482535a328785c0829182 Mon Sep 17 00:00:00 2001 From: Maher El Gamil Date: Tue, 28 Apr 2026 14:01:18 +0300 Subject: [PATCH 250/596] Add detailed @return shape to Schema\\Builder::getForeignKeys (#59903) --- src/Illuminate/Database/Schema/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 180e6477438c..810ee5dc70de 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -469,7 +469,7 @@ public function hasIndex($table, $index, $type = null) * Get the foreign keys for a given table. * * @param string $table - * @return array + * @return list, foreign_schema: string|null, foreign_table: string, foreign_columns: list, on_update: string|null, on_delete: string|null}> */ public function getForeignKeys($table) { From 68b3a39b5e75fb19e3dbefbd3fa282dc46ead24d Mon Sep 17 00:00:00 2001 From: Maher El Gamil Date: Tue, 28 Apr 2026 14:01:33 +0300 Subject: [PATCH 251/596] Fix EloquentModelDecimalCastingTest expectation across brick/math versions (#59904) --- tests/Integration/Database/EloquentModelDecimalCastingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Database/EloquentModelDecimalCastingTest.php b/tests/Integration/Database/EloquentModelDecimalCastingTest.php index 105b328b0020..beef17c6e2dc 100644 --- a/tests/Integration/Database/EloquentModelDecimalCastingTest.php +++ b/tests/Integration/Database/EloquentModelDecimalCastingTest.php @@ -71,7 +71,7 @@ public function testItWrapsThrownExceptions() } catch (MathException $e) { $this->assertSame('Unable to cast value to a decimal.', $e->getMessage()); $this->assertInstanceOf(NumberFormatException::class, $e->getPrevious()); - $this->assertSame('The given value "foo" does not represent a valid number.', $e->getPrevious()->getMessage()); + $this->assertStringContainsString('"foo" does not represent a valid number.', $e->getPrevious()->getMessage()); } } From f55739c9447803072c3f36ffe924814affcba49a Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:04:41 +0600 Subject: [PATCH 252/596] [13.x] Correct Lock getCurrentOwner @return type to string|null (#59890) --- src/Illuminate/Cache/Lock.php | 2 +- src/Illuminate/Cache/RedisLock.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Cache/Lock.php b/src/Illuminate/Cache/Lock.php index c263d61d8026..54367c4a0e04 100644 --- a/src/Illuminate/Cache/Lock.php +++ b/src/Illuminate/Cache/Lock.php @@ -76,7 +76,7 @@ abstract public function release(); /** * Returns the owner value written into the driver for this lock. * - * @return string + * @return string|null */ abstract protected function getCurrentOwner(); diff --git a/src/Illuminate/Cache/RedisLock.php b/src/Illuminate/Cache/RedisLock.php index d28490fac737..7e87c3756990 100644 --- a/src/Illuminate/Cache/RedisLock.php +++ b/src/Illuminate/Cache/RedisLock.php @@ -63,7 +63,7 @@ public function forceRelease() /** * Returns the owner value written into the driver for this lock. * - * @return string + * @return string|null */ protected function getCurrentOwner() { From 3c5cecce43de3c7521dcacd57bc2802ffbd5a258 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:04:57 +0600 Subject: [PATCH 253/596] [13.x] Correct Batch fresh and add @return to self|null (#59891) --- src/Illuminate/Bus/Batch.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Bus/Batch.php b/src/Illuminate/Bus/Batch.php index 6d9f5846dc01..a37e8c20a679 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -137,7 +137,7 @@ public function __construct( /** * Get a fresh instance of the batch represented by this ID. * - * @return self + * @return self|null */ public function fresh() { @@ -148,7 +148,7 @@ public function fresh() * Add additional jobs to the batch. * * @param \Illuminate\Support\Enumerable|object|array $jobs - * @return self + * @return self|null */ public function add($jobs) { From 0d00add267882adbbb184e908473b620f310fbb7 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:05:44 +0600 Subject: [PATCH 254/596] [13.x] Align Mailable::cc @return with sibling fluent methods (#59892) --- src/Illuminate/Contracts/Mail/Mailable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Contracts/Mail/Mailable.php b/src/Illuminate/Contracts/Mail/Mailable.php index b7fdd42efebd..a60ecc8b3fe6 100644 --- a/src/Illuminate/Contracts/Mail/Mailable.php +++ b/src/Illuminate/Contracts/Mail/Mailable.php @@ -36,7 +36,7 @@ public function later($delay, Queue $queue); * * @param object|array|string $address * @param string|null $name - * @return self + * @return $this */ public function cc($address, $name = null); From 92cf2def4532d3f6f20a1bfb3b6b56972276c062 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:16:32 +0200 Subject: [PATCH 255/596] [13.x] Add support for `SortDirection` enum to collections and Arr (#59859) * Add PHP 8.6 polyfill to Collection * Update Collection methods to support SortDirection enum * Update Arr methods to support SortDirection enum --- composer.json | 1 + src/Illuminate/Collections/Arr.php | 21 +++++++++------- src/Illuminate/Collections/Collection.php | 29 +++++++++++++++-------- src/Illuminate/Collections/composer.json | 3 ++- tests/Support/SupportCollectionTest.php | 19 +++++++++++++++ 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index c991f797152f..0f08779da24b 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "symfony/mime": "^7.4.0 || ^8.0.0", "symfony/polyfill-php84": "^1.33", "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php86": "^1.36", "symfony/process": "^7.4.5 || ^8.0.5", "symfony/routing": "^7.4.0 || ^8.0.0", "symfony/uid": "^7.4.0 || ^8.0.0", diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index c064bf729469..545f865643be 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -11,6 +11,7 @@ use InvalidArgumentException; use JsonSerializable; use Random\Randomizer; +use SortDirection; use Traversable; use WeakMap; @@ -1091,7 +1092,7 @@ public static function sole($array, ?callable $callback = null) * @template TValue * * @param iterable $array - * @param callable|string|null|array $callback + * @param callable|string|null|array $callback * @return array */ public static function sort($array, $callback = null) @@ -1122,7 +1123,7 @@ public static function sortDesc($array, $callback = null) * * @param array $array * @param int-mask-of $options - * @param bool $descending + * @param SortDirection|bool $descending * @return array */ public static function sortRecursive($array, $options = SORT_REGULAR, $descending = false) @@ -1134,13 +1135,15 @@ public static function sortRecursive($array, $options = SORT_REGULAR, $descendin } if (! array_is_list($array)) { - $descending - ? krsort($array, $options) - : ksort($array, $options); + match ($descending) { + false, SortDirection::Ascending => ksort($array, $options), + true, SortDirection::Descending => krsort($array, $options), + }; } else { - $descending - ? rsort($array, $options) - : sort($array, $options); + match ($descending) { + false, SortDirection::Ascending => sort($array, $options), + true, SortDirection::Descending => rsort($array, $options), + }; } return $array; @@ -1158,7 +1161,7 @@ public static function sortRecursive($array, $options = SORT_REGULAR, $descendin */ public static function sortRecursiveDesc($array, $options = SORT_REGULAR) { - return static::sortRecursive($array, $options, true); + return static::sortRecursive($array, $options, SortDirection::Descending); } /** diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index d35a2a4e4880..f647f82e1f70 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -9,6 +9,7 @@ use Illuminate\Support\Traits\Macroable; use Illuminate\Support\Traits\TransformsToResourceCollection; use InvalidArgumentException; +use SortDirection; use stdClass; use Traversable; @@ -1577,7 +1578,7 @@ public function sortDesc($options = SORT_REGULAR) * * @param array|(callable(TValue, TKey): mixed)|string|int $callback * @param int $options - * @param bool $descending + * @param SortDirection|bool $descending * @return static */ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) @@ -1597,8 +1598,10 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) $results[$key] = $callback($value, $key); } - $descending ? arsort($results, $options) - : asort($results, $options); + match ($descending) { + false, SortDirection::Ascending => asort($results, $options), + true, SortDirection::Descending => arsort($results, $options), + }; // Once we have sorted all of the keys in the array, we will loop through them // and grab the corresponding model so we can set the underlying items list @@ -1627,15 +1630,18 @@ protected function sortByMany(array $comparisons = [], int $options = SORT_REGUL $prop = $comparison[0]; - $ascending = Arr::get($comparison, 1, true) === true || - Arr::get($comparison, 1, true) === 'asc'; + $direction = match (Arr::get($comparison, 1, true)) { + true, 'asc', SortDirection::Ascending => SortDirection::Ascending, + false, 'desc', SortDirection::Descending => SortDirection::Descending, + default => SortDirection::Descending, // for backwards compatibility + }; if (! is_string($prop) && is_callable($prop)) { $result = $prop($a, $b); } else { $values = [data_get($a, $prop), data_get($b, $prop)]; - if (! $ascending) { + if ($direction === SortDirection::Descending) { $values = array_reverse($values); } @@ -1680,7 +1686,7 @@ public function sortByDesc($callback, $options = SORT_REGULAR) foreach ($callback as $index => $key) { $comparison = Arr::wrap($key); - $comparison[1] = 'desc'; + $comparison[1] = SortDirection::Descending; $callback[$index] = $comparison; } @@ -1693,14 +1699,17 @@ public function sortByDesc($callback, $options = SORT_REGULAR) * Sort the collection keys. * * @param int $options - * @param bool $descending + * @param SortDirection|bool $descending * @return static */ public function sortKeys($options = SORT_REGULAR, $descending = false) { $items = $this->items; - $descending ? krsort($items, $options) : ksort($items, $options); + match ($descending) { + false, SortDirection::Ascending => ksort($items, $options), + true, SortDirection::Descending => krsort($items, $options), + }; return $this->newInstance($items); } @@ -1713,7 +1722,7 @@ public function sortKeys($options = SORT_REGULAR, $descending = false) */ public function sortKeysDesc($options = SORT_REGULAR) { - return $this->sortKeys($options, true); + return $this->sortKeys($options, SortDirection::Descending); } /** diff --git a/src/Illuminate/Collections/composer.json b/src/Illuminate/Collections/composer.json index 512cf07b004e..1f5571a6f3a9 100644 --- a/src/Illuminate/Collections/composer.json +++ b/src/Illuminate/Collections/composer.json @@ -19,7 +19,8 @@ "illuminate/contracts": "^13.0", "illuminate/macroable": "^13.0", "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33" + "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php86": "^1.36" }, "suggest": { "illuminate/http": "Required to convert collections to API resources (^13.0).", diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 98330e3aab63..abc13cb0bf5c 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -23,6 +23,7 @@ use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use ReflectionClass; +use SortDirection; use stdClass; use Symfony\Component\VarDumper\VarDumper; use UnexpectedValueException; @@ -2063,6 +2064,16 @@ public function testSortBy($collection) $this->assertEquals(['dayle', 'taylor'], array_values($data->all())); + $data = new $collection(['dayle', 'taylor']); + $data = $data->sortBy( + function ($x) { + return $x; + }, + SORT_REGULAR, + SortDirection::Descending); + + $this->assertEquals(['taylor', 'dayle'], array_values($data->all())); + $data = new $collection(['dayle', 'taylor']); $data = $data->sortByDesc(function ($x) { return $x; @@ -2153,6 +2164,14 @@ public function testSortByMany($collection) $data = $data->sortBy([['item', 'desc']]); $this->assertEquals($data->pluck('item')->toArray(), $expected); + rsort($expected); + $data = $data->sortBy([['item', false]]); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + rsort($expected); + $data = $data->sortBy([['item', SortDirection::Descending]]); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + sort($expected, SORT_STRING); $data = $data->sortBy(['item'], SORT_STRING); $this->assertEquals($data->pluck('item')->toArray(), $expected); From ca0263c7c5c753b4468628ecb656ceb5f6c610af Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Tue, 28 Apr 2026 12:51:23 +0100 Subject: [PATCH 256/596] Add @fonts Blade directive and Vite font optimization runtime (#59584) * Add @fonts Blade directive and Vite::fonts() runtime Consume the font manifest written by the Vite plugin to render preload links and inline font CSS. The directive and facade method support optional family filtering, CSP nonces, preload attribute customization, and both build and hot mode. ViteFonts handles manifest reading and validation while Vite owns rendering and preload recording, keeping the public API surface narrow. * Fix font optimization contract and API consistency - Filter preloads by requested families before validation so a malformed entry for an unrequested family does not throw - Filter CSS variables block alongside familyStyles so filtered output only emits variables for the requested families - Pass 'fonts' instead of null to preload attribute resolvers for consistent callback shape with existing Vite preloads - Remove unused public buildDirectory() getter and facade annotation to keep the API surface minimal * Add stricter PHPStan array types to font optimization docblocks * Add tests for font utility class rendering through ViteFonts pipeline Verifies that .font-* CSS utility classes generated by the Vite plugin flow through the framework's familyStyles rendering in build mode, hot mode, filtered-by-family, and with custom variable names. * Update font runtime to use alias-based manifest keys Align Vite::fonts() and ViteFonts with the vite-plugin's new alias-keyed manifest structure. Preloads and family entries are now filtered by alias instead of family name, and validation messages reference aliases accordingly. * Apply fixes from StyleCI * Parse font CSS variables structurally instead of line-by-line The previous filterVariables() implementation split on newlines and matched variables by string containment, coupling it to the plugin's multiline formatting. If the :root block were emitted on a single line or minified, filtering would leak unrelated variables. Now parses the :root block with regex to extract declarations regardless of whitespace, making it resilient to formatting changes. * Apply fixes from StyleCI * Address code review follow-ups on font optimization runtime * Simplify fonts() alias gate and type ViteFonts parameters * fix facade param name * remove dead null check * use null coalesce * Render readable font HTML and harden manifest style contract - Render each preload link on its own line, wrap the inline style tag on newlines, and separate the preload block from the style block so the rendered font HTML stays readable. - Throw a descriptive ViteException when style.familyStyles or style.variables is not an alias-keyed array, replacing the cryptic TypeError users hit when a newer framework reads an older plugin's manifest. - Update existing rendering assertions to the new format and add regression tests for the readable layout and the contract error. * formatting * fix method --------- Co-authored-by: StyleCI Bot Co-authored-by: Joe Tannenbaum Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Vite.php | 172 +++ src/Illuminate/Foundation/ViteFonts.php | 249 ++++ src/Illuminate/Support/Facades/Vite.php | 2 + .../Compilers/Concerns/CompilesHelpers.php | 15 + tests/Foundation/FoundationViteFontsTest.php | 1288 +++++++++++++++++ tests/Http/Middleware/VitePreloadingTest.php | 88 ++ tests/View/Blade/BladeHelpersTest.php | 4 + 7 files changed, 1818 insertions(+) create mode 100644 src/Illuminate/Foundation/ViteFonts.php create mode 100644 tests/Foundation/FoundationViteFontsTest.php diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 855aa73992e4..900864368eca 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -97,6 +97,20 @@ class Vite implements Htmlable */ protected static $manifests = []; + /** + * The ViteFonts instance. + * + * @var \Illuminate\Foundation\ViteFonts|null + */ + protected $fonts = null; + + /** + * The name of the font manifest file. + * + * @var string + */ + protected $fontsManifestFilename = 'fonts-manifest.json'; + /** * The prefetching strategy to use. * @@ -1045,6 +1059,160 @@ protected function nonceAttribute() return new HtmlString(' nonce="'.$this->cspNonce().'"'); } + /** + * Render font preload links and inline styles. + * + * @param list|string|null $aliases + * @return \Illuminate\Support\HtmlString + * + * @throws \Illuminate\Foundation\ViteException + */ + public function fonts($aliases = null) + { + $isHot = $this->isRunningHot(); + + $fonts = $this->viteFonts(); + + $manifest = $fonts->manifest($isHot, $this->buildDirectory, $this->fontsManifestFilename, $this->hotFile()); + + if ($manifest === null) { + return new HtmlString(''); + } + + $fonts->ensureValidManifest($manifest); + + $preloads = $manifest['preloads'] ?? []; + + if ($aliases !== null) { + $aliases = is_string($aliases) + ? [$aliases] + : $aliases; + + $fonts->ensureValidFamilies($aliases, $manifest); + + $preloads = array_filter($preloads, fn ($preload) => in_array($preload['alias'] ?? null, $aliases, true)); + } + + $fonts->ensureValidPreloads($preloads, $isHot); + + $preloadsHtml = $this->renderFontPreloads($preloads); + $styleHtml = $this->renderFontStyle($manifest, $aliases); + + return new HtmlString(match (true) { + $preloadsHtml !== '' && $styleHtml !== '' => $preloadsHtml."\n".$styleHtml, + default => $preloadsHtml.$styleHtml, + }); + } + + /** + * Render preload link tags for font entries. + * + * @param list> $preloads + * @return string + */ + protected function renderFontPreloads($preloads) + { + $tags = []; + + foreach ($preloads as $preload) { + $url = $preload['url'] ?? $this->assetPath($this->buildDirectory.'/'.$preload['file']); + + if (isset($this->preloadedAssets[$url])) { + continue; + } + + $attributes = $this->resolveFontPreloadAttributes($url, $preload); + + if ($attributes === false) { + continue; + } + + $this->preloadedAssets[$url] = $this->parseAttributes( + (new Collection($attributes))->forget('href')->all() + ); + + $tags[] = 'parseAttributes($attributes)).' />'; + } + + return implode("\n", $tags); + } + + /** + * Resolve the attributes for a font preload tag. + * + * @param string $url + * @param array $preload + * @return array|false + */ + protected function resolveFontPreloadAttributes($url, $preload) + { + $attributes = [ + 'rel' => 'preload', + 'as' => $preload['as'] ?? 'font', + 'href' => $url, + 'type' => $preload['type'] ?? false, + 'crossorigin' => $preload['crossorigin'] ?? false, + 'nonce' => $this->nonce ?? false, + ]; + + foreach ($this->preloadTagAttributesResolvers as $resolver) { + if (false === ($resolved = $resolver('fonts', $url, [], []))) { + return false; + } + + $attributes = array_merge($attributes, $resolved); + } + + return $attributes; + } + + /** + * Render the inline style block for the font manifest. + * + * @param array $manifest + * @param list|null $aliases + * @return string + */ + protected function renderFontStyle($manifest, $aliases) + { + $css = $this->viteFonts()->resolveStyleContent($manifest, $aliases, $this->buildDirectory); + + if ($css === '') { + return ''; + } + + $attributes = $this->parseAttributes([ + 'nonce' => $this->nonce ?? false, + ]); + + $attributeString = $attributes ? ' '.implode(' ', $attributes) : ''; + + return "\n".trim($css, "\n")."\n"; + } + + /** + * Get the ViteFonts instance. + * + * @return \Illuminate\Foundation\ViteFonts + */ + protected function viteFonts() + { + return $this->fonts ??= new ViteFonts; + } + + /** + * Set the font manifest filename. + * + * @param string $filename + * @return $this + */ + public function useFontsManifestFilename($filename) + { + $this->fontsManifestFilename = $filename; + + return $this; + } + /** * Determine if the HMR server is running. * @@ -1073,5 +1241,9 @@ public function toHtml() public function flush() { $this->preloadedAssets = []; + + $this->fonts?->flush(); + + $this->fonts = null; } } diff --git a/src/Illuminate/Foundation/ViteFonts.php b/src/Illuminate/Foundation/ViteFonts.php new file mode 100644 index 000000000000..5a0022b877ba --- /dev/null +++ b/src/Illuminate/Foundation/ViteFonts.php @@ -0,0 +1,249 @@ +> + */ + protected static $manifests = []; + + /** + * Read the font manifest for the given configuration. + * + * @param bool $isHot + * @param string $buildDirectory + * @param string $manifestFilename + * @param string $hotFile + * @return array|null + * + * @throws \Illuminate\Foundation\ViteException + */ + public function manifest(bool $isHot, string $buildDirectory, string $manifestFilename, string $hotFile) + { + $path = $isHot + ? dirname($hotFile).'/fonts-manifest.dev.json' + : public_path($buildDirectory.'/'.$manifestFilename); + + return $this->readManifest($path); + } + + /** + * Read and decode a manifest file. + * + * @param string $path + * @return array|null + * + * @throws \Illuminate\Foundation\ViteException + */ + protected function readManifest(string $path) + { + if (isset(static::$manifests[$path])) { + return static::$manifests[$path]; + } + + if (! is_file($path)) { + return null; + } + + $contents = file_get_contents($path); + + $manifest = json_decode($contents, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new ViteException("The font manifest at [{$path}] is not valid JSON."); + } + + return static::$manifests[$path] = $manifest; + } + + /** + * Resolve the CSS content from the manifest. + * + * @param array $manifest + * @param list|null $aliases + * @param string $buildDirectory + * @return string + * + * @throws \Illuminate\Foundation\ViteException + */ + public function resolveStyleContent(array $manifest, ?array $aliases, string $buildDirectory) + { + $style = $manifest['style'] ?? null; + + return match (true) { + $style === null => '', + $aliases !== null => $this->resolveFilteredStyleContent($style, $aliases), + isset($style['inline']) => $style['inline'], + isset($style['file']) => $this->readStyleFile($buildDirectory, $style['file']), + default => '', + }; + } + + /** + * Resolve filtered CSS content using per-alias fragments from the manifest. + * + * @param array{inline?: string, file?: string, familyStyles?: array, variables?: array} $style + * @param list $aliases + * @return string + * + * @throws \Illuminate\Foundation\ViteException + */ + protected function resolveFilteredStyleContent(array $style, array $aliases) + { + $familyStyles = $style['familyStyles'] ?? []; + $variables = $style['variables'] ?? []; + + if (! is_array($familyStyles)) { + throw new ViteException( + 'The font manifest [style.familyStyles] must be an object keyed by alias; the manifest was likely produced by an incompatible plugin version.' + ); + } + + if (! is_array($variables)) { + throw new ViteException( + 'The font manifest [style.variables] must be an object keyed by alias; the manifest was likely produced by an incompatible plugin version.' + ); + } + + $parts = []; + + foreach ($aliases as $alias) { + if (isset($familyStyles[$alias])) { + $parts[] = $familyStyles[$alias]; + } + } + + if ($variables !== []) { + $parts[] = $this->filterVariables($variables, $aliases); + } + + return implode("\n\n", $parts); + } + + /** + * Build a `:root` block containing only the CSS variables for the given aliases. + * + * @param array $variables + * @param list $aliases List of aliases in desired emission order. + * @return string + */ + protected function filterVariables(array $variables, array $aliases) + { + $declarations = []; + + foreach ($aliases as $alias) { + if (isset($variables[$alias])) { + $declarations[] = ' '.$variables[$alias]; + } + } + + if ($declarations === []) { + return ''; + } + + return ":root {\n".implode("\n", $declarations)."\n}"; + } + + /** + * Read a CSS file from the build directory. + * + * @param string $buildDirectory + * @param string $file + * @return string + * + * @throws \Illuminate\Foundation\ViteException + */ + protected function readStyleFile(string $buildDirectory, string $file) + { + $path = public_path($buildDirectory.'/'.$file); + + if (! is_file($path)) { + throw new ViteException("Unable to locate font CSS file from manifest: {$path}."); + } + + return file_get_contents($path); + } + + /** + * Validate the font manifest structure. + * + * @param array $manifest + * @return void + * + * @throws \Illuminate\Foundation\ViteException + */ + public function ensureValidManifest(array $manifest) + { + if (! isset($manifest['version'])) { + throw new ViteException('The font manifest is missing the [version] key.'); + } + + if ($manifest['version'] !== 1) { + throw new ViteException("Unsupported font manifest version [{$manifest['version']}]. Supported versions: 1."); + } + + if (! isset($manifest['families']) || ! is_array($manifest['families'])) { + throw new ViteException('The font manifest is missing the [families] key.'); + } + } + + /** + * Validate that the requested aliases exist in the manifest. + * + * @param list $aliases + * @param array $manifest + * @return void + * + * @throws \Illuminate\Foundation\ViteException + */ + public function ensureValidFamilies(array $aliases, array $manifest) + { + $available = array_keys($manifest['families'] ?? []); + + foreach ($aliases as $alias) { + if (! in_array($alias, $available, true)) { + throw new ViteException( + "Font alias [{$alias}] is not defined in the font manifest. Available aliases: ".implode(', ', $available).'.' + ); + } + } + } + + /** + * Validate that each preload entry contains the required keys. + * + * @param list> $preloads + * @param bool $isHot + * @return void + * + * @throws \Illuminate\Foundation\ViteException + */ + public function ensureValidPreloads(array $preloads, bool $isHot) + { + $urlKey = $isHot ? 'url' : 'file'; + + foreach ($preloads as $index => $preload) { + if (! isset($preload['alias'])) { + throw new ViteException("Font manifest preload entry [{$index}] is missing the [alias] key."); + } + + if (! isset($preload[$urlKey])) { + throw new ViteException("Font manifest preload entry [{$index}] for alias [{$preload['alias']}] is missing the [{$urlKey}] key."); + } + } + } + + /** + * Flush cached manifests. + * + * @return void + */ + public function flush() + { + static::$manifests = []; + } +} diff --git a/src/Illuminate/Support/Facades/Vite.php b/src/Illuminate/Support/Facades/Vite.php index 6f727c89c77d..410c2015629f 100644 --- a/src/Illuminate/Support/Facades/Vite.php +++ b/src/Illuminate/Support/Facades/Vite.php @@ -25,6 +25,8 @@ * @method static string asset(string $asset, string|null $buildDirectory = null) * @method static string content(string $asset, string|null $buildDirectory = null) * @method static string|null manifestHash(string|null $buildDirectory = null) + * @method static \Illuminate\Support\HtmlString fonts(list|string|null $aliases = null) + * @method static \Illuminate\Foundation\Vite useFontsManifestFilename(string $filename) * @method static bool isRunningHot() * @method static string toHtml() * @method static void flush() diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php b/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php index f217f59bea89..ba948ac14e7c 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php @@ -75,4 +75,19 @@ protected function compileViteReactRefresh() return "reactRefresh(); ?>"; } + + /** + * Compile the "fonts" statements into valid PHP. + * + * @param string|null $arguments + * @return string + */ + protected function compileFonts($arguments) + { + $arguments ??= '()'; + + $class = Vite::class; + + return "fonts{$arguments}; ?>"; + } } diff --git a/tests/Foundation/FoundationViteFontsTest.php b/tests/Foundation/FoundationViteFontsTest.php new file mode 100644 index 000000000000..4a0d635df63a --- /dev/null +++ b/tests/Foundation/FoundationViteFontsTest.php @@ -0,0 +1,1288 @@ +set('app.asset_url', 'https://example.com'); + } + + protected function tearDown(): void + { + $this->cleanFontsManifest(); + $this->cleanFontsManifest('custom-build'); + $this->cleanHotFontsManifest(); + $this->cleanHotFontsManifest(__DIR__.'/custom-hot-dir'); + $this->cleanHotFile(); + $this->cleanHotFile(__DIR__.'/custom-hot-dir/hot'); + app(Vite::class)->flush(); + + parent::tearDown(); + } + + public function testFontsReturnsEmptyStringWhenNoManifestExists() + { + app()->usePublicPath(__DIR__); + + $result = app(Vite::class)->fonts(); + + $this->assertSame('', $result->toHtml()); + } + + public function testFontsReturnsEmptyStringWhenHotFileExistsButNoHotManifest() + { + $this->makeHotFile(); + + $result = app(Vite::class)->fonts(); + + $this->assertSame('', $result->toHtml()); + } + + public function testFontsRendersPreloadsAndStyleInBuildMode() + { + $this->makeFontsManifest(); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; src: url('../fonts/inter-400.woff2') format('woff2'); }"); + + $result = app(Vite::class)->fonts(); + + $this->assertStringContainsString( + '', + $result->toHtml() + ); + $this->assertStringContainsString( + "", + $result->toHtml() + ); + } + + public function testFontsRendersPreloadsBeforeStyle() + { + $this->makeFontsManifest(); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; }"); + + $result = app(Vite::class)->fonts()->toHtml(); + + $preloadPos = strpos($result, ''); + + $this->assertNotFalse($preloadPos); + $this->assertNotFalse($stylePos); + $this->assertLessThan($stylePos, $preloadPos); + } + + public function testFontsRendersInHotMode() + { + $this->makeHotFile(); + $this->makeHotFontsManifest(); + + $result = app(Vite::class)->fonts(); + + $this->assertStringContainsString( + '', + $result->toHtml() + ); + $this->assertStringContainsString( + "", + $result->toHtml() + ); + } + + public function testFontsRespectsCustomBuildDirectory() + { + $this->makeFontsManifest($this->defaultManifest(), 'custom-build'); + $this->makeFontsCssFile('custom-build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; }"); + + ViteFacade::useBuildDirectory('custom-build'); + + $result = app(Vite::class)->fonts(); + + $this->assertStringContainsString( + 'href="https://example.com/custom-build/assets/inter-400.woff2"', + $result->toHtml() + ); + } + + public function testFontsRespectsCreateAssetPathsUsing() + { + $this->makeFontsManifest(); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; }"); + + ViteFacade::createAssetPathsUsing(fn ($path) => "https://cdn.example.com/{$path}"); + + $result = app(Vite::class)->fonts(); + + $this->assertStringContainsString( + 'href="https://cdn.example.com/build/assets/inter-400.woff2"', + $result->toHtml() + ); + + ViteFacade::createAssetPathsUsing(null); + } + + public function testFontsAppliesCspNonceToStyleAndPreloads() + { + Str::createRandomStringsUsing(fn ($length) => "random-string-with-length:{$length}"); + $this->makeFontsManifest(); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; }"); + + ViteFacade::useCspNonce(); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertStringContainsString('nonce="random-string-with-length:40"', $result); + $this->assertStringContainsString('", $result); + } + + public function testFontsWithNoStyleStillRendersPreloads() + { + $this->makeFontsManifest([ + 'version' => 1, + 'preloads' => [ + [ + 'alias' => 'sans', + 'family' => 'Inter', + 'weight' => 400, + 'style' => 'normal', + 'file' => 'assets/inter-400.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin' => 'anonymous', + ], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ]); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertStringContainsString('assertStringNotContainsString('flush(); + + $this->assertEmpty($vite->preloadedAssets()); + } + + public function testFontsFlushClearsPreloadedAssetsButPreservesConfiguration() + { + $this->makeFontsManifest(); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; }"); + + $vite = app(Vite::class); + $vite->useFontsManifestFilename('fonts-manifest.json'); + $vite->fonts(); + + $this->assertNotEmpty($vite->preloadedAssets()); + + $vite->flush(); + + $this->assertEmpty($vite->preloadedAssets()); + + $result = $vite->fonts()->toHtml(); + $this->assertStringContainsString('usePublicPath(__DIR__); + + $customHotDir = __DIR__.'/custom-hot-dir'; + + if (! file_exists($customHotDir)) { + mkdir($customHotDir, 0755, true); + } + + file_put_contents($customHotDir.'/hot', 'http://localhost:3000'); + + $manifest = json_encode([ + 'version' => 1, + 'style' => [ + 'inline' => "@font-face { font-family: 'Inter'; }", + ], + 'preloads' => [], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + file_put_contents($customHotDir.'/fonts-manifest.dev.json', $manifest); + + ViteFacade::useHotFile($customHotDir.'/hot'); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertStringContainsString("font-family: 'Inter'", $result); + } + + public function testHotManifestNotFoundWithCustomHotFileReturnsEmpty() + { + app()->usePublicPath(__DIR__); + + $customHotDir = __DIR__.'/custom-hot-dir'; + + if (! file_exists($customHotDir)) { + mkdir($customHotDir, 0755, true); + } + + file_put_contents($customHotDir.'/hot', 'http://localhost:3000'); + + ViteFacade::useHotFile($customHotDir.'/hot'); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertSame('', $result); + } + + public function testFontsRendersUtilityClassInBuildMode() + { + $this->makeFontsManifest([ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => "@font-face { font-family: \"Inter\"; src: url('inter.woff2'); }\n\n.font-sans {\n font-family: var(--font-sans);\n}", + ], + 'variables' => ['sans' => '--font-sans: "Inter";'], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/inter-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ]); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: \"Inter\"; }\n\n.font-sans { font-family: var(--font-sans); }"); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertStringContainsString('.font-sans', $result); + $this->assertStringContainsString('font-family: var(--font-sans)', $result); + } + + public function testFontsRendersUtilityClassInHotMode() + { + $this->makeHotFile(); + $this->makeHotFontsManifest([ + 'version' => 1, + 'style' => [ + 'inline' => "@font-face { font-family: \"Inter\"; }\n\n.font-sans {\n font-family: var(--font-sans);\n}", + 'familyStyles' => [ + 'sans' => "@font-face { font-family: \"Inter\"; }\n\n.font-sans {\n font-family: var(--font-sans);\n}", + ], + 'variables' => ['sans' => '--font-sans: "Inter";'], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'url' => 'http://localhost:3000/__laravel_vite_plugin__/fonts/inter.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ]); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertStringContainsString('.font-sans', $result); + $this->assertStringContainsString('font-family: var(--font-sans)', $result); + } + + public function testFontsFilteredByAliasIncludesUtilityClass() + { + $this->makeFontsManifest([ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => "@font-face { font-family: \"Inter\"; }\n\n.font-sans {\n font-family: var(--font-sans);\n}", + 'heading' => "@font-face { font-family: \"Roboto\"; }\n\n.font-heading {\n font-family: var(--font-heading);\n}", + ], + 'variables' => [ + 'sans' => '--font-sans: "Inter";', + 'heading' => '--font-heading: "Roboto";', + ], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/inter-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ['alias' => 'heading', 'family' => 'Roboto', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/roboto-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + 'heading' => ['family' => 'Roboto', 'variable' => '--font-heading'], + ], + ]); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', 'full-css'); + + $result = app(Vite::class)->fonts(['sans'])->toHtml(); + + $this->assertStringContainsString('.font-sans', $result); + $this->assertStringNotContainsString('.font-heading', $result); + } + + public function testFontsFiltersByMultipleAliases() + { + $this->makeFontsManifest([ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => '@font-face { font-family: "Inter"; }', + 'mono' => '@font-face { font-family: "JetBrains Mono"; }', + 'heading' => '@font-face { font-family: "Playfair Display"; }', + ], + 'variables' => [ + 'sans' => '--font-sans: "Inter";', + 'mono' => '--font-mono: "JetBrains Mono";', + 'heading' => '--font-heading: "Playfair Display";', + ], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/inter-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ['alias' => 'mono', 'family' => 'JetBrains Mono', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/jb-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ['alias' => 'heading', 'family' => 'Playfair Display', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/playfair-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + 'mono' => ['family' => 'JetBrains Mono', 'variable' => '--font-mono'], + 'heading' => ['family' => 'Playfair Display', 'variable' => '--font-heading'], + ], + ]); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', 'full-css'); + + $result = app(Vite::class)->fonts(['sans', 'mono'])->toHtml(); + + $this->assertStringContainsString('inter-400.woff2', $result); + $this->assertStringContainsString('jb-400.woff2', $result); + $this->assertStringNotContainsString('playfair-400.woff2', $result); + $this->assertStringContainsString('font-family: "Inter"', $result); + $this->assertStringContainsString('font-family: "JetBrains Mono"', $result); + $this->assertStringNotContainsString('Playfair Display', $result); + $this->assertStringContainsString('--font-sans:', $result); + $this->assertStringContainsString('--font-mono:', $result); + $this->assertStringNotContainsString('--font-heading:', $result); + } + + public function testFontsFiltersByAliasWithSingleLineVariables() + { + $this->makeFontsManifest([ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => '@font-face { font-family: "Inter"; }', + 'mono' => '@font-face { font-family: "JetBrains Mono"; }', + ], + 'variables' => [ + 'sans' => '--font-sans: "Inter", "Inter fallback";', + 'mono' => '--font-mono: "JetBrains Mono";', + ], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/inter-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ['alias' => 'mono', 'family' => 'JetBrains Mono', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/jb-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + 'mono' => ['family' => 'JetBrains Mono', 'variable' => '--font-mono'], + ], + ]); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', 'full-css'); + + $result = app(Vite::class)->fonts(['sans'])->toHtml(); + + $this->assertStringContainsString('--font-sans:', $result); + $this->assertStringNotContainsString('--font-mono:', $result); + } + + public function testFontsDuplicateFamilyWithDifferentAliasesRenderIndependently() + { + $this->makeFontsManifest([ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => '@font-face { font-family: "Inter"; font-weight: 400; }', + 'heading' => '@font-face { font-family: "Inter"; font-weight: 700; }', + ], + 'variables' => [ + 'sans' => '--font-sans: "Inter";', + 'heading' => '--font-heading: "Inter";', + ], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/inter-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ['alias' => 'heading', 'family' => 'Inter', 'weight' => 700, 'style' => 'normal', 'file' => 'assets/inter-700.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + 'heading' => ['family' => 'Inter', 'variable' => '--font-heading'], + ], + ]); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', 'full-css'); + + $result = app(Vite::class)->fonts(['sans'])->toHtml(); + + $this->assertStringContainsString('inter-400.woff2', $result); + $this->assertStringNotContainsString('inter-700.woff2', $result); + $this->assertStringContainsString('font-weight: 400', $result); + $this->assertStringNotContainsString('font-weight: 700', $result); + } + + public function testFontsRendersUtilityClassWithCustomAlias() + { + $this->makeFontsManifest([ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => "@font-face { font-family: \"Inter\"; }\n\n.font-sans {\n font-family: var(--font-sans);\n}", + ], + 'variables' => ['sans' => '--font-sans: "Inter";'], + ], + 'preloads' => [ + ['alias' => 'sans', 'family' => 'Inter', 'weight' => 400, 'style' => 'normal', 'file' => 'assets/inter-400.woff2', 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous'], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ]); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: \"Inter\"; }\n\n.font-sans { font-family: var(--font-sans); }"); + + $result = app(Vite::class)->fonts()->toHtml(); + + $this->assertStringContainsString('.font-sans', $result); + $this->assertStringContainsString('font-family: var(--font-sans)', $result); + } + + public function testFontsCallsIsRunningHotOnceInHotMode() + { + $this->makeHotFile(); + $this->makeHotFontsManifest(); + + $vite = new class extends Vite + { + public int $isRunningHotCalls = 0; + + public function isRunningHot() + { + $this->isRunningHotCalls++; + + return parent::isRunningHot(); + } + }; + + $result = $vite->fonts(); + + $this->assertSame(1, $vite->isRunningHotCalls); + $this->assertStringContainsString('toHtml()); + } + + public function testFilterVariablesSignatureIsNonNullable() + { + $method = new ReflectionMethod(ViteFonts::class, 'filterVariables'); + $aliasesParam = $method->getParameters()[1]; + + $this->assertSame('aliases', $aliasesParam->getName()); + $this->assertFalse( + $aliasesParam->allowsNull(), + 'filterVariables() must declare $aliases as a non-nullable array; the null branch was dead.' + ); + + $type = $aliasesParam->getType(); + $this->assertNotNull($type); + $this->assertSame('array', (string) $type); + } + + public function testFilterVariablesEmitsOnlyRequestedAlias() + { + $fonts = new ViteFonts; + $method = new ReflectionMethod(ViteFonts::class, 'filterVariables'); + + $result = $method->invoke($fonts, [ + 'sans' => '--font-sans: "Inter";', + 'mono' => '--font-mono: "JetBrains Mono";', + ], ['sans']); + + $this->assertStringContainsString('--font-sans:', $result); + $this->assertStringNotContainsString('--font-mono:', $result); + } + + public function testFilterVariablesPreservesAliasOrder() + { + $fonts = new ViteFonts; + $method = new ReflectionMethod(ViteFonts::class, 'filterVariables'); + + $result = $method->invoke($fonts, [ + 'sans' => '--font-sans: "Inter";', + 'mono' => '--font-mono: "JetBrains Mono";', + 'heading' => '--font-heading: "Playfair";', + ], ['heading', 'sans']); + + $headingPos = strpos($result, '--font-heading:'); + $sansPos = strpos($result, '--font-sans:'); + + $this->assertNotFalse($headingPos); + $this->assertNotFalse($sansPos); + $this->assertLessThan($sansPos, $headingPos); + $this->assertStringNotContainsString('--font-mono:', $result); + } + + public function testFilterVariablesSkipsUnknownAlias() + { + $fonts = new ViteFonts; + $method = new ReflectionMethod(ViteFonts::class, 'filterVariables'); + + $result = $method->invoke($fonts, [ + 'sans' => '--font-sans: "Inter";', + ], ['sans', 'missing']); + + $this->assertStringContainsString('--font-sans:', $result); + $this->assertStringNotContainsString('missing', $result); + } + + public function testFilterVariablesEmptyAliasListProducesNoBlock() + { + $fonts = new ViteFonts; + $method = new ReflectionMethod(ViteFonts::class, 'filterVariables'); + + $result = $method->invoke($fonts, [ + 'sans' => '--font-sans: "Inter";', + ], []); + + $this->assertSame('', $result); + } + + public function testFontsCallsIsRunningHotOnceInBuildMode() + { + $this->makeFontsManifest(); + $this->makeFontsCssFile('build', 'assets/fonts-abc123.css', "@font-face { font-family: 'Inter'; }"); + + $vite = new class extends Vite + { + public int $isRunningHotCalls = 0; + + public function isRunningHot() + { + $this->isRunningHotCalls++; + + return parent::isRunningHot(); + } + }; + + $result = $vite->fonts(); + + $this->assertSame(1, $vite->isRunningHotCalls); + $this->assertStringContainsString('toHtml()); + } + + protected function defaultManifest(): array + { + return [ + 'version' => 1, + 'style' => [ + 'file' => 'assets/fonts-abc123.css', + 'familyStyles' => [ + 'sans' => "@font-face { font-family: 'Inter'; }", + ], + 'variables' => ['sans' => '--font-sans: "Inter";'], + ], + 'preloads' => [ + [ + 'alias' => 'sans', + 'family' => 'Inter', + 'weight' => 400, + 'style' => 'normal', + 'file' => 'assets/inter-400.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin' => 'anonymous', + ], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ]; + } + + protected function defaultHotManifest(): array + { + return [ + 'version' => 1, + 'style' => [ + 'inline' => "@font-face { font-family: 'Inter'; src: url('http://localhost:3000/fonts/inter.woff2'); }", + 'familyStyles' => [ + 'sans' => "@font-face { font-family: 'Inter'; src: url('http://localhost:3000/fonts/inter.woff2'); }", + ], + 'variables' => ['sans' => '--font-sans: "Inter";'], + ], + 'preloads' => [ + [ + 'alias' => 'sans', + 'family' => 'Inter', + 'weight' => 400, + 'style' => 'normal', + 'url' => 'http://localhost:3000/__laravel_vite_plugin__/fonts/inter.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin' => 'anonymous', + ], + ], + 'families' => [ + 'sans' => ['family' => 'Inter', 'variable' => '--font-sans'], + ], + ]; + } + + protected function makeFontsManifest(?array $contents = null, string $buildDir = 'build'): void + { + app()->usePublicPath(__DIR__); + + $dir = public_path($buildDir); + + if (! file_exists($dir)) { + mkdir($dir, 0755, true); + } + + $manifest = json_encode($contents ?? $this->defaultManifest(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + file_put_contents(public_path("{$buildDir}/fonts-manifest.json"), $manifest); + } + + protected function makeFontsCssFile(string $buildDir, string $file, string $content): void + { + app()->usePublicPath(__DIR__); + + $dir = public_path($buildDir.'/assets'); + + if (! file_exists($dir)) { + mkdir($dir, 0755, true); + } + + file_put_contents(public_path("{$buildDir}/{$file}"), $content); + } + + protected function makeHotFile(?string $path = null): void + { + app()->usePublicPath(__DIR__); + + $path ??= public_path('hot'); + + $dir = dirname($path); + + if (! file_exists($dir)) { + mkdir($dir, 0755, true); + } + + file_put_contents($path, 'http://localhost:3000'); + } + + protected function makeHotFontsManifest(?array $contents = null, ?string $dir = null): void + { + app()->usePublicPath(__DIR__); + + $dir ??= __DIR__; + + if (! file_exists($dir)) { + mkdir($dir, 0755, true); + } + + $manifest = json_encode($contents ?? $this->defaultHotManifest(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + file_put_contents($dir.'/fonts-manifest.dev.json', $manifest); + } + + protected function cleanFontsManifest(string $buildDir = 'build'): void + { + $cssFile = public_path("{$buildDir}/assets/fonts-abc123.css"); + + if (file_exists($cssFile)) { + unlink($cssFile); + } + + $assetsDir = public_path("{$buildDir}/assets"); + + if (is_dir($assetsDir) && count(glob("{$assetsDir}/*")) === 0) { + rmdir($assetsDir); + } + + $manifestFile = public_path("{$buildDir}/fonts-manifest.json"); + + if (file_exists($manifestFile)) { + unlink($manifestFile); + } + + $dir = public_path($buildDir); + + if (is_dir($dir) && count(glob("{$dir}/*")) === 0) { + rmdir($dir); + } + } + + protected function cleanHotFontsManifest(?string $dir = null): void + { + $dir ??= __DIR__; + + $path = $dir.'/fonts-manifest.dev.json'; + + if (file_exists($path)) { + unlink($path); + } + + if ($dir !== __DIR__ && is_dir($dir) && count(glob("{$dir}/*")) === 0) { + rmdir($dir); + } + } + + protected function cleanHotFile(?string $path = null): void + { + $path ??= public_path('hot'); + + if (file_exists($path)) { + unlink($path); + } + + $dir = dirname($path); + + if ($dir !== __DIR__ && is_dir($dir) && count(glob("{$dir}/*")) === 0) { + rmdir($dir); + } + } +} diff --git a/tests/Http/Middleware/VitePreloadingTest.php b/tests/Http/Middleware/VitePreloadingTest.php index 682fc5591f1c..41037398abae 100644 --- a/tests/Http/Middleware/VitePreloadingTest.php +++ b/tests/Http/Middleware/VitePreloadingTest.php @@ -145,6 +145,94 @@ public function testItCanLimitNumberOfAssetsPreloaded() ); } + public function testFontPreloadEntriesResultInLinkHeaders() + { + $app = new Container; + $app->instance(Vite::class, new class extends Vite + { + protected $preloadedAssets = [ + 'https://example.com/build/assets/inter-400.woff2' => [ + 'rel="preload"', + 'as="font"', + 'type="font/woff2"', + 'crossorigin="anonymous"', + ], + ]; + }); + Facade::setFacadeApplication($app); + + $response = (new AddLinkHeadersForPreloadedAssets)->handle(new Request, function () { + return new Response('Hello Laravel'); + }); + + $this->assertSame( + '; rel="preload"; as="font"; type="font/woff2"; crossorigin="anonymous"', + $response->headers->get('Link'), + ); + } + + public function testFontPreloadsDoNotOverwriteExistingJsPreloads() + { + $app = new Container; + $app->instance(Vite::class, new class extends Vite + { + protected $preloadedAssets = [ + 'https://example.com/build/assets/app.js' => [ + 'rel="modulepreload"', + ], + 'https://example.com/build/assets/inter-400.woff2' => [ + 'rel="preload"', + 'as="font"', + 'type="font/woff2"', + 'crossorigin="anonymous"', + ], + ]; + }); + Facade::setFacadeApplication($app); + + $response = (new AddLinkHeadersForPreloadedAssets)->handle(new Request, function () { + return new Response('Hello Laravel'); + }); + + $this->assertSame( + [ + '; rel="modulepreload", ; rel="preload"; as="font"; type="font/woff2"; crossorigin="anonymous"', + ], + $response->headers->all('Link'), + ); + } + + public function testLimitAppliesToCombinedJsAndFontPreloads() + { + $app = new Container; + $app->instance(Vite::class, new class extends Vite + { + protected $preloadedAssets = [ + 'https://example.com/build/assets/app.js' => [ + 'rel="modulepreload"', + ], + 'https://example.com/build/assets/inter-400.woff2' => [ + 'rel="preload"', + 'as="font"', + ], + 'https://example.com/build/assets/inter-700.woff2' => [ + 'rel="preload"', + 'as="font"', + ], + ]; + }); + Facade::setFacadeApplication($app); + + $response = (new AddLinkHeadersForPreloadedAssets)->handle(new Request, fn () => new Response('ok'), 2); + + $this->assertSame( + [ + '; rel="modulepreload", ; rel="preload"; as="font"', + ], + $response->headers->all('Link'), + ); + } + public function test_it_can_configure_the_middleware() { $definition = AddLinkHeadersForPreloadedAssets::using(limit: 5); diff --git a/tests/View/Blade/BladeHelpersTest.php b/tests/View/Blade/BladeHelpersTest.php index 8e071c38b6c6..0e091e0ed6c2 100644 --- a/tests/View/Blade/BladeHelpersTest.php +++ b/tests/View/Blade/BladeHelpersTest.php @@ -16,5 +16,9 @@ public function testEchosAreCompiled() $this->assertSame('', $this->compiler->compileString('@vite(\'resources/js/app.js\')')); $this->assertSame('', $this->compiler->compileString('@vite([\'resources/js/app.js\'])')); $this->assertSame('reactRefresh(); ?>', $this->compiler->compileString('@viteReactRefresh')); + $this->assertSame('fonts(); ?>', $this->compiler->compileString('@fonts')); + $this->assertSame('fonts(); ?>', $this->compiler->compileString('@fonts()')); + $this->assertSame('fonts([\'Inter\']); ?>', $this->compiler->compileString('@fonts([\'Inter\'])')); + $this->assertSame('fonts([\'Inter\', \'JetBrains Mono\']); ?>', $this->compiler->compileString('@fonts([\'Inter\', \'JetBrains Mono\'])')); } } From a7f1c5bcfd3cd1f11ffd0295ef3300e4c0172aac Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:52:01 +0000 Subject: [PATCH 257/596] Update facade docblocks --- src/Illuminate/Support/Facades/Vite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Vite.php b/src/Illuminate/Support/Facades/Vite.php index 410c2015629f..a0ed9206f477 100644 --- a/src/Illuminate/Support/Facades/Vite.php +++ b/src/Illuminate/Support/Facades/Vite.php @@ -25,7 +25,7 @@ * @method static string asset(string $asset, string|null $buildDirectory = null) * @method static string content(string $asset, string|null $buildDirectory = null) * @method static string|null manifestHash(string|null $buildDirectory = null) - * @method static \Illuminate\Support\HtmlString fonts(list|string|null $aliases = null) + * @method static \Illuminate\Support\HtmlString fonts(array|string|null $aliases = null) * @method static \Illuminate\Foundation\Vite useFontsManifestFilename(string $filename) * @method static bool isRunningHot() * @method static string toHtml() From 5ffd3cb57d24772a331daef467d5033bcc4f512a Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Tue, 28 Apr 2026 20:41:19 +0330 Subject: [PATCH 258/596] [13.x] Refactor: add `match` (#59914) * refactor: add match * fix: style --- src/Illuminate/Foundation/Http/FormRequest.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index b95e81be6777..bad072c3f9a9 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -299,15 +299,12 @@ protected function getRedirectUrl() { $url = $this->redirector->getUrlGenerator(); - if ($this->redirect) { - return $url->to($this->redirect); - } elseif ($this->redirectRoute) { - return $url->route($this->redirectRoute); - } elseif ($this->redirectAction) { - return $url->action($this->redirectAction); - } - - return $url->previous(); + return match (true) { + ! empty($this->redirect) => $url->to($this->redirect), + ! empty($this->redirectRoute) => $url->route($this->redirectRoute), + ! empty($this->redirectAction) => $url->action($this->redirectAction), + default => $url->previous(), + }; } /** From ade858eb19cbf22429b69841ea64305b4252d919 Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Tue, 28 Apr 2026 20:41:39 +0330 Subject: [PATCH 259/596] refactor: remove unnecessary call function (#59915) --- src/Illuminate/Support/Str.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 4b42dd00df76..53c549404669 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1202,7 +1202,7 @@ public static function repeat(string $string, int $times) public static function replaceArray($search, $replace, $subject) { if ($replace instanceof Traversable) { - $replace = Arr::from($replace); + $replace = iterator_to_array($replace); } $segments = explode($search, $subject); @@ -1244,15 +1244,15 @@ private static function toStringOr($value, $fallback) public static function replace($search, $replace, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = Arr::from($search); + $search = iterator_to_array($search); } if ($replace instanceof Traversable) { - $replace = Arr::from($replace); + $replace = iterator_to_array($replace); } if ($subject instanceof Traversable) { - $subject = Arr::from($subject); + $subject = iterator_to_array($subject); } return $caseSensitive @@ -1385,7 +1385,7 @@ public static function replaceMatches($pattern, $replace, $subject, $limit = -1) public static function remove($search, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = Arr::from($search); + $search = iterator_to_array($search); } return $caseSensitive From 4e5db2a2234f6886d755e9862ad4100db4fb50f6 Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Tue, 28 Apr 2026 20:42:00 +0330 Subject: [PATCH 260/596] [13.x] Refactor: improve tests (#59912) * refactor: remove unnecessary arg * refactor: test * fix: style --- .../Http/RequestDurationThresholdTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Integration/Http/RequestDurationThresholdTest.php b/tests/Integration/Http/RequestDurationThresholdTest.php index 4cb8d8a43b7f..f5e41dac15c5 100644 --- a/tests/Integration/Http/RequestDurationThresholdTest.php +++ b/tests/Integration/Http/RequestDurationThresholdTest.php @@ -20,14 +20,14 @@ public function testItCanHandleExceedingRequestDuration() $response = new Response(); $called = false; $kernel = $this->app[Kernel::class]; - $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called) { + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function () use (&$called) { $called = true; }); Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMillisecond()); $kernel->terminate($request, $response); $this->assertTrue($called); @@ -40,7 +40,7 @@ public function testItDoesntCallWhenExactlyThresholdDuration() $response = new Response(); $called = false; $kernel = $this->app[Kernel::class]; - $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called) { + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function () use (&$called) { $called = true; }); @@ -60,7 +60,7 @@ public function testItProvidesRequestToHandler() $response = new Response(); $url = null; $kernel = $this->app[Kernel::class]; - $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::seconds(1), function ($startedAt, $request) use (&$url) { + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function ($startedAt, $request) use (&$url) { $url = $request->url(); }); @@ -79,7 +79,7 @@ public function testUsesTheConfiguredDateTimezone() Route::get('test-route', fn () => 'ok'); $kernel = $this->app[Kernel::class]; $startedAt = null; - $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::seconds(1), function ($started) use (&$startedAt) { + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function ($started) use (&$startedAt) { $startedAt = $started; }); @@ -106,7 +106,7 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds() Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMillisecond()); $kernel->terminate($request, $response); $this->assertTrue($called); @@ -146,7 +146,7 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime() Carbon::setTestNow(Carbon::now()); $kernel->handle($request); - Carbon::setTestNow(Carbon::now()->addSecond()->addMilliseconds(1)); + Carbon::setTestNow(Carbon::now()->addSecond()->addMillisecond()); $kernel->terminate($request, $response); $this->assertTrue($called); From 11fef3acbf02973c93c1778cf4d36fd9fadac8ac Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:12:58 +0600 Subject: [PATCH 261/596] Align Enumerable all, times and range @return with implementations (#59911) --- src/Illuminate/Collections/Enumerable.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 0edd48b59c7b..e6d62efe95b5 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -34,9 +34,11 @@ public static function make($items = []); /** * Create a new instance by invoking the callback a given amount of times. * + * @template TTimesValue + * * @param int $number - * @param callable|null $callback - * @return static + * @param (callable(int): TTimesValue)|null $callback + * @return static */ public static function times($number, ?callable $callback = null); @@ -46,7 +48,7 @@ public static function times($number, ?callable $callback = null); * @param int $from * @param int $to * @param int $step - * @return static + * @return static */ public static function range($from, $to, $step = 1); @@ -81,7 +83,7 @@ public static function empty(); /** * Get all items in the enumerable. * - * @return array + * @return array */ public function all(); From 92f76dc981fc9d372c9acfba0fbd1c2e5d950a45 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:13:46 +0600 Subject: [PATCH 262/596] Align Enumerable search and flatten @return with implementations (#59910) --- src/Illuminate/Collections/Enumerable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index e6d62efe95b5..d1e5bdb3c48e 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -496,7 +496,7 @@ public function firstWhere($key, $operator = null, $value = null); * Get a flattened array of the items in the collection. * * @param int $depth - * @return static + * @return static */ public function flatten($depth = INF); @@ -926,7 +926,7 @@ public function reverse(); * * @param TValue|callable(TValue,TKey): bool $value * @param bool $strict - * @return TKey|bool + * @return TKey|false */ public function search($value, $strict = false); From 0069d0dba785939b0d6b8c830f6e9c30f271560b Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:16:27 +0600 Subject: [PATCH 263/596] Specify Translation Loader namespaces shape (#59913) --- src/Illuminate/Contracts/Translation/Loader.php | 2 +- src/Illuminate/Translation/ArrayLoader.php | 2 +- src/Illuminate/Translation/FileLoader.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Contracts/Translation/Loader.php b/src/Illuminate/Contracts/Translation/Loader.php index b08418d13665..6f43109e87c0 100755 --- a/src/Illuminate/Contracts/Translation/Loader.php +++ b/src/Illuminate/Contracts/Translation/Loader.php @@ -34,7 +34,7 @@ public function addJsonPath($path); /** * Get an array of all the registered namespaces. * - * @return array + * @return array */ public function namespaces(); } diff --git a/src/Illuminate/Translation/ArrayLoader.php b/src/Illuminate/Translation/ArrayLoader.php index 117e0440ee31..44268092051a 100644 --- a/src/Illuminate/Translation/ArrayLoader.php +++ b/src/Illuminate/Translation/ArrayLoader.php @@ -72,7 +72,7 @@ public function addMessages($locale, $group, array $messages, $namespace = null) /** * Get an array of all the registered namespaces. * - * @return array + * @return array */ public function namespaces() { diff --git a/src/Illuminate/Translation/FileLoader.php b/src/Illuminate/Translation/FileLoader.php index e30fdf7c413e..c179d77d0131 100755 --- a/src/Illuminate/Translation/FileLoader.php +++ b/src/Illuminate/Translation/FileLoader.php @@ -33,7 +33,7 @@ class FileLoader implements Loader /** * All of the namespace hints. * - * @var array + * @var array */ protected $hints = []; @@ -174,7 +174,7 @@ public function addNamespace($namespace, $hint) /** * Get an array of all the registered namespaces. * - * @return array + * @return array */ public function namespaces() { From 3dd5396981d597be82c83fc77bbb1ea3f388d173 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:17:00 +0600 Subject: [PATCH 264/596] Fix duplicate type key in getTypes/processTypes return shape (#59909) --- src/Illuminate/Database/Query/Processors/Processor.php | 2 +- src/Illuminate/Database/Schema/Builder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 46f692e49a58..0e2c561dbd6a 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -102,7 +102,7 @@ public function processViews($results) * Process the results of a types query. * * @param list> $results - * @return list + * @return list */ public function processTypes($results) { diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 810ee5dc70de..006e60d3c22b 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -251,7 +251,7 @@ public function getViews($schema = null) * Get the user-defined types that belong to the connection. * * @param string|string[]|null $schema - * @return list + * @return list */ public function getTypes($schema = null) { From 4137af02d25eda55d0c44afba0177bde8fef7757 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:17:15 +0600 Subject: [PATCH 265/596] int argument for sortByDesc and Enumerable sort methods (#59907) --- src/Illuminate/Collections/Collection.php | 2 +- src/Illuminate/Collections/Enumerable.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index f647f82e1f70..2067cc34b55d 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -1676,7 +1676,7 @@ protected function sortByMany(array $comparisons = [], int $options = SORT_REGUL /** * Sort the collection in descending order using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string $callback + * @param array|(callable(TValue, TKey): mixed)|string|int $callback * @param int $options * @return static */ diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index d1e5bdb3c48e..f6c447d246e7 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -1073,7 +1073,7 @@ public function sortDesc($options = SORT_REGULAR); /** * Sort the collection using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string $callback + * @param array|(callable(TValue, TKey): mixed)|string|int $callback * @param int $options * @param bool $descending * @return static @@ -1083,7 +1083,7 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false); /** * Sort the collection in descending order using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string $callback + * @param array|(callable(TValue, TKey): mixed)|string|int $callback * @param int $options * @return static */ From 8f1ac32273c959f1f80f96d4e14fd909b411c371 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:17:34 +0600 Subject: [PATCH 266/596] Match processForeignKeys return shape to Builder::getForeignKeys (#59908) --- src/Illuminate/Database/Query/Processors/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 0e2c561dbd6a..d027d0cb9fc7 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -135,7 +135,7 @@ public function processIndexes($results) * Process the results of a foreign keys query. * * @param list> $results - * @return list, foreign_schema: string, foreign_table: string, foreign_columns: list, on_update: string, on_delete: string}> + * @return list, foreign_schema: string|null, foreign_table: string, foreign_columns: list, on_update: string|null, on_delete: string|null}> */ public function processForeignKeys($results) { From f13b85b2cce7ef5e8f3bcdf2b6c6364bbdedae0b Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:18:25 +0000 Subject: [PATCH 267/596] Update version to v13.7.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index dcd38cfb1dda..67e5ea63befd 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 = '13.6.0'; + const VERSION = '13.7.0'; /** * The base path for the Laravel installation. From f99d31793268e1a5323535c9e592d3a97709000a Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:20:11 +0000 Subject: [PATCH 268/596] Update CHANGELOG --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f20680c2ed..d03e7d6f35c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,54 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.6.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.7.0...13.x) + +## [v13.7.0](https://github.com/laravel/framework/compare/v13.6.0...v13.7.0) - 2026-04-28 + +* [13.x] Apply rector fixes by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/59787 +* Support enum in LazyCollection -> keyBy() by [@Back1ng](https://github.com/Back1ng) in https://github.com/laravel/framework/pull/59809 +* [13.x] Add enum support to ConcurrencyManager driver method by [@maherelgamil](https://github.com/maherelgamil) in https://github.com/laravel/framework/pull/59801 +* [13.x] Allow arrays for assertSoftDeleted & assertNotSoftDeleted by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59796 +* [13.x] Extract exception context in `JsonFormatter` when `ExceptionHandler` is not bound by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/59799 +* [13.x] Add isLocked to the Lock class by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59791 +* Fix route registration for domain-scoped routes by [@Bottelet](https://github.com/Bottelet) in https://github.com/laravel/framework/pull/59793 +* [13.x] Mark `Scope@apply` builder parameter as having covariant template by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59790 +* [13.x] Allowing `DebounceFor` attribute to be inherited by [@TWithers](https://github.com/TWithers) in https://github.com/laravel/framework/pull/59795 +* [13.x] Fix PendingDispatch resolving Cache for every dispatched job by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59821 +* [13.x] Add bulk JSON path assertions to TestResponse by [@cyrodjohn](https://github.com/cyrodjohn) in https://github.com/laravel/framework/pull/59829 +* [13.x] Fix false positives in LazyCollection::has() for duplicate keys by [@Button99](https://github.com/Button99) in https://github.com/laravel/framework/pull/59832 +* [13.x] Add UnitEnum type support for $limiterName on RateLimitedWithRedis by [@trippo](https://github.com/trippo) in https://github.com/laravel/framework/pull/59841 +* [13.x] Allow jobs to react to worker signals by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59833 +* [13.x] Honor empty JSON:API sparse fieldsets by [@prateekbhujel](https://github.com/prateekbhujel) in https://github.com/laravel/framework/pull/59813 +* [13.x] Fix flaky DynamoBatchTest timing assertions by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59844 +* [13.x] Memoize credentials in SqsConnector by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59866 +* [13.x] Disable pausing on managed queue workers by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59870 +* [13.x] Fix getMigrationBatches return type annotation by [@mahfuz-rahman007](https://github.com/mahfuz-rahman007) in https://github.com/laravel/framework/pull/59876 +* [13.x] Fix PHPDoc typo in MigrationRepositoryInterface by [@mahfuz-rahman007](https://github.com/mahfuz-rahman007) in https://github.com/laravel/framework/pull/59875 +* [13.x] Add UnitEnum support to Cache Repository touch method by [@shane-zeng](https://github.com/shane-zeng) in https://github.com/laravel/framework/pull/59864 +* [13.x] Prevent array query params from bypassing signed URL validation by [@maherelgamil](https://github.com/maherelgamil) in https://github.com/laravel/framework/pull/59860 +* [13.x] Add enum support to setDefaultDriver in QueueManager, LogManager, and SessionManager by [@maherelgamil](https://github.com/maherelgamil) in https://github.com/laravel/framework/pull/59861 +* [13.x] Add enum support to RedisManager purge method by [@genius-asif-hub](https://github.com/genius-asif-hub) in https://github.com/laravel/framework/pull/59857 +* [13.x] Fix factory hasAttached method pivot JSON attribute handling by [@rmd974](https://github.com/rmd974) in https://github.com/laravel/framework/pull/59856 +* [13.x] Implement CanFlushLocks on NullStore and MemoizedStore by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/59850 +* [13.x] Introduce WorkerInterrupted event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59848 +* [13.x] Fix MigrationRepositoryInterface return type docblocks (object vs array) by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59887 +* int argument for Collection::sortBy() by [@lorenzolosa](https://github.com/lorenzolosa) in https://github.com/laravel/framework/pull/59894 +* [13.x] Add detailed [@return](https://github.com/return) shape to Schema\Builder::getForeignKeys by [@maherelgamil](https://github.com/maherelgamil) in https://github.com/laravel/framework/pull/59903 +* [13.x] Fix EloquentModelDecimalCastingTest assertion across brick/math versions by [@maherelgamil](https://github.com/maherelgamil) in https://github.com/laravel/framework/pull/59904 +* [13.x] Correct Lock getCurrentOwner [@return](https://github.com/return) type to string|null by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59890 +* [13.x] Correct Batch fresh and add [@return](https://github.com/return) to self|null by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59891 +* [13.x] Align Mailable::cc [@return](https://github.com/return) with sibling fluent methods by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59892 +* [13.x] Add support for `SortDirection` enum to collections and Arr by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59859 +* Add [@fonts](https://github.com/fonts) Blade directive and Vite font optimization runtime by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/59584 +* [13.x] Refactor: add `match` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/59914 +* [13.x] Refactor: remove unnecessary call function by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/59915 +* [13.x] Refactor: improve tests by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/59912 +* Align Enumerable all, times and range [@return](https://github.com/return) with implementations by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59911 +* Align Enumerable search and flatten [@return](https://github.com/return) with implementations by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59910 +* Specify Translation Loader namespaces shape by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59913 +* Fix duplicate type key in getTypes/processTypes return shape by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59909 +* int argument for sortByDesc and Enumerable sort methods by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59907 +* Match processForeignKeys return shape to Builder::getForeignKeys by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59908 ## [v13.6.0](https://github.com/laravel/framework/compare/v13.5.0...v13.6.0) - 2026-04-21 From 3c6e03bb9cf25474ade8b7bce4b8d7b0c20dab8a Mon Sep 17 00:00:00 2001 From: JurianArie <28654085+JurianArie@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:44:03 +0200 Subject: [PATCH 269/596] [13.x] Exclude expired locks in DatabaseLock::isLock (#59948) * Check for expired locks in DatabaseLock::isLock * Don't check current owner before deletion --- src/Illuminate/Cache/DatabaseLock.php | 29 +++++++++---------- .../Integration/Database/DatabaseLockTest.php | 18 ++++++++---- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/Illuminate/Cache/DatabaseLock.php b/src/Illuminate/Cache/DatabaseLock.php index 7cac0e33f499..c865f34d78b7 100644 --- a/src/Illuminate/Cache/DatabaseLock.php +++ b/src/Illuminate/Cache/DatabaseLock.php @@ -118,24 +118,18 @@ protected function expiresAt() */ public function release() { - if ($this->isOwnedByCurrentProcess()) { - try { - $this->connection->table($this->table) - ->where('key', $this->name) - ->where('owner', $this->owner) - ->delete(); - + try { + return $this->connection->table($this->table) + ->where('key', $this->name) + ->where('owner', $this->owner) + ->delete() > 0; + } catch (Throwable $e) { + if ($this->causedByConcurrencyError($e)) { return true; - } catch (Throwable $e) { - if ($this->causedByConcurrencyError($e)) { - return true; - } - - throw $e; } - } - return false; + throw $e; + } } /** @@ -177,7 +171,10 @@ public function pruneExpiredLocks() */ protected function getCurrentOwner() { - return $this->connection->table($this->table)->where('key', $this->name)->first()?->owner; + return $this->connection->table($this->table) + ->where('key', $this->name) + ->where('expiration', '>', $this->currentTime()) + ->first()?->owner; } /** diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php index 2a670c01db8c..789f4a933ed8 100644 --- a/tests/Integration/Database/DatabaseLockTest.php +++ b/tests/Integration/Database/DatabaseLockTest.php @@ -77,6 +77,18 @@ public function testIsLocked() $this->assertFalse($lock->isLocked()); } + public function testExpiredLockIsNotLocked() + { + $lock = Cache::driver('database')->lock('foo'); + $this->assertFalse($lock->isLocked()); + + $lock->get(); + $this->assertTrue($lock->isLocked()); + + DB::table('cache_locks')->update(['expiration' => Carbon::now()->subDay()->getTimestamp()]); + $this->assertFalse($lock->isLocked()); + } + public function testOtherOwnerDoesNotOwnLockAfterRestore() { $firstLock = Cache::store('database')->lock('foo'); @@ -126,14 +138,10 @@ public function testIgnoresConcurrencyException(string $message, int $code, bool public function testReleaseIgnoresConcurrencyException(string $message, int $code, bool $hasConcurrencyError) { $connection = m::mock(Connection::class); - $selectBuilder = m::mock(Builder::class); $deleteBuilder = m::mock(Builder::class); $owner = 'owner-123'; - $selectBuilder->shouldReceive('where')->with('key', 'foo')->once()->andReturnSelf(); - $selectBuilder->shouldReceive('first')->once()->andReturn((object) ['owner' => $owner]); - $deleteBuilder->shouldReceive('where')->with('key', 'foo')->once()->andReturnSelf(); $deleteBuilder->shouldReceive('where')->with('owner', $owner)->once()->andReturnSelf(); $deleteBuilder->shouldReceive('delete')->once()->andThrow( @@ -145,7 +153,7 @@ public function testReleaseIgnoresConcurrencyException(string $message, int $cod ) ); - $connection->shouldReceive('table')->with('cache_locks')->andReturn($selectBuilder, $deleteBuilder); + $connection->shouldReceive('table')->with('cache_locks')->andReturn($deleteBuilder); $lock = new DatabaseLock($connection, 'cache_locks', 'foo', 10, $owner); // same owner... From 7961b10577ff6344102ba0a8f407e930271e5160 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:47:20 +0200 Subject: [PATCH 270/596] [13.x] Merge attribute-provided middleware with existing middleware (#59944) * Allow combining attribute-based middleware with other methods * Add test for attribute middleware merging --- src/Illuminate/Routing/Route.php | 28 +++++------ .../RoutingControllerAttributeTest.php | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index 341d07f0e36b..df460dc03545 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -1122,21 +1122,19 @@ public function controllerMiddleware() $this->getControllerMethod(), ]; - if (is_a($controllerClass, HasMiddleware::class, true)) { - return $this->staticallyProvidedControllerMiddleware( - $controllerClass, $controllerMethod - ); - } - - if (method_exists($controllerClass, 'getMiddleware')) { - return $this->controllerDispatcher()->getMiddleware( - $this->getController(), $controllerMethod - ); - } - - return $this->attributeProvidedControllerMiddleware( - $controllerClass, $controllerMethod - ); + $attributeMiddleware = $this->attributeProvidedControllerMiddleware($controllerClass, $controllerMethod); + + return match (true) { + is_a($controllerClass, HasMiddleware::class, true) => array_merge( + $this->staticallyProvidedControllerMiddleware($controllerClass, $controllerMethod), + $attributeMiddleware, + ), + method_exists($controllerClass, 'getMiddleware') => array_merge( + $this->controllerDispatcher()->getMiddleware($this->getController(), $controllerMethod), + $attributeMiddleware, + ), + default => $attributeMiddleware, + }; } /** diff --git a/tests/Routing/RoutingControllerAttributeTest.php b/tests/Routing/RoutingControllerAttributeTest.php index ba77ae2820f8..5b3fc0977585 100644 --- a/tests/Routing/RoutingControllerAttributeTest.php +++ b/tests/Routing/RoutingControllerAttributeTest.php @@ -4,7 +4,10 @@ use Illuminate\Container\Container; use Illuminate\Routing\Attributes\Controllers\Middleware; +use Illuminate\Routing\Controller as RoutingController; +use Illuminate\Routing\Controllers\HasMiddleware; use Illuminate\Routing\Route; +use Override; use PHPUnit\Framework\TestCase; class RoutingControllerAttributeTest extends TestCase @@ -24,6 +27,19 @@ public function testControllerMiddlewareAttributesAreInheritedInDeclarationOrder $this->assertEquals(['middleware1', 'middleware2', 'middleware3'], $route->gatherMiddleware()); } + + public function testControllerMiddlewareMergesWithAttributeMiddleware() + { + $route = new Route('GET', 'foo', ['uses' => StaticMiddlewareController::class.'@index']); + $route->setContainer(new Container); + + $this->assertEquals(['static-middleware', 'attribute-middleware-1', 'attribute-middleware-2'], $route->gatherMiddleware()); + + $route = new Route('GET', 'bar', ['uses' => DynamicMiddlewareController::class.'@index']); + $route->setContainer(new Container); + + $this->assertEquals(['dynamic-middleware', 'attribute-middleware-1', 'attribute-middleware-2'], $route->gatherMiddleware()); + } } abstract class Controller @@ -61,3 +77,34 @@ public function index() // } } + +#[Middleware('attribute-middleware-1')] +class StaticMiddlewareController implements HasMiddleware +{ + #[Override] + public static function middleware(): array + { + return ['static-middleware']; + } + + #[Middleware('attribute-middleware-2')] + public function index() + { + // + } +} + +#[Middleware('attribute-middleware-1')] +class DynamicMiddlewareController extends RoutingController +{ + public function __construct() + { + $this->middleware('dynamic-middleware'); + } + + #[Middleware('attribute-middleware-2')] + public function index() + { + // + } +} From 64b3ab97ecdc13c321881c9d9b436b37c3016331 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:49:16 +0600 Subject: [PATCH 271/596] Tighten getCurrentSchemaListing @return in MySQL and SQLite builders (#59942) --- src/Illuminate/Database/Schema/MySqlBuilder.php | 2 +- src/Illuminate/Database/Schema/SQLiteBuilder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index 6676411225ea..383b81e1c660 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -49,7 +49,7 @@ public function dropAllViews() /** * Get the names of current schemas for the connection. * - * @return string[]|null + * @return string[] */ public function getCurrentSchemaListing() { diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index f750e97edf56..145238ce85c5 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -164,7 +164,7 @@ public function refreshDatabaseFile($path = null) /** * Get the names of current schemas for the connection. * - * @return string[]|null + * @return string[] */ public function getCurrentSchemaListing() { From d4b8f7bb462b7a7d95d97ebdd8a457bacba797d9 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:50:05 +0600 Subject: [PATCH 272/596] Correct Repository::setStore @return to $this (#59940) --- src/Illuminate/Cache/Repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 35781779921a..1d3458a3f360 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -941,7 +941,7 @@ public function getStore() * Set the cache store implementation. * * @param \Illuminate\Contracts\Cache\Store $store - * @return static + * @return $this */ public function setStore($store) { From 9ea153c6fd28fea00891989f0842942274a73baa Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:50:37 +0600 Subject: [PATCH 273/596] Correct Limit::none() @return type to Unlimited (#59938) --- src/Illuminate/Cache/RateLimiting/Limit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/RateLimiting/Limit.php b/src/Illuminate/Cache/RateLimiting/Limit.php index 351bbf11fb8f..1aae4694ffea 100644 --- a/src/Illuminate/Cache/RateLimiting/Limit.php +++ b/src/Illuminate/Cache/RateLimiting/Limit.php @@ -116,7 +116,7 @@ public static function perDay($maxAttempts, $decayDays = 1) /** * Create a new unlimited rate limit. * - * @return static + * @return \Illuminate\Cache\RateLimiting\Unlimited */ public static function none() { From 09b4defc58925b98dacc361694606fb5e77bcbbd Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:50:54 +0600 Subject: [PATCH 274/596] Add collation to processColumns and getColumns return shape (#59937) --- src/Illuminate/Database/Query/Processors/Processor.php | 2 +- src/Illuminate/Database/Schema/Builder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index d027d0cb9fc7..758b80c1e598 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -113,7 +113,7 @@ public function processTypes($results) * Process the results of a columns query. * * @param list> $results - * @return list + * @return list */ public function processColumns($results) { diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 006e60d3c22b..2a15b13417b9 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -394,7 +394,7 @@ public function getColumnListing($table) * Get the columns for a given table. * * @param string $table - * @return list + * @return list */ public function getColumns($table) { From 1a5a679065e6e7817def5559a5645e26998d488f Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:51:31 +0600 Subject: [PATCH 275/596] Mark processViews schema field nullable in @return shape (#59941) --- src/Illuminate/Database/Query/Processors/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 758b80c1e598..12187f2cf5ac 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -82,7 +82,7 @@ public function processTables($results) * Process the results of a views query. * * @param list> $results - * @return list + * @return list */ public function processViews($results) { From 5a49532be38eb7ccf3064d3ebd81de42beff5653 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Thu, 30 Apr 2026 16:53:04 +0500 Subject: [PATCH 276/596] Improve docblock wording in AurthorizationException (#59930) Co-authored-by: Muhammad Talha --- src/Illuminate/Auth/Access/AuthorizationException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Auth/Access/AuthorizationException.php b/src/Illuminate/Auth/Access/AuthorizationException.php index 1dd157e34e94..bc4cd525ef06 100644 --- a/src/Illuminate/Auth/Access/AuthorizationException.php +++ b/src/Illuminate/Auth/Access/AuthorizationException.php @@ -8,7 +8,7 @@ class AuthorizationException extends Exception { /** - * The response from the gate. + * The authorization response returned by the gate. * * @var \Illuminate\Auth\Access\Response */ From 3e1d3f7025614ae39ac12ebf7c303d1efc657483 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 30 Apr 2026 12:57:11 +0100 Subject: [PATCH 277/596] [13.x] Add Worker Pausing/Resuming events (#59895) * 13.x add ability to know if its been paused / resumed * use --- src/Illuminate/Queue/Events/WorkerPausing.php | 18 ++++++++++++++++++ src/Illuminate/Queue/Events/WorkerResuming.php | 18 ++++++++++++++++++ src/Illuminate/Queue/Worker.php | 15 +++++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Queue/Events/WorkerPausing.php create mode 100644 src/Illuminate/Queue/Events/WorkerResuming.php diff --git a/src/Illuminate/Queue/Events/WorkerPausing.php b/src/Illuminate/Queue/Events/WorkerPausing.php new file mode 100644 index 000000000000..df8445d01f7e --- /dev/null +++ b/src/Illuminate/Queue/Events/WorkerPausing.php @@ -0,0 +1,18 @@ + $this->paused = true); - pcntl_signal(SIGCONT, fn () => $this->paused = false); + pcntl_signal(SIGUSR2, function () use ($queue, $connectionName) { + $this->paused = true; + + $this->events->dispatch(new WorkerPausing($connectionName, $queue)); + }); + + pcntl_signal(SIGCONT, function () use ($connectionName, $queue) { + $this->paused = false; + + $this->events->dispatch(new WorkerResuming($connectionName, $queue)); + }); } /** From 97c2865adb1cfe1dbc678f028f61a3cf6679b713 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Fri, 1 May 2026 14:44:04 +0200 Subject: [PATCH 278/596] Allow PHPStan to infer the pivot type when passing the pivot model directly (#59959) --- .../Eloquent/Concerns/HasRelationships.php | 6 ++++-- types/Database/Eloquent/Relations.php | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 76c46e4f2777..3659aa574995 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -678,15 +678,17 @@ protected function newMorphMany(Builder $query, Model $parent, $type, $id, $loca * Define a many-to-many relationship. * * @template TRelatedModel of \Illuminate\Database\Eloquent\Model + * @template TPivotModel of \Illuminate\Database\Eloquent\Relations\Pivot = \Illuminate\Database\Eloquent\Relations\Pivot + * @template TPivotTable of string|null * * @param class-string $related - * @param string|class-string<\Illuminate\Database\Eloquent\Model>|null $table + * @param class-string|TPivotTable $table * @param string|null $foreignPivotKey * @param string|null $relatedPivotKey * @param string|null $parentKey * @param string|null $relatedKey * @param string|null $relation - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function belongsToMany( $related, diff --git a/types/Database/Eloquent/Relations.php b/types/Database/Eloquent/Relations.php index a9d305707c43..d91ce7d2a95e 100644 --- a/types/Database/Eloquent/Relations.php +++ b/types/Database/Eloquent/Relations.php @@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; use function PHPStan\Testing\assertType; @@ -166,6 +167,18 @@ public function roles(): BelongsToMany return $belongsToMany; } + /** @return BelongsToMany */ + public function tenantRoles(): BelongsToMany + { + $belongsToMany = $this->belongsToMany(Role::class)->using(Tenant::class); + assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany', $belongsToMany); + + $belongsToManyShorthand = $this->belongsToMany(Role::class, Tenant::class); + assertType('Illuminate\Database\Eloquent\Relations\BelongsToMany', $belongsToManyShorthand); + + return $belongsToMany; + } + /** @return HasOne */ public function mechanic(): HasOne { @@ -352,6 +365,9 @@ class Address extends Model class Role extends Model { } +class Tenant extends Pivot +{ +} class Car extends Model { } From e25d8ed700eb5e36c4c051002969b847aec015db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Fleury?= <23384755+noefleury@users.noreply.github.com> Date: Fri, 1 May 2026 14:44:38 +0200 Subject: [PATCH 279/596] [12.x] Fix infinite recursion when defining model scope with attribute as private (#59958) * [12.x] Fix infinite recursion when defining model scope with attribute as private * Fix style --- src/Illuminate/Database/Eloquent/Model.php | 10 +++++++--- .../Integration/Database/EloquentModelScopeTest.php | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 2b6deb58333f..d0b990bcb830 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1765,9 +1765,13 @@ public function callNamedScope($scope, array $parameters = []) */ protected static function isScopeMethodWithAttribute(string $method) { - return method_exists(static::class, $method) && - (new ReflectionMethod(static::class, $method)) - ->getAttributes(LocalScope::class) !== []; + if (method_exists(static::class, $method)) { + $reflectionClass = new ReflectionMethod(static::class, $method); + + return ! $reflectionClass->isPrivate() && $reflectionClass->getAttributes(LocalScope::class) !== []; + } + + return false; } /** diff --git a/tests/Integration/Database/EloquentModelScopeTest.php b/tests/Integration/Database/EloquentModelScopeTest.php index 8fca382000a7..83d151c1b3d0 100644 --- a/tests/Integration/Database/EloquentModelScopeTest.php +++ b/tests/Integration/Database/EloquentModelScopeTest.php @@ -28,6 +28,13 @@ public function testModelHasAttributedScope() $this->assertTrue($model->hasNamedScope('existsAsWell')); } + + public function testModelDoesNotHaveScopeWhenPrivateVisibility() + { + $model = new TestScopeModel1; + + $this->assertFalse($model->hasNamedScope('existsAsPrivate')); + } } class TestScopeModel1 extends Model @@ -42,4 +49,10 @@ protected function existsAsWell(Builder $builder) { return $builder; } + + #[Scope] + private function existsAsPrivate(Builder $builder) + { + return $builder; + } } From b9b077bf33e7fa6d033770698f9398fc2c060635 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:09:24 +0600 Subject: [PATCH 280/596] Document missing $health param on ApplicationBuilder::withRouting (#59968) --- src/Illuminate/Foundation/Configuration/ApplicationBuilder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index b7e189eb95ca..d93ed8f81adc 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -147,6 +147,7 @@ public function withBroadcasting(string $channels, array $attributes = []) * @param string|null $commands * @param string|null $channels * @param string|null $pages + * @param string|null $health * @param string $apiPrefix * @param callable|null $then * @return $this From 62820aad3e8ef48464d35336645731a479fb4c1c Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:09:57 +0600 Subject: [PATCH 281/596] Correct Log\Context\Repository::handleUnserializeExceptionsUsing @return to $this (#59965) --- src/Illuminate/Log/Context/Repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Log/Context/Repository.php b/src/Illuminate/Log/Context/Repository.php index 4950639a0821..5b8158270109 100644 --- a/src/Illuminate/Log/Context/Repository.php +++ b/src/Illuminate/Log/Context/Repository.php @@ -607,7 +607,7 @@ public function hydrated($callback) * Handle unserialize exceptions using the given callback. * * @param callable|null $callback - * @return static + * @return $this */ public function handleUnserializeExceptionsUsing($callback) { From 370cfa53d8f06d47a92d19b82630ddc38c72361b Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:10:09 +0600 Subject: [PATCH 282/596] Correct Attribute caching toggles @return to $this (#59962) --- src/Illuminate/Database/Eloquent/Casts/Attribute.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Casts/Attribute.php b/src/Illuminate/Database/Eloquent/Casts/Attribute.php index 26d13ba3fbe3..41a85a66168a 100644 --- a/src/Illuminate/Database/Eloquent/Casts/Attribute.php +++ b/src/Illuminate/Database/Eloquent/Casts/Attribute.php @@ -81,7 +81,7 @@ public static function set(callable $set) /** * Disable object caching for the attribute. * - * @return static + * @return $this */ public function withoutObjectCaching() { @@ -93,7 +93,7 @@ public function withoutObjectCaching() /** * Enable caching for the attribute. * - * @return static + * @return $this */ public function shouldCache() { From 4727569b831d0bb5596889f45ea273d91b2e67de Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:10:18 +0600 Subject: [PATCH 283/596] Correct Factory::configure @return to $this (#59963) --- src/Illuminate/Database/Eloquent/Factories/Factory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index b2dcba24a34c..8f016849daf1 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -230,7 +230,7 @@ public static function times(int $count) /** * Configure the factory. * - * @return static + * @return $this */ public function configure() { From 92fb84242d25a320985164c379765e8893c1b566 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:10:36 +0600 Subject: [PATCH 284/596] Correct Translator::handleMissingKeysUsing @return to $this (#59964) --- src/Illuminate/Translation/Translator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index 7465895b0a1e..12f18dd868ef 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -386,7 +386,7 @@ protected function handleMissingTranslationKey($key, $replace, $locale, $fallbac * Register a callback that is responsible for handling missing translation keys. * * @param callable|null $callback - * @return static + * @return $this */ public function handleMissingKeysUsing(?callable $callback) { From 1ac44e1e8fb47ebcdea90971b986ca247ecd3d1c Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:10:49 +0600 Subject: [PATCH 285/596] Correct Password::min @return to static (#59967) --- src/Illuminate/Validation/Rules/Password.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 9e08b9b7d65c..e4c5eadbb3b2 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -230,7 +230,7 @@ public function setData($data) * Set the minimum size of the password. * * @param int $size - * @return $this + * @return static */ public static function min($size) { From 70b6d48c2f2c61b675fec30f97cfbbaab3963af6 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:12:02 +0600 Subject: [PATCH 286/596] Mark processIndexes type field nullable in @return shape (#59961) --- src/Illuminate/Database/Query/Processors/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 12187f2cf5ac..3670f7d3682c 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -124,7 +124,7 @@ public function processColumns($results) * Process the results of an indexes query. * * @param list> $results - * @return list, type: string, unique: bool, primary: bool}> + * @return list, type: string|null, unique: bool, primary: bool}> */ public function processIndexes($results) { From 5f127cfd33de5c11a26f44f2caa3e66f18e50fb5 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Fri, 1 May 2026 16:12:29 -0400 Subject: [PATCH 287/596] Add `assertSessionMissingInput` (#59970) --- src/Illuminate/Testing/TestResponse.php | 24 ++++++++++++++++++++++++ tests/Testing/TestResponseTest.php | 14 ++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index e7c08da98430..93b4242c1646 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -1642,6 +1642,30 @@ public function assertSessionHasInput($key, $value = null) return $this; } + /** + * Assert that the session is missing a given key in the flashed input array. + * + * @param string|array $key + * @return $this + */ + public function assertSessionMissingInput($key) + { + if (is_array($key)) { + foreach ($key as $k) { + $this->assertSessionMissingInput($k); + } + + return $this; + } + + PHPUnit::withResponse($this)->assertFalse( + $this->session()->hasOldInput($key), + "Session has unexpected key [{$key}]." + ); + + return $this; + } + /** * Assert that the session has the given errors. * diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index ac7401d5ee8c..95e9cc0c37f0 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -3138,6 +3138,20 @@ public function testAssertSessionHasInput(): void }); } + public function testAssertSessionMissingInput(): void + { + app()->instance('session.store', $store = new Store('test-session', new ArraySessionHandler(1))); + + $store->put('_old_input', [ + 'foo' => 'value', + ]); + + $response = TestResponse::fromBaseResponse(new Response()); + + $response->assertSessionMissingInput('bar'); + $response->assertSessionMissingInput(['bar', 'baz']); + } + public function testGetEncryptedCookie(): void { $container = Container::getInstance(); From 6d0b518cbcf14811a091a60920694aa7090ac206 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 2 May 2026 02:12:40 +0600 Subject: [PATCH 288/596] Mark generation type field nullable in processColumns @return shape (#59960) --- src/Illuminate/Database/Query/Processors/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 3670f7d3682c..410bdee04377 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -113,7 +113,7 @@ public function processTypes($results) * Process the results of a columns query. * * @param list> $results - * @return list + * @return list */ public function processColumns($results) { From 1392107aa82398743e91add54d214806a3f147dc Mon Sep 17 00:00:00 2001 From: JurianArie <28654085+JurianArie@users.noreply.github.com> Date: Mon, 4 May 2026 14:33:29 +0200 Subject: [PATCH 289/596] Allow custom on delete/update strings (#59986) --- src/Illuminate/Database/Schema/ForeignKeyDefinition.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Schema/ForeignKeyDefinition.php b/src/Illuminate/Database/Schema/ForeignKeyDefinition.php index cd17896560a5..310c3728d4c7 100644 --- a/src/Illuminate/Database/Schema/ForeignKeyDefinition.php +++ b/src/Illuminate/Database/Schema/ForeignKeyDefinition.php @@ -9,8 +9,8 @@ * @method ForeignKeyDefinition initiallyImmediate(bool $value = true) Set the default time to check the constraint (PostgreSQL) * @method ForeignKeyDefinition lock(('none'|'shared'|'default'|'exclusive') $value) Specify the DDL lock mode for the foreign key operation (MySQL) * @method ForeignKeyDefinition on(string $table) Specify the referenced table - * @method ForeignKeyDefinition onDelete(('cascade'|'restrict'|'set null'|'no action') $action) Add an ON DELETE action - * @method ForeignKeyDefinition onUpdate(('cascade'|'restrict'|'set null'|'no action') $action) Add an ON UPDATE action + * @method ForeignKeyDefinition onDelete(string $action) Add an ON DELETE action + * @method ForeignKeyDefinition onUpdate(string $action) Add an ON UPDATE action * @method ForeignKeyDefinition references(string|string[] $columns) Specify the referenced column(s) */ class ForeignKeyDefinition extends Fluent From 9bfbcee398c1f0958d0fbfab70f89e84c69ab289 Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Mon, 4 May 2026 14:34:24 +0200 Subject: [PATCH 290/596] Allow mail default driver to accept enums (#59973) --- src/Illuminate/Mail/MailManager.php | 6 ++++-- tests/Mail/MailManagerTest.php | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index b581e46e0f6b..e55c17bb83a6 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -559,11 +559,13 @@ public function getDefaultDriver() /** * Set the default mail driver name. * - * @param string $name + * @param \UnitEnum|string $name * @return void */ - public function setDefaultDriver(string $name) + public function setDefaultDriver($name) { + $name = enum_value($name); + if ($this->app['config']['mail.driver']) { $this->app['config']['mail.driver'] = $name; } diff --git a/tests/Mail/MailManagerTest.php b/tests/Mail/MailManagerTest.php index 1b62064c7f19..4a64e68cb58a 100644 --- a/tests/Mail/MailManagerTest.php +++ b/tests/Mail/MailManagerTest.php @@ -149,6 +149,17 @@ public function testMailManagerCanResolveBackedEnumDriver(): void $this->assertSame($mailer1, $mailer2); } + public function testSetDefaultDriverAcceptsBackedEnum(): void + { + $this->app['config']->set('mail.mailers.array', [ + 'transport' => 'array', + ]); + + $this->app['mail.manager']->setDefaultDriver(MailerName::ArrayMailer); + + $this->assertSame('array', $this->app['config']->get('mail.default')); + } + public function testPurgeAcceptsBackedEnum(): void { $this->app['config']->set('mail.mailers.array', [ From 671bd37f35ca292adb6b8cba2f7c8f8cf41d0e02 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 4 May 2026 12:34:54 +0000 Subject: [PATCH 291/596] Update facade docblocks --- src/Illuminate/Support/Facades/Mail.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Mail.php b/src/Illuminate/Support/Facades/Mail.php index a3ccceb00768..972e83cf6dcf 100755 --- a/src/Illuminate/Support/Facades/Mail.php +++ b/src/Illuminate/Support/Facades/Mail.php @@ -10,7 +10,7 @@ * @method static \Illuminate\Mail\Mailer build(array $config) * @method static \Symfony\Component\Mailer\Transport\TransportInterface createSymfonyTransport(array $config) * @method static string getDefaultDriver() - * @method static void setDefaultDriver(string $name) + * @method static void setDefaultDriver(\UnitEnum|string $name) * @method static void purge(\UnitEnum|string|null $name = null) * @method static \Illuminate\Mail\MailManager extend(string $driver, \Closure $callback) * @method static \Illuminate\Contracts\Foundation\Application getApplication() From 26f92f2e4af2f1d2665d0c86479e0e4db35edb33 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 4 May 2026 13:35:50 +0100 Subject: [PATCH 292/596] [12.x] Fix infinite recursion when defining model scope with attribute as private (#59958) (#59979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [12.x] Fix infinite recursion when defining model scope with attribute as private * Fix style Co-authored-by: Noé Fleury <23384755+noefleury@users.noreply.github.com> --- src/Illuminate/Database/Eloquent/Model.php | 10 +++++++--- .../Integration/Database/EloquentModelScopeTest.php | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 78a05ce62ae0..cb36dd411b36 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1989,9 +1989,13 @@ public function callNamedScope($scope, array $parameters = []) */ protected static function isScopeMethodWithAttribute(string $method) { - return method_exists(static::class, $method) && - (new ReflectionMethod(static::class, $method)) - ->getAttributes(LocalScope::class) !== []; + if (method_exists(static::class, $method)) { + $reflectionClass = new ReflectionMethod(static::class, $method); + + return ! $reflectionClass->isPrivate() && $reflectionClass->getAttributes(LocalScope::class) !== []; + } + + return false; } /** diff --git a/tests/Integration/Database/EloquentModelScopeTest.php b/tests/Integration/Database/EloquentModelScopeTest.php index 8fca382000a7..83d151c1b3d0 100644 --- a/tests/Integration/Database/EloquentModelScopeTest.php +++ b/tests/Integration/Database/EloquentModelScopeTest.php @@ -28,6 +28,13 @@ public function testModelHasAttributedScope() $this->assertTrue($model->hasNamedScope('existsAsWell')); } + + public function testModelDoesNotHaveScopeWhenPrivateVisibility() + { + $model = new TestScopeModel1; + + $this->assertFalse($model->hasNamedScope('existsAsPrivate')); + } } class TestScopeModel1 extends Model @@ -42,4 +49,10 @@ protected function existsAsWell(Builder $builder) { return $builder; } + + #[Scope] + private function existsAsPrivate(Builder $builder) + { + return $builder; + } } From 5cbe7eb2313a9a8ecf211e39917d7ecc8d7d8951 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 4 May 2026 08:05:26 -0500 Subject: [PATCH 293/596] normalize to string --- .../Queue/Attributes/Connection.php | 4 +- src/Illuminate/Queue/Attributes/Queue.php | 4 +- .../Integration/Queue/JobDispatchingTest.php | 32 ++++++++ tests/Queue/QueueAttributesTest.php | 76 +++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 tests/Queue/QueueAttributesTest.php diff --git a/src/Illuminate/Queue/Attributes/Connection.php b/src/Illuminate/Queue/Attributes/Connection.php index c5b1db67ccc5..4c65cdecfa84 100644 --- a/src/Illuminate/Queue/Attributes/Connection.php +++ b/src/Illuminate/Queue/Attributes/Connection.php @@ -5,6 +5,8 @@ use Attribute; use UnitEnum; +use function Illuminate\Support\enum_value; + #[Attribute(Attribute::TARGET_CLASS)] class Connection { @@ -15,6 +17,6 @@ class Connection */ public function __construct(public UnitEnum|string $connection) { - // + $this->connection = enum_value($connection); } } diff --git a/src/Illuminate/Queue/Attributes/Queue.php b/src/Illuminate/Queue/Attributes/Queue.php index a1892e99c32c..c286416c1d9a 100644 --- a/src/Illuminate/Queue/Attributes/Queue.php +++ b/src/Illuminate/Queue/Attributes/Queue.php @@ -5,6 +5,8 @@ use Attribute; use UnitEnum; +use function Illuminate\Support\enum_value; + #[Attribute(Attribute::TARGET_CLASS)] class Queue { @@ -15,6 +17,6 @@ class Queue */ public function __construct(public UnitEnum|string $queue) { - // + $this->queue = enum_value($queue); } } diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index 441cb59dea97..e74c2ade6443 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\Attributes\Queue as QueueAttribute; use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\JobQueueing; use Illuminate\Queue\InteractsWithQueue; @@ -215,6 +216,26 @@ public function testCanDisableDispatchingAfterResponse() $this->assertTrue(Job::$ran); } + public function testQueueAttributeWithEnumNormalizesToStringInJobQueuedEvent() + { + Config::set('queue.default', 'database'); + $events = []; + $this->app['events']->listen(function (JobQueueing $e) use (&$events) { + $events[] = $e; + }); + $this->app['events']->listen(function (JobQueued $e) use (&$events) { + $events[] = $e; + }); + + JobWithEnumQueueAttribute::dispatch(); + + $this->assertCount(2, $events); + $this->assertInstanceOf(JobQueueing::class, $events[0]); + $this->assertSame('default', $events[0]->queue); + $this->assertInstanceOf(JobQueued::class, $events[1]); + $this->assertSame('default', $events[1]->queue); + } + /** * Helpers. */ @@ -263,3 +284,14 @@ class MyTestDispatchableJob implements ShouldQueue { use Dispatchable; } + +enum JobDispatchingTestQueueEnum: string +{ + case DEFAULT = 'default'; +} + +#[QueueAttribute(JobDispatchingTestQueueEnum::DEFAULT)] +class JobWithEnumQueueAttribute implements ShouldQueue +{ + use Dispatchable; +} diff --git a/tests/Queue/QueueAttributesTest.php b/tests/Queue/QueueAttributesTest.php new file mode 100644 index 000000000000..a3c89ad42e7a --- /dev/null +++ b/tests/Queue/QueueAttributesTest.php @@ -0,0 +1,76 @@ +assertSame('default', $attribute->queue); + } + + public function test_queue_attribute_normalizes_unit_enum_to_string() + { + $attribute = new Queue(QueueAttributeUnitEnum::High); + + $this->assertSame('High', $attribute->queue); + } + + public function test_queue_attribute_keeps_string_as_string() + { + $attribute = new Queue('high'); + + $this->assertSame('high', $attribute->queue); + } + + public function test_connection_attribute_normalizes_backed_enum_to_string() + { + $attribute = new Connection(ConnectionAttributeBackedEnum::REDIS); + + $this->assertSame('redis', $attribute->connection); + } + + public function test_connection_attribute_normalizes_unit_enum_to_string() + { + $attribute = new Connection(ConnectionAttributeUnitEnum::Redis); + + $this->assertSame('Redis', $attribute->connection); + } + + public function test_connection_attribute_keeps_string_as_string() + { + $attribute = new Connection('redis'); + + $this->assertSame('redis', $attribute->connection); + } +} + +enum QueueAttributeBackedEnum: string +{ + case DEFAULT = 'default'; + case HIGH = 'high'; +} + +enum QueueAttributeUnitEnum +{ + case High; + case Default; +} + +enum ConnectionAttributeBackedEnum: string +{ + case REDIS = 'redis'; + case SQS = 'sqs'; +} + +enum ConnectionAttributeUnitEnum +{ + case Redis; + case Sqs; +} From 596565170554b17298d075ecf3aac76423a28257 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 5 May 2026 13:56:29 +0100 Subject: [PATCH 294/596] [13.x] Add an environment filter to the `schedule:list` command (#59993) * Filter the schedule:list output to show tasks which run on the specified environments * Move the environment filtering logic onto the scheduler, so it's more accessible * Move tests around * Update phpdoc * OS-agnostic assertion (windows outputs " vs ' on mac etc) * Shorten the logic using array_any. Add polyfill to the console to provide it on php8.3. --- .../Console/Scheduling/Schedule.php | 14 +++++ .../Scheduling/ScheduleListCommand.php | 10 ++- src/Illuminate/Console/composer.json | 1 + tests/Console/Scheduling/ScheduleTest.php | 25 ++++++++ .../Scheduling/ScheduleListCommandTest.php | 61 ++++++++++++++++++- 5 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 653a0e4d7a7d..14f262bbb4c0 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -429,6 +429,20 @@ public function events() return $this->events; } + /** + * Get all of the events on the schedule which run on any of the provided environments. + * + * @param list $environments + * @return \Illuminate\Console\Scheduling\Event[] + */ + public function eventsForEnvironments(array $environments): array + { + return array_values(array_filter( + $this->events(), + static fn (Event $event) => array_any($environments, $event->runsInEnvironment(...)) + )); + } + /** * Specify the cache store that should be used to store mutexes. * diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index bc42dfcf20cc..15f4a572467d 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -6,6 +6,7 @@ use Cron\CronExpression; use DateTimeZone; use Illuminate\Console\Command; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use ReflectionClass; @@ -23,6 +24,7 @@ class ScheduleListCommand extends Command */ protected $signature = 'schedule:list {--timezone= : The timezone that times should be displayed in} + {--environment=* : Display the tasks scheduled to run on this environment} {--next : Sort the listed tasks by their next due date} {--json : Output the scheduled tasks as JSON} '; @@ -51,7 +53,13 @@ class ScheduleListCommand extends Command */ public function handle(Schedule $schedule) { - $events = new Collection($schedule->events()); + $environments = Arr::wrap($this->option('environment')); + + $events = new Collection( + empty($environments) + ? $schedule->events() + : $schedule->eventsForEnvironments($environments) + ); if ($events->isEmpty()) { if ($this->option('json')) { diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index b850977720db..2ac0ac5e2bf2 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -24,6 +24,7 @@ "laravel/prompts": "^0.3.0", "nunomaduro/termwind": "^2.0", "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.37", "symfony/process": "^7.4.5 || ^8.0.5" }, "suggest": { diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index 3354974023db..1071f543208d 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -64,4 +64,29 @@ public function testJobIsNotInstantiatedIfSuppliedAsClassname(): void $this->assertSame(JobToTestWithSchedule::class, $scheduledJob->description); $this->assertFalse($this->container->resolved(JobToTestWithSchedule::class)); } + + public function testItCanFilterEventsByEnvironments(): void + { + $schedule = new Schedule(); + $schedule->job(JobToTestWithSchedule::class)->environments('production')->daily(); + $schedule->command('inspire')->environments(['staging', 'production'])->everyMinute(); + $schedule->command('foobar', ['a' => 'b'])->environments(['local', 'uat'])->everyMinute(); + $schedule->command('foobar')->hourly(); + + $filteredEvents = $schedule->eventsForEnvironments(['production', 'staging']); + + $this->assertCount(3, $filteredEvents); + + $this->assertSame(JobToTestWithSchedule::class, $filteredEvents[0]->description); + $this->assertSame(['production'], $filteredEvents[0]->environments); + $this->assertSame('0 0 * * *', $filteredEvents[0]->expression); + + $this->assertMatchesRegularExpression('/artisan.*inspire$/', $filteredEvents[1]->command); + $this->assertSame(['staging', 'production'], $filteredEvents[1]->environments); + $this->assertSame('* * * * *', $filteredEvents[1]->expression); + + $this->assertMatchesRegularExpression('/artisan.*foobar$/', $filteredEvents[2]->command); + $this->assertSame([], $filteredEvents[2]->environments); + $this->assertSame('0 * * * *', $filteredEvents[2]->expression); + } } diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index dbb72958c01d..884c948c1ea3 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -120,7 +120,7 @@ public function testDisplayScheduleAsJson() $this->assertStringContainsString('ScheduleListCommandTest.php', $data[8]['command']); } - public function testDisplayScheduleAsJsonWithSpecificEnvironment() + public function testDisplayScheduleAsJsonWithEnvironmentData() { $environment = 'production'; $this->schedule->command(FooCommand::class)->quarterly()->environments($environment); @@ -138,6 +138,65 @@ public function testDisplayScheduleAsJsonWithSpecificEnvironment() $this->assertContains($environment, $data[0]['environments']); } + public function testDisplayScheduleWithEnvironmentFilterAsJson() + { + $this->schedule->command(FooCommand::class)->environments('production')->everyMinute(); + $this->schedule->command('inspire')->environments('local')->everyTwoMinutes(); + $this->schedule->job(FooJob::class)->everyFiveMinutes(); + + $this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [ + '--environment' => 'production', + '--json' => true, + ]); + + $output = Artisan::output(); + + $this->assertJson($output); + $data = json_decode($output, true); + + $this->assertIsArray($data); + $this->assertCount(2, $data); + + $this->assertSame('* * * * *', $data[0]['expression']); + $this->assertSame('php artisan foo:command', $data[0]['command']); + $this->assertSame(['production'], $data[0]['environments']); + + $this->assertSame('*/5 * * * *', $data[1]['expression']); + $this->assertSame('Illuminate\Tests\Integration\Console\Scheduling\FooJob', $data[1]['command']); + $this->assertSame([], $data[1]['environments']); + } + + public function testDisplayScheduleWithMultipleEnvironmentFilterAsJson() + { + $this->schedule->command(FooCommand::class)->environments('production')->everyMinute(); + $this->schedule->command('foobar', ['a' => 'b'])->environments(['staging', 'local'])->everyTwoMinutes(); + $this->schedule->command('inspire')->environments('local')->everyFiveMinutes(); + $this->schedule->job(FooJob::class)->everyTenMinutes(); + + $this->withoutMockingConsoleOutput() + ->artisan('schedule:list --environment=staging --environment=local --json'); + + $output = Artisan::output(); + + $this->assertJson($output); + $data = json_decode($output, true); + + $this->assertIsArray($data); + $this->assertCount(3, $data); + + $this->assertSame('*/2 * * * *', $data[0]['expression']); + $this->assertSame('php artisan foobar a='.ProcessUtils::escapeArgument('b'), $data[0]['command']); + $this->assertSame(['staging', 'local'], $data[0]['environments']); + + $this->assertSame('*/5 * * * *', $data[1]['expression']); + $this->assertSame('php artisan inspire', $data[1]['command']); + $this->assertSame(['local'], $data[1]['environments']); + + $this->assertSame('*/10 * * * *', $data[2]['expression']); + $this->assertSame('Illuminate\Tests\Integration\Console\Scheduling\FooJob', $data[2]['command']); + $this->assertSame([], $data[2]['environments']); + } + public function testDisplayScheduleWithSortAsJson() { $this->schedule->command(FooCommand::class)->quarterly(); From 99064da886df50059d206295b6fe84d1b393a4e0 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 5 May 2026 12:57:07 +0000 Subject: [PATCH 295/596] Update facade docblocks --- src/Illuminate/Support/Facades/Schedule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index 86a2c02e6933..0c0024d02f35 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -14,6 +14,7 @@ * @method static bool serverShouldRun(\Illuminate\Console\Scheduling\Event $event, \DateTimeInterface $time) * @method static \Illuminate\Support\Collection dueEvents(\Illuminate\Contracts\Foundation\Application $app) * @method static \Illuminate\Console\Scheduling\Event[] events() + * @method static \Illuminate\Console\Scheduling\Event[] eventsForEnvironments(array $environments) * @method static \Illuminate\Console\Scheduling\Schedule useCache(\UnitEnum|string $store) * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) From 9f448244b04d82ac407de6de9dd6c6f859a59440 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 5 May 2026 14:58:01 +0200 Subject: [PATCH 296/596] [13.x] Add generic result type to collection min/max methods (#59991) * Add generic result type to collection min/max methods * Extend min/max return type to also infer return type on null (no argument) * Use type differing from value in min/max type check to be more specific in testing --- src/Illuminate/Collections/Enumerable.php | 12 ++++++++---- .../Collections/Traits/EnumeratesValues.php | 12 ++++++++---- types/Support/Collection.php | 16 ++++++++-------- types/Support/LazyCollection.php | 16 ++++++++-------- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index f6c447d246e7..731cefc8d03a 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -805,16 +805,20 @@ public function union($items); /** * Get the min value of a given key. * - * @param (callable(TValue):mixed)|string|null $callback - * @return mixed + * @template TMinResult = mixed + * + * @param (callable(TValue): TMinResult)|string|null $callback + * @return ($callback is callable ? ?TMinResult : ($callback is null ? ?TValue : mixed)) */ public function min($callback = null); /** * Get the max value of a given key. * - * @param (callable(TValue):mixed)|string|null $callback - * @return mixed + * @template TMaxResult = mixed + * + * @param (callable(TValue): TMaxResult)|string|null $callback + * @return ($callback is callable ? ?TMaxResult : ($callback is null ? ?TValue : mixed)) */ public function max($callback = null); diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index 2b950148273a..fd20a2cc90ad 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -486,8 +486,10 @@ public function mapInto($class) /** * Get the min value of a given key. * - * @param (callable(TValue):mixed)|string|null $callback - * @return mixed + * @template TMinResult = mixed + * + * @param (callable(TValue): TMinResult)|string|null $callback + * @return ($callback is callable ? ?TMinResult : ($callback is null ? ?TValue : mixed)) */ public function min($callback = null) { @@ -501,8 +503,10 @@ public function min($callback = null) /** * Get the max value of a given key. * - * @param (callable(TValue):mixed)|string|null $callback - * @return mixed + * @template TMaxResult = mixed + * + * @param (callable(TValue): TMaxResult)|string|null $callback + * @return ($callback is callable ? ?TMaxResult : ($callback is null ? ?TValue : mixed)) */ public function max($callback = null) { diff --git a/types/Support/Collection.php b/types/Support/Collection.php index 09227ea84cc8..fe7a48365e27 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -659,24 +659,24 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection::make([1])->union([1])); assertType('Illuminate\Support\Collection', $collection::make(['string' => 'string'])->union(['string' => 'string'])); -assertType('mixed', $collection::make()->min()); -assertType('mixed', $collection::make([1])->min()); +assertType('null', $collection::make()->min()); +assertType('int|null', $collection::make([1])->min()); assertType('mixed', $collection::make([1])->min('string')); assertType('mixed', $collection::make(['string' => 1])->min('string')); -assertType('mixed', $collection::make([1])->min(function ($int) { +assertType("'foo'|null", $collection::make([1])->min(function ($int) { assertType('int', $int); - return 1; + return 'foo'; })); assertType('mixed', $collection::make([new User])->min('id')); -assertType('mixed', $collection::make()->max()); -assertType('mixed', $collection::make([1])->max()); +assertType('null', $collection::make()->max()); +assertType('int|null', $collection::make([1])->max()); assertType('mixed', $collection::make([1])->max('string')); -assertType('mixed', $collection::make([1])->max(function ($int) { +assertType("'foo'|null", $collection::make([1])->max(function ($int) { assertType('int', $int); - return 1; + return 'foo'; })); assertType('mixed', $collection::make([new User])->max('id')); diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index 30f30eeea905..7f4b94f72331 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -551,23 +551,23 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection', $collection::make([1])->union([1])); assertType('Illuminate\Support\LazyCollection', $collection::make(['string' => 'string'])->union(['string' => 'string'])); -assertType('mixed', $collection::make()->min()); -assertType('mixed', $collection::make([1])->min()); +assertType('null', $collection::make()->min()); +assertType('int|null', $collection::make([1])->min()); assertType('mixed', $collection::make([1])->min('string')); -assertType('mixed', $collection::make([1])->min(function ($int) { +assertType("'foo'|null", $collection::make([1])->min(function ($int) { assertType('int', $int); - return 1; + return 'foo'; })); assertType('mixed', $collection::make([new User])->min('id')); -assertType('mixed', $collection::make()->max()); -assertType('mixed', $collection::make([1])->max()); +assertType('null', $collection::make()->max()); +assertType('int|null', $collection::make([1])->max()); assertType('mixed', $collection::make([1])->max('string')); -assertType('mixed', $collection::make([1])->max(function ($int) { +assertType("'foo'|null", $collection::make([1])->max(function ($int) { assertType('int', $int); - return 1; + return 'foo'; })); assertType('mixed', $collection::make([new User])->max('id')); From 05f47d487ab766e8372d9cbf885f4cd1b0d1973a Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 5 May 2026 14:59:32 +0200 Subject: [PATCH 297/596] Drop 12.x release notes and update heading (#59987) --- CHANGELOG.md | 1778 +------------------------------------------------- 1 file changed, 1 insertion(+), 1777 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d03e7d6f35c6..62b2d25424db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Release Notes for 12.x +# Release Notes for 13.x ## [Unreleased](https://github.com/laravel/framework/compare/v13.7.0...13.x) @@ -382,1779 +382,3 @@ * [12.x] Display file path and line number for closure routes in `route:list` by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/59237 * [12.x] Add wantsMarkdown() and acceptsMarkdown() request methods by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/laravel/framework/pull/59238 * [13.x] Ensure RequiredUnless handles null by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59235 - -## [v12.54.1](https://github.com/laravel/framework/compare/v12.54.0...v12.54.1) - 2026-03-10 - -* [12.x] Makes imports consistent by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/59149 - -## [v12.54.0](https://github.com/laravel/framework/compare/v12.53.0...v12.54.0) - 2026-03-10 - -* [12.x] Fix division by zero error in `repeatEvery()` method by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58987 -* [12.x] Allow app.editor.base_path to be an empty string by [@kminek](https://github.com/kminek) in https://github.com/laravel/framework/pull/58991 -* [12.x] Add missing, remove unused parameters to docblocks by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/58989 -* Fix URL validation for punycode subdomains by [@mpa12](https://github.com/mpa12) in https://github.com/laravel/framework/pull/58982 -* [12.x] Prevent queue deadlock when reserving a job throws an exception (e.g., attempts overflow) by [@sadique-cws](https://github.com/sadique-cws) in https://github.com/laravel/framework/pull/58978 -* [12.x] bug: throttle with redis ignores after callback by [@RobertBoes](https://github.com/RobertBoes) in https://github.com/laravel/framework/pull/58990 -* Update brick/math version constraint to include 0.15 by [@julien-boudry](https://github.com/julien-boudry) in https://github.com/laravel/framework/pull/59005 -* Revert "Update brick/math version constraint to include 0.15" by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/59009 -* Bump rollup from 4.46.3 to 4.59.0 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/59013 -* [12.x] Fix TwoColumnDetail stripping trailing punctuation from second column values by [@theritvars](https://github.com/theritvars) in https://github.com/laravel/framework/pull/59010 -* [12.x] Add support for assertions on `BinaryFileResponse` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/59018 -* [12.x] fix: array offset deprecation warning by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/59019 -* [12.x] Add tsvector column type for PostgreSQL by [@milroyfraser](https://github.com/milroyfraser) in https://github.com/laravel/framework/pull/59004 -* Memory Limit passed as string when run from supervisor by [@turbo124](https://github.com/turbo124) in https://github.com/laravel/framework/pull/59049 -* [12.x] Fix facade cache file permissions by [@nkoestinger](https://github.com/nkoestinger) in https://github.com/laravel/framework/pull/59059 -* [12.x] Display oldest pending job in queue:monitor output by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59073 -* Fix type() method return type in Illuminate\Filesystem\Filesystem by [@GNfsys](https://github.com/GNfsys) in https://github.com/laravel/framework/pull/59071 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/59068 -* [12.x] Wrap flags in `int-mask-of` annotation by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/59082 -* Fix after-commit observers breaking -ing event cancellation by [@eyupcanakman](https://github.com/eyupcanakman) in https://github.com/laravel/framework/pull/59058 -* [12.x] Fix migrate:fresh failing when database does not exist by [@MElkmeshi](https://github.com/MElkmeshi) in https://github.com/laravel/framework/pull/59113 -* [12.x] Add `interval()` method to `InteractsWithData` by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/59114 -* [12.x] Hash displayName() in cache lock keys by [@A5hleyRich](https://github.com/A5hleyRich) in https://github.com/laravel/framework/pull/59141 -* [12.x] Improved html test helpers by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59140 -* [12.x] Add Model::withoutRelation() for selective relation unloading by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/59137 -* [12.x] Include request context in HTTP client Response::dump() by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/59136 -* Fix enum handling in ModelNotFoundException error message by [@isaackaara](https://github.com/isaackaara) in https://github.com/laravel/framework/pull/59132 -* [12.x] Update composer.json to enforce commonmark version without DisallowedRawHtmlRenderer exploit by [@Smoggert](https://github.com/Smoggert) in https://github.com/laravel/framework/pull/59131 -* [12.x] Suppress chmod errors in Filesystem::replace() for non-POSIX filesystems by [@eyupcanakman](https://github.com/eyupcanakman) in https://github.com/laravel/framework/pull/59126 -* Add composite index to jobs table migration for improved queue polling by [@firecow](https://github.com/firecow) in https://github.com/laravel/framework/pull/59111 -* [12.x] Load custom markdown extensions for mail by [@dasundev](https://github.com/dasundev) in https://github.com/laravel/framework/pull/59051 -* [12.x] Fix docblock for RateLimiter `for()` method by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/59144 -* [12.x] Deduplicate paths in view:cache by [@ganyicz](https://github.com/ganyicz) in https://github.com/laravel/framework/pull/59145 - -## [v12.53.0](https://github.com/laravel/framework/compare/v12.52.0...v12.53.0) - 2026-02-24 - -* [12.x] Add multipleOf support to JsonSchema numeric types by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/58903 -* [12.x] chore: don't format notifiables in NotificationSender::send by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58900 -* [12.x] Add vector option to whereFullText for pre-computed tsvector columns by [@milroyfraser](https://github.com/milroyfraser) in https://github.com/laravel/framework/pull/58893 -* [12.x] Add array key support for `buildMorphMapFromModels()` function by [@josephkerkhof](https://github.com/josephkerkhof) in https://github.com/laravel/framework/pull/58891 -* [12.x] Fix RequestException summarizing for Guzzle streamed responses by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58909 -* [12.x] Tests for streamed RequestException by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58910 -* Support a serializable classes value on caches by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/58911 -* [12.x] Simplify TokenGuard methods by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/58923 -* [12.x] Add uniqueItems support to JsonSchema ArrayType by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/58922 -* [12.x] Add tests for `PhpRedisClusterConnection` flushdb method by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58917 -* [12.x] Add support for named arguments in event dispatching and broadcasting by [@ph7jack](https://github.com/ph7jack) in https://github.com/laravel/framework/pull/58913 -* [12.x] Allow `down` command to refresh maintenance mode options by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/58918 -* [12.x] Rollback lingering PDO transaction before retrying on commit deadlock by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58906 -* [12.x] Simplify queue resolution using `match` expression by [@josephkerkhof](https://github.com/josephkerkhof) in https://github.com/laravel/framework/pull/58928 -* Bump tar from 7.5.7 to 7.5.9 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/58931 -* [12.x] Fix model serialization in queue jobs by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/58939 -* [12.x] Change Mail text alignment from left to start by [@zvizvi](https://github.com/zvizvi) in https://github.com/laravel/framework/pull/58935 -* [12.x] Refactor `convertValuesToBoolean` to use `match` for cleaner logic by [@josephkerkhof](https://github.com/josephkerkhof) in https://github.com/laravel/framework/pull/58927 -* [12.x] Allow Scheduled Command `Event` macros to be applied to schedule groups by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/58926 -* [12.x] Fix race condition on creating the real-time facade cache file (#58945) by [@sosias](https://github.com/sosias) in https://github.com/laravel/framework/pull/58947 -* [12.x] Show all mismatched values in `assertSessionHasAll` failure output by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58946 -* Fix RetryCommand not working for SQS FIFO queue by [@cwang22](https://github.com/cwang22) in https://github.com/laravel/framework/pull/58936 -* [12.x] Improve return types for Wormhole and InteractsWithTime by [@KentarouTakeda](https://github.com/KentarouTakeda) in https://github.com/laravel/framework/pull/58951 -* [12.x] Add `Cache::funnel()` for concurrency limiting with any cache driver by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/58439 -* [12.x] Ensure `oldest_pending` is displayed in `queue:monitor` by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58952 -* [12.x] Fix/cross database null safe equals by [@patrickomeara](https://github.com/patrickomeara) in https://github.com/laravel/framework/pull/58962 -* [12.x] Add MySQL inRandomOrder regression tests by [@laraib15](https://github.com/laraib15) in https://github.com/laravel/framework/pull/58966 -* [12.x] Add missing [@throws](https://github.com/throws) docblocks to Illuminate/Http by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/58965 -* Fix Invalid Types by [@RyanSchaefer](https://github.com/RyanSchaefer) in https://github.com/laravel/framework/pull/58963 -* JSONP Check by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/58971 -* [12.x] Resolve Stan Mailable CI error by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58972 -* [12.x] Remove unnecessary dirname calls. by [@jelleroorda](https://github.com/jelleroorda) in https://github.com/laravel/framework/pull/58984 -* [12.x] Add missing [@throws](https://github.com/throws) docblocks to `Serializer` and `Type` classes in `Illuminate/JsonSchema` by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/58981 - -## [v12.52.0](https://github.com/laravel/framework/compare/v12.51.0...v12.52.0) - 2026-02-17 - -* [12.x] Fix: `@return` in doc blocks by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58746 -* [12.x] Ensure defer callbacks aren't discarded when using the sync queue by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58745 -* [12.x] Refactor: remove `Arr::wrap()` and add `Collection::wrap()` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58748 -* Add support for `temporaryUploadUrl` to the `local` filesystem by [@mnapoli](https://github.com/mnapoli) in https://github.com/laravel/framework/pull/58499 -* Only merge cached casts for accessed attribute by [@ug-christoph](https://github.com/ug-christoph) in https://github.com/laravel/framework/pull/57627 -* [12.x] Sort stan issue on PendingRequest by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58760 -* [12.x] Update alphabetical order in facades.yml by [@luisscruza](https://github.com/luisscruza) in https://github.com/laravel/framework/pull/58757 -* [12.x] allow string-based expressions for selectExpression() by [@tpetry](https://github.com/tpetry) in https://github.com/laravel/framework/pull/58753 -* Revert "[12.x] Adjust freshTimestamp for SQL Server" by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58758 -* [12.x] Fix return empty Collection for non-model JSON:API resources by [@noir4y](https://github.com/noir4y) in https://github.com/laravel/framework/pull/58752 -* [12.x] Refactor: remove extra space by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58751 -* [12.x] Standardize regex delimiter in ObserverMakeCommand::parseModel by [@mohammadRezaei1380](https://github.com/mohammadRezaei1380) in https://github.com/laravel/framework/pull/58777 -* [12.x] Fix incorrect [@return](https://github.com/return) type in VendorPublishCommand::publishTag by [@mohammadRezaei1380](https://github.com/mohammadRezaei1380) in https://github.com/laravel/framework/pull/58774 -* Fix phpdoc type in promptForMissingArgumentsUsing by [@billypoke](https://github.com/billypoke) in https://github.com/laravel/framework/pull/58768 -* [12.x] cast `Batch::progress()` return value to `int` by [@zjbarg](https://github.com/zjbarg) in https://github.com/laravel/framework/pull/58767 -* [12.x] Drop Collection import from `AbstractRouteCollection` by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58769 -* [12.x] Fix missing InputArgument::IS_ARRAY in getArguments PHPDoc by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/58771 -* [12.x] Fix: `@return` for `resolveResourceRelationshipIdentifiers()` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58764 -* [12.x] `Mailable::later()` does not set delay on `SendQueuedMailable` instance by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58765 -* [12.x] Refactor: use `enum_value()` helper for environment value extraction by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58785 -* [12.x] Add delay support assertions for queued mailables by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58787 -* Fix MySQL connection string to use --ssl-mode=DISABLED for modern clients by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/58786 -* [12.x] Refactor: standardize regex by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58789 -* [12.x] Allow $preserveKeys param for LazyCollection random by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58791 -* [12.x] Refactor: `new Collection()` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58793 -* [12.x] Add `makeMany` method to Factory by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58795 -* [12.x] Add `withoutAfterMaking()` and `withoutAfterCreating()` factory helpers by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/58794 -* [12.x] Backport withMiddleware changes from 13.x by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58798 -* [12.x] Fix: add `|array` in doc block by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58805 -* [12.x] Add option to opt out of parallel safe cache prefix by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58801 -* [12.x] Normalize Throwable docblocks to fully-qualified name by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58802 -* [12.x] Refactor: remove unnecessary `\BackedEnum` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58807 -* Use atomic writes when creating inline Blade component views by [@cyppe](https://github.com/cyppe) in https://github.com/laravel/framework/pull/58815 -* [12.x] Add missing tests for Request::fullUrlWithoutQuery by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58814 -* Improve File::toKilobytes() DocBlock return type by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/58811 -* Use atomic writes in BladeCompiler to prevent race condition by [@cyppe](https://github.com/cyppe) in https://github.com/laravel/framework/pull/58812 -* [12.x] Refactor: add `JSON decoded` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58830 -* [12.x] Refactor: add missing `@throws` tag in dock block by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58829 -* [12.x] Formatting by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58828 -* [12x]Refactor: remove unnecessary \BackedEnum in HasAttributes.php by [@mohammadRezaei1380](https://github.com/mohammadRezaei1380) in https://github.com/laravel/framework/pull/58827 -* [12x] Refactor conditional message formatting using match expression by [@mohammadRezaei1380](https://github.com/mohammadRezaei1380) in https://github.com/laravel/framework/pull/58825 -* [12.x] Refactor: use `match` expression by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58824 -* [12.x] Simplify `compileSelect` method return by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58821 -* [12.x] Refactor: simplify code by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58820 -* [12.x] Refactor: remove unnecessary `\BackedEnum` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58818 -* [12.x] Ensure HttpClientTest doesnt flake in Windows CI by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58817 -* [12.x] Refactor: `JSON decoded` to `decoded JSON` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58849 -* [12.x] Allow closure parameters in docblock for when() helper function by [@gazben](https://github.com/gazben) in https://github.com/laravel/framework/pull/58862 -* [12.x] Fix typo in cache `composer.json` by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58875 -* [12.x] Remove unnecessary `forgetDriver()`from TestCaches by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58878 -* Revert "[12.x] Fixed precision checks for column types in SQL Server grammar" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/58888 -* [12.x] Display closures and standalone functions correctly in exception trace by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/58879 - -## [v12.51.0](https://github.com/laravel/framework/compare/v12.50.0...v12.51.0) - 2026-02-10 - -* Remove type hint in favor of return type by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/58621 -* [12.x] Adjust freshTimestamp for SQL Server by [@aimeos](https://github.com/aimeos) in https://github.com/laravel/framework/pull/58614 -* [12.x] Handle binary data in Js::encode() debug renderer by [@denis-chmel](https://github.com/denis-chmel) in https://github.com/laravel/framework/pull/58618 -* [12.x] Add ArrayObject props to AsEncryptedArrayObject to match AsArrayObject by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/58619 -* fix: Arr::wrap() return type by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58625 -* [12.x] Fix typo in type definition by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58624 -* [12.x] Prevent dupe locale checks in `Lang::get()` when locale matches fallback by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58626 -* [12.x] chore: add deprecation to Request::get() by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58635 -* [12.x] Fix Str::substrReplace for edge cases with negative offset or length by [@jboonstra70](https://github.com/jboonstra70) in https://github.com/laravel/framework/pull/58634 -* [12.x] Refactor: improve doc blocks by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58630 -* [12.x] Add BatchCancelled Event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58627 -* [12.x] Fix typo in type definition by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58638 -* [12.x] Update `reload` tasks to include `schedule:interruption` by [@adevade](https://github.com/adevade) in https://github.com/laravel/framework/pull/58637 -* docs: add missing description to FilesystemAdapter::report() docblock by [@eranishojha](https://github.com/eranishojha) in https://github.com/laravel/framework/pull/58640 -* [12.x] Allow closures for values in `firstOrCreate` and `createOrFirst` by [@gcavanunez](https://github.com/gcavanunez) in https://github.com/laravel/framework/pull/58639 -* [12.x] Support `afterSending` method on notification by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/58654 -* [12.x] Allow Stringable::deduplicate() to accept array of characters by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/58649 -* Update regex for trans_choice to allow negative ranges by [@hmazter](https://github.com/hmazter) in https://github.com/laravel/framework/pull/58648 -* Added timeout method to query builder for MySQL by [@Vladelis](https://github.com/Vladelis) in https://github.com/laravel/framework/pull/58644 -* [12.x] Add `assertJobs` method on `PendingBatchFake` by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/58606 -* [12.x] Fix batch counts when deleteWhenMissingModels skips missing model jobs by [@yankewei](https://github.com/yankewei) in https://github.com/laravel/framework/pull/58541 -* [12.x] Fix Postgres sequence starting value for custom schemas/connections by [@joteejotee](https://github.com/joteejotee) in https://github.com/laravel/framework/pull/58199 -* [12.x] Add `whenFails` and `whenPasses` methods on `Validator` by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/58655 -* [12.x] Bus::assertBatched() with array by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/58659 -* [12.x] Refactor: improve doc block by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58677 -* [12.x] Add withoutHeader() method to Response by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58671 -* [12.x] Add integer array key support in phpdocs by [@dluague](https://github.com/dluague) in https://github.com/laravel/framework/pull/58668 -* `Illuminate\Console\Parser` typehint fix. by [@LastDragon-ru](https://github.com/LastDragon-ru) in https://github.com/laravel/framework/pull/58670 -* fix: replace substr with mb_substr for user agent encoding by [@jonagoldman](https://github.com/jonagoldman) in https://github.com/laravel/framework/pull/58703 -* chore: fix the Laravel ASCII SVG so that its characters perfectly align to columns by [@markjaquith](https://github.com/markjaquith) in https://github.com/laravel/framework/pull/58702 -* [12.x] Allow retrieving all view data via viewData() in TestResponse by [@shane-zeng](https://github.com/shane-zeng) in https://github.com/laravel/framework/pull/58700 -* Exception page: fix pop in for non main frames by [@martinpl](https://github.com/martinpl) in https://github.com/laravel/framework/pull/58698 -* [12.x] Add missing [@throws](https://github.com/throws) annotations to validation rules and JsonResponse by [@QDenka](https://github.com/QDenka) in https://github.com/laravel/framework/pull/58697 -* [12.x] Add conditional return type hint for Route::middleware() method. by [@marcreichel](https://github.com/marcreichel) in https://github.com/laravel/framework/pull/58699 -* [12.x] Improved type hints for when() helper function. by [@marcreichel](https://github.com/marcreichel) in https://github.com/laravel/framework/pull/58696 -* [12.x] Support Eloquent builders and relations as subqueries to update queries by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/58692 -* Add cache prefix isolation for parallel testing (#57584) by [@HeathNaylor](https://github.com/HeathNaylor) in https://github.com/laravel/framework/pull/58691 -* Fix whereBetween to accept DatePeriod and handle missing end dates (#58092) by [@HeathNaylor](https://github.com/HeathNaylor) in https://github.com/laravel/framework/pull/58687 -* Fix Str::isUrl() returning false for single-char domain names (#58538) by [@HeathNaylor](https://github.com/HeathNaylor) in https://github.com/laravel/framework/pull/58686 -* Fix HTTP client response type hints for IDE compatibility (#58555) by [@HeathNaylor](https://github.com/HeathNaylor) in https://github.com/laravel/framework/pull/58684 -* Fix types for ConfirmableTrait::confirmToProceed by [@rolfvandekrol](https://github.com/rolfvandekrol) in https://github.com/laravel/framework/pull/58681 -* [12.x] Refactor: simplify return with `??` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58679 -* [12.x] Refactor: replace `header` / `headers` with standardized `header(s)` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58678 -* Add SSL cert/key support to MySQL schema dump and load (#57821) by [@HeathNaylor](https://github.com/HeathNaylor) in https://github.com/laravel/framework/pull/58690 -* Allow specifying Redis connection on Redis-based queue middleware by [@markieo1](https://github.com/markieo1) in https://github.com/laravel/framework/pull/58656 -* [12.x] Use JS to create the Laravel ASCII SVG logo on the fly by [@markjaquith](https://github.com/markjaquith) in https://github.com/laravel/framework/pull/58719 -* [12.x] Feat: add `orderByPivotDesc()` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58720 -* [12.x] Refactor: add `@throws \InvalidArgumentException` to doc blocks by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58714 -* [12.x] Restore original dispatcher bindings after precognitive request by [@pindab0ter](https://github.com/pindab0ter) in https://github.com/laravel/framework/pull/58716 -* [12.x] Ensure throwIfStatus / throwUnlessStatus work for all status codes by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58724 -* Fix Queue::fake() not releasing unique job locks between tests (#58533) by [@HeathNaylor](https://github.com/HeathNaylor) in https://github.com/laravel/framework/pull/58718 -* [12.x] Refactor: add `_` to more readability digit by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58738 -* [12.x] Refactor: Clean up unused $config parameters in ConcurrencyManager by [@alizadeh7091](https://github.com/alizadeh7091) in https://github.com/laravel/framework/pull/58739 -* [12.x] Refactor: use `Dumpable` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58743 -* [12.x] Update forever cookie factory docblock to reflect 400-day duration by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58744 - -## [v12.50.0](https://github.com/laravel/framework/compare/v12.49.0...v12.50.0) - 2026-02-04 - -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58531 -* [12.x] Resolve the correct queue factory when using laravel octane by [@BertvanHoekelen](https://github.com/BertvanHoekelen) in https://github.com/laravel/framework/pull/58530 -* [12.x] Clear parallel test view cache directories by [@eduPHP](https://github.com/eduPHP) in https://github.com/laravel/framework/pull/58525 -* [12.x] fix: allow phpstan to understand default value for Request::enum by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58529 -* [12.x] feat: allow queued listeners to be unique by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58402 -* [12.x] Use morphMap when serializing model identifiers by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58482 -* [12.x] Add `authority`method to Support/Uri by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58534 -* [12.x] Use try/finally for buildStack cleanup in Container::build by [@comhon-project](https://github.com/comhon-project) in https://github.com/laravel/framework/pull/58536 -* [12.x] Update phpunit version constraints to address CVE by [@PerryvanderMeer](https://github.com/PerryvanderMeer) in https://github.com/laravel/framework/pull/58526 -* Bump tar from 7.5.6 to 7.5.7 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/58539 -* [12.x] Ensure Validator message defaults if using custom size messages by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58554 -* [12.x] Add withoutAppends to HasAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/58552 -* [12.x] Refactor: simplify in `match` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58547 -* Revert "[12.x] Update phpunit version constraints to address CVE" by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58542 -* [12.x] Add `hasMany()` method to collections by [@JosephSilber](https://github.com/JosephSilber) in https://github.com/laravel/framework/pull/58550 -* [12.x] Retain associative keys on eager loaded relations by [@Jade-GG](https://github.com/Jade-GG) in https://github.com/laravel/framework/pull/58506 -* [12.x] Add typed getters on Cache by [@ahinkle](https://github.com/ahinkle) in https://github.com/laravel/framework/pull/58451 -* [12.x] Add `MaintenanceMode` facade to docblock generator by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/58564 -* [12.x] Adjust docblock for formatActionForCli by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58562 -* [12.x] brick/math `of` float deprecation by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58563 -* [12.x] Improve migration types by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58561 -* [12.x] Remove extra space by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58573 -* [12.x] Drop foreach from preg_replace_callback helper by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58570 -* [12.x] Improve typing in console/command namespace by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58565 -* [12.x] Refactor: improve tests by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58582 -* [12.x] Update callback type hints for Context's `Repository` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58581 -* [12.x] Exclude decorative ASCII art SVG from exception page in non-browser contexts by [@serhiilabs](https://github.com/serhiilabs) in https://github.com/laravel/framework/pull/58580 -* [12.x] Improve types of `Arr` helper by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58518 -* [12.x] Add tests for withoutAppends() method by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/58583 -* [12.x] Add tests for hasAppended() method by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/58587 -* [12.x] Sort flaky MaintenanceModeTest by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58590 -* [12.x] Preserve notification state mutations from via() in sendNow() by [@alimorgaan](https://github.com/alimorgaan) in https://github.com/laravel/framework/pull/58558 -* [12.x] Fix: add `|null` for `$name` in `storeAs()` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58613 -* [12.x] Add `InteractsWithData::clamp()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58608 -* [12.x] try-catch all composer package uninstalls by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58609 -* [12.x] `InteractsWithData@enum()` refactor by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58607 -* [12.x] Enum support for Cache::get() with array keys by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58616 -* [12.x] Fixed precision checks for column types in SQL Server grammar by [@aimeos](https://github.com/aimeos) in https://github.com/laravel/framework/pull/58602 -* [12.x] Fix `illuminate/reflection` workflow directory by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58600 -* [12.x] Ensure File fail doesn't double translate in fail() by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58598 -* [12.x] Ensure mailable HTML assertions properly escape quotes by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58595 - -## [v12.49.0](https://github.com/laravel/framework/compare/v12.48.1...v12.49.0) - 2026-01-28 - -* [12.x] Clean up compiled views after parallel testing by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58440 -* [12.x] Support "where subquery between columns" by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/58441 -* [12.x] Use searchable prompt for db:table command by [@sakshamgorey](https://github.com/sakshamgorey) in https://github.com/laravel/framework/pull/58442 -* [12.x] keep single NotificationSender instance by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/58452 -* [12.x] Allow enum keys in Cache::flexible() and withoutOverlapping() by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58444 -* [12.x] Add preserveKeys method to AnonymousResourceCollection by [@comhon-project](https://github.com/comhon-project) in https://github.com/laravel/framework/pull/58443 -* Bump tar from 7.5.3 to 7.5.6 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/58454 -* [12.x] Fix memory leak in `Arr::dot()` by [@benjamin-commentor](https://github.com/benjamin-commentor) in https://github.com/laravel/framework/pull/58458 -* 12.x fix: use multibyte-safe functions in Str::afterLast() by [@irabbi360](https://github.com/irabbi360) in https://github.com/laravel/framework/pull/58457 -* [12.x] Ensure Session now() and flash() accept enums by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58459 -* [12.x] Revert "feat: aliasing when selecting database expressions (#58436)" by [@u01jmg3](https://github.com/u01jmg3) in https://github.com/laravel/framework/pull/58469 -* [12.x] Add `hasSole()` method to collections by [@JosephSilber](https://github.com/JosephSilber) in https://github.com/laravel/framework/pull/58463 -* [12.x] Skip message serialization when log level is not handled by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58475 -* [12.x] Add missing [@param](https://github.com/param) documentation to SessionGuard constructor by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58493 -* [12.x] do assignment instead of mutating to handle immutable carbon object. by [@MrPunyapal](https://github.com/MrPunyapal) in https://github.com/laravel/framework/pull/58498 -* Enhance index hint validation for multiple indexes by [@FlexIDK](https://github.com/FlexIDK) in https://github.com/laravel/framework/pull/58505 -* [12.x] Make QueueFake assertPushedTimes public by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58511 -* [12.x] Ignore deadlock on DatabaseLock release by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58507 -* [12.x] Allow `down` command --retry option to accept datetime values by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/58509 - -## [v12.48.1](https://github.com/laravel/framework/compare/v12.48.0...v12.48.1) - 2026-01-20 - -## [v12.48.0](https://github.com/laravel/framework/compare/v12.47.0...v12.48.0) - 2026-01-20 - -* [12.x] Fix missing variable reassignment by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/58376 -* [12.x] Improve PendingRequest types by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58386 -* [12.x] Fix backward compatibility with third-party guards by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/58385 -* Make \Illuminate\Testing\TestResponse::assertHeader() case insensitive by [@HenkPoley](https://github.com/HenkPoley) in https://github.com/laravel/framework/pull/58383 -* [12.x] Fix TypeError when validation rule has empty parameters by [@irabbi360](https://github.com/irabbi360) in https://github.com/laravel/framework/pull/58380 -* [12.x] Adjust PendingBatchFake to filter by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58375 -* [12.x] Resolve infinite loop when using deferred queue by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58373 -* [12.x] Fix and improve `\Illuminate\Support\Str` types further by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58372 -* [12.x] Ensure Bus::chain filters out falsy items by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58369 -* [12.x] fix invalid array doctypes for Str::replaceMatches in v12.47.0 by [@marcreichel](https://github.com/marcreichel) in https://github.com/laravel/framework/pull/58364 -* [12.x] Remove useless use of `MockeryPHPUnitIntegration` by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/58363 -* [12.x] Fix: Drop indexes from failed_jobs by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58362 -* Translation lines may contain square brackets and curly braces now by [@edwinheij](https://github.com/edwinheij) in https://github.com/laravel/framework/pull/58367 -* [12.x] Add `skipWhen` functionality to `HandleCors` middleware by [@RobertBoes](https://github.com/RobertBoes) in https://github.com/laravel/framework/pull/58361 -* [12.x] Fix backward compatibility with third-party guards, again by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/58389 -* [12.x] Add type tests for `\Illuminate\Support\Str` by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58365 -* [12.x] `new $class` instead of reflection for better performance by [@takaram](https://github.com/takaram) in https://github.com/laravel/framework/pull/58391 -* [12.x] Isolate compiled views per process during parallel testing by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58390 -* [12.x] Fix broken import by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/58394 -* [12.x] Implement Stringable in Enum rule by [@owenconti](https://github.com/owenconti) in https://github.com/laravel/framework/pull/58392 -* [12.x] Fix restoreLock for MemoizedStore by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58423 -* [12.x] Fix Filesystem::sharedGet partial reads (#58418) by [@sv63rus](https://github.com/sv63rus) in https://github.com/laravel/framework/pull/58419 -* [12.x] Add missing [@param](https://github.com/param) docblock to ValidatedInput::__isset() by [@ismaildasci](https://github.com/ismaildasci) in https://github.com/laravel/framework/pull/58410 -* [12.x] Add queue to JobPopping by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58413 -* [12.x] add backoff to JobReleasedAfterException event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58414 -* [12.x] Fix return type docblock for Number::abbreviate method by [@ismaildasci](https://github.com/ismaildasci) in https://github.com/laravel/framework/pull/58408 -* [12.x] Annotate tuple return type of Number::pairs() by [@ismaildasci](https://github.com/ismaildasci) in https://github.com/laravel/framework/pull/58409 -* Bump tar from 7.4.3 to 7.5.3 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/58404 -* [12.x] Update return type of merge for collections by [@ramonrietdijk](https://github.com/ramonrietdijk) in https://github.com/laravel/framework/pull/58405 -* [12.x] Fix missing import by [@irabbi360](https://github.com/irabbi360) in https://github.com/laravel/framework/pull/58401 -* [12.x] Account for `Throwable` inside of PendingRequest by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58395 -* [12.x] Allow setting flags for decoding json in the Http Client's Response by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58379 -* [12.x] chore: make PruneCommand::isPrunable() protected by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58430 -* [12.x] widen PendingRequest@pool() return type by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58437 -* [12.x] feat: query builder aliases for expressions by [@tpetry](https://github.com/tpetry) in https://github.com/laravel/framework/pull/58436 -* Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58432 -* [12.x] add BatchFinished event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58431 - -## [v12.47.0](https://github.com/laravel/framework/compare/v12.46.0...v12.47.0) - 2026-01-13 - -* [12.x] Add `@includeIsolated` directive for isolated Blade includes by [@KennedyTedesco](https://github.com/KennedyTedesco) in https://github.com/laravel/framework/pull/58311 -* [12.x] Fix typo in JsonApiResource trait method by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58326 -* [12.x] Add `Cache::withoutOverlapping()` to wrap `Cache::lock()->block()` by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/58303 -* Update return type annotations in FormRequest.php by [@arttiger](https://github.com/arttiger) in https://github.com/laravel/framework/pull/58333 -* [12.x] Fix QueryException showing wrong connection details for read PDO by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/58331 -* [12.x] Only fire `CacheFailedOver` and `QueueFailedOver` on first failure by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58330 -* allow precognitive requests to use wildcards with array validations (#57437) by [@markusheinemann](https://github.com/markusheinemann) in https://github.com/laravel/framework/pull/57486 -* [12.x] Fix docblock for Failovers by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58336 -* [12.x] Only fire composer uninstall events when removing dev packages by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58338 -* Vector things by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/58337 -* Update tailwind version by [@laserhybiz](https://github.com/laserhybiz) in https://github.com/laravel/framework/pull/58344 -* [12.x] Allow for enum keys in additional Session Store methods by [@riesjart](https://github.com/riesjart) in https://github.com/laravel/framework/pull/58343 -* [12.x] JSON API: Deduplicate circular references by [@mateusjatenee](https://github.com/mateusjatenee) in https://github.com/laravel/framework/pull/58348 -* [12.x] Improve `key:generate` error message when `APP_KEY` is set by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58345 -* [12.x] Add indexes to failed_jobs stub by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58355 -* [12.x] Improve types in `\Illuminate\Support\Str` helper by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58356 -* [12.x] Fix MySQL port conflict in tests workflow by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58357 -* [12.x] Improve the return types for `Number::with*()` helpers by [@CasEbb](https://github.com/CasEbb) in https://github.com/laravel/framework/pull/58358 -* [12.x] Ensure `Bus::batch` filters out falsy items by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58360 -* [12.] Annotate tuple return type of `TableGuesser::guess()` by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58354 -* [12.x] Make Notification macroable by [@ekateiva](https://github.com/ekateiva) in https://github.com/laravel/framework/pull/58352 -* [12.x] Allow PendingBatch `onConnection` to use Enum by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58350 - -## [v12.46.0](https://github.com/laravel/framework/compare/v12.45.2...v12.46.0) - 2026-01-07 - -* [12.x] Add `Arr::onlyValues` and `Arr::exceptValues` by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/58317 -* [12.x] Fixed phpdoc of `Container::buildSelfBuildingInstance`, to prevent psalm from erroring when parsing the class by [@nicDamours](https://github.com/nicDamours) in https://github.com/laravel/framework/pull/58314 -* [12.x] Add `Collection::containsManyItems()` method by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/58312 -* [12.x] Table prefix not applied when cloning connections by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58288 -* [12.x] Added MySQL DDL locking options to MySQL grammar by [@Vladelis](https://github.com/Vladelis) in https://github.com/laravel/framework/pull/58293 - -## [v12.45.2](https://github.com/laravel/framework/compare/v12.45.1...v12.45.2) - 2026-01-07 - -* [12.x] Feature: allow UnitEnum in has() method of Gate class by [@webard](https://github.com/webard) in https://github.com/laravel/framework/pull/58310 -* [12.x] Fix `Validator::appendRules()` with pipe-separated rule strings by [@leo108](https://github.com/leo108) in https://github.com/laravel/framework/pull/58304 -* [12.x] Fix calling `toArray()` on `AnonymousResourceCollection` returns an array of resources by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58302 - -## [v12.45.1](https://github.com/laravel/framework/compare/v12.45.0...v12.45.1) - 2026-01-07 - -* [12.x] Fix `ResourceCollection` usage when used with an array instead of Model collection by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58299 - -## [v12.45.0](https://github.com/laravel/framework/compare/v12.44.0...v12.45.0) - 2026-01-06 - -* [12.x] JSON:API Resource by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57571 -* [12.x] Add static constructor to guest middleware by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/58204 -* [12.x] Include JsonResource in `ModelInspector` result by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58205 -* [12.x] Add queue paused / resume events by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58202 -* [12.x] Add attachment helper method to add attachment from cloud storage by [@PhiloNL](https://github.com/PhiloNL) in https://github.com/laravel/framework/pull/58201 -* [12.x] Normalize APP_URL when generating filesystem URLs by [@congkv](https://github.com/congkv) in https://github.com/laravel/framework/pull/58210 -* [12.x] Adjust AuthDatabaseTokenRepositoryTest by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58206 -* [12.x] Refactor `queuePaused` logic by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58215 -* Fix queue:listen timeout false positives after system sleep/wake by [@ranjith67](https://github.com/ranjith67) in https://github.com/laravel/framework/pull/58216 -* Change the remember cookie to store a MAC of the users password hash instead of their real password hash by [@Synchro](https://github.com/Synchro) in https://github.com/laravel/framework/pull/58107 -* Add connection details to QueryException error messages by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/58218 -* [12.x] Adjust README test status badge by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58222 -* [12.x] Use constant for session ID length by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58224 -* [12.x] Add type tests for PendingRequest.php by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/58232 -* feat: fire JobAttempted for sync jobs too by [@veeshpath](https://github.com/veeshpath) in https://github.com/laravel/framework/pull/58228 -* [12.x] Adjust getEventDispatcher docblock to allow null return by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58242 -* [12.x] Extract `JobAttempted` event dispatch to a separate method in `SyncQueue` by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58240 -* [12.x] ValidationException: update redirectTo property definition to include null by [@cheack](https://github.com/cheack) in https://github.com/laravel/framework/pull/58238 -* [12.x] Add BackedEnum support for session keys by [@ahinkle](https://github.com/ahinkle) in https://github.com/laravel/framework/pull/58241 -* [12.x] Update `upload-artifact` action by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58250 -* [12.x] Allow BackedEnum for Cache keys by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58246 -* [12.x] Add CommandFailed event and listenForFailures() for Redis connections by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58251 -* [12.x] Fix: Change BackedEnum to UnitEnum in Authorizable trait by [@webard](https://github.com/webard) in https://github.com/laravel/framework/pull/58258 -* [12.x]Refactor: remove if and replace ? by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58256 -* [12.x]Feat(MustVerifyEmail): add markEmailAsNotVerified() by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58255 -* [12.x] Feat: add havingNotBetween && orHavingBetween && orHavingNotBetween by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58259 -* [12.x] Formatting by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58266 -* [12.x] Refactor: add |null in dock block by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58268 -* Add type guard for ChainedBatch/Queueable chained property before array_shift by [@cyppe](https://github.com/cyppe) in https://github.com/laravel/framework/pull/58264 -* [12.x] Add lang attributes to mail layout by [@DBawazir2002](https://github.com/DBawazir2002) in https://github.com/laravel/framework/pull/58274 -* [12.x] Clean up `Builder` docblocks by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/58270 -* [12.x] Run Mockery cleanup via PHPUnit subscriber instead of explicit `m::close()` calls by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/58278 -* [12.x] Fix delayed Redis queue jobs with phpredis serialization enabled by [@iazaran](https://github.com/iazaran) in https://github.com/laravel/framework/pull/58235 -* [12.x] Add missing return type to Arr::array() by [@mischasigtermans](https://github.com/mischasigtermans) in https://github.com/laravel/framework/pull/58280 -* [12.x] Add --readable flag to env:encrypt for visible key names by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/58262 -* [12.x] Update fake method parameter type for disk by [@murilo-plantae](https://github.com/murilo-plantae) in https://github.com/laravel/framework/pull/58285 -* [12.x] Fix typo in BelongsToMany::createOrFirst method name by [@mischasigtermans](https://github.com/mischasigtermans) in https://github.com/laravel/framework/pull/58284 -* [12.x] Fix nth(), split() and splitIn() to throw InvalidArgumentException for invalid parameters by [@mischasigtermans](https://github.com/mischasigtermans) in https://github.com/laravel/framework/pull/58283 -* [12.x] Fix Str::chopStart() and Str::chopEnd() returning empty string when given empty needle by [@mischasigtermans](https://github.com/mischasigtermans) in https://github.com/laravel/framework/pull/58286 -* [12.x] Add AsBinary castable class by [@plumthedev](https://github.com/plumthedev) in https://github.com/laravel/framework/pull/58254 -* [12.x] Add enum to `persistentFake()`- and add tests by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58287 -* Update Inspiring Qoute's author name by [@kerog](https://github.com/kerog) in https://github.com/laravel/framework/pull/58292 -* [12.x] Fix: add `@throws \InvalidArgumentException` to some dock block by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/58289 -* [12.x] Fix `Validator::sometimes()` usage with attributes containing `.` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58291 -* [12.x] Support "where subquery between values" by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/58290 -* Capture PDO read / write type for query events by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/58156 - -## [v12.44.0](https://github.com/laravel/framework/compare/v12.43.1...v12.44.0) - 2025-12-23 - -* [12.x] Allow easier opting out of `DatabaseLock` prune lottery by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58152 -* [12.x] Specify that the query builder returns instances of `stdClass` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/58150 -* [12.x] feat: add now methods to Date rule by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/58059 -* [12.x] Add ability to run callbacks after building an Http response by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58088 -* Fix docblocks by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/58157 -* [12.x] Fix Password::required() missing value validation and nullable empty … by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/58158 -* [12.x] Fixup Eloquent `Collection` (param) docblocks by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/58170 -* [12.x] add MigrationSkipped event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58167 -* [12.x] Simplify `LazyCollection` `passthru` calls and docblocks by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/58180 -* [12.x] Add BusBatchable tests by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58175 -* Add previous locale to LocaleUpdated event by [@OutlawPlz](https://github.com/OutlawPlz) in https://github.com/laravel/framework/pull/58179 -* [12.x] Fix inline mail embed replacement by Content-ID by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/58173 -* [12.x] Fix multibyte string handling in chopStart and chopEnd by [@mdariftiens](https://github.com/mdariftiens) in https://github.com/laravel/framework/pull/58183 -* [12.x] Improve `Collection` docblock types by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/58176 -* [12.x] Fix unable to disable `created_at` or `updated_at` column when attaching models by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58164 -* Remove unused variables from destructured arrays by [@rolfvandekrol](https://github.com/rolfvandekrol) in https://github.com/laravel/framework/pull/58187 -* [12.x] use process to trigger package uninstall event by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58177 -* [12.x] Fix null array key deprecation in HasOneOrMany relation matching by [@serhiilabs](https://github.com/serhiilabs) in https://github.com/laravel/framework/pull/58191 -* [12.x] Fix `Password::required()` and `Password::sometimes()` usage as array by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58188 -* [12.x] Add TestResponse::assertHeaderContains assertion and tests by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58200 -* [12.x] Update setup-node action to v6 by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58196 - -## [v12.43.1](https://github.com/laravel/framework/compare/v12.43.0...v12.43.1) - 2025-12-16 - -* [12.x] Only exclude Command ending with `Test` isn't an instance of `Command` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58147 - -## [v12.43.0](https://github.com/laravel/framework/compare/v12.42.0...v12.43.0) - 2025-12-16 - -* [12.x] Add PHPDoc callable types for BusFake methods by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/58070 -* Improve type annotations of `$batchId` in `Batchable` trait by [@markieo1](https://github.com/markieo1) in https://github.com/laravel/framework/pull/58069 -* [12.x] Fix deadlock in cache_locks on cleanup by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58071 -* feat: implement 'assertFailedDependency' response assertion by [@artengin](https://github.com/artengin) in https://github.com/laravel/framework/pull/58061 -* [12.x] Fix using `null` cache store triggering PHP 8.5 deprecation by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/58074 -* [12.x] Fix deprecated usage of passing `null` to `array_key_exists` in `AsPivot` class by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/58073 -* [12.x] Simplify clearing resolved instances for `Facade` classes by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/58072 -* [12.x] Add PHP 8.5 with Herd to passthrough variables in ServeCommand by [@bashgeek](https://github.com/bashgeek) in https://github.com/laravel/framework/pull/58080 -* [12.x] Update actions/checkout v4 to v6 by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58078 -* [12.x] Let Storage::fake() accept enum as disk name by [@bbredewold](https://github.com/bbredewold) in https://github.com/laravel/framework/pull/58076 -* Improve PHPDoc return type for synchronous HTTP Client methods by [@khaled-sadek](https://github.com/khaled-sadek) in https://github.com/laravel/framework/pull/58090 -* [12.x] Adjust testCanRetrieveAllFailedJobs by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58096 -* [12.x] Allow Factory connection method to accept null by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58108 -* [12.x] Make PendingDispatch::afterResponse conditional by [@kenneth-saey](https://github.com/kenneth-saey) in https://github.com/laravel/framework/pull/58099 -* [12.x] Add `mergeHidden` and `mergeVisible` methods to Collection class by [@mahmoudmohamedramadan](https://github.com/mahmoudmohamedramadan) in https://github.com/laravel/framework/pull/58110 -* Added "SSL error: unexpected eof" message to LostConnectionDetector by [@GuidoHendriks](https://github.com/GuidoHendriks) in https://github.com/laravel/framework/pull/58113 -* [12.x] Update git-auto-commit action by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58118 -* [12.x] Add tests for Support Uri class by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58114 -* [12.x] Make the Client Response class tappable by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/58115 -* [12.x] Add missing docblock param in FailedOver event docblocks by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58123 -* [12.x] Clean up DynamoDbStore by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58116 -* [12.x] Fix: Handle `ParseError` in `hasEvenNumberOfParentheses` when Xdebug is active by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58128 -* [12.x] Fix Password::required() to fail when value is missing by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/58125 -* [12.x] Add HigherOrderProxy tests (Collection & Tap) by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58138 -* [12.x] Run `ConnectionEstablished` event on database reconnection by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58136 - -## [v12.42.0](https://github.com/laravel/framework/compare/v12.41.1...v12.42.0) - 2025-12-09 - -* [12.x] Improve `Context::scope()` return type by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58012 -* [12.x] Allow float values in duration helpers for CarbonInterval by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/58006 -* Add whenTableHasIndex and whenTableDoesntHaveIndex to Builder by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/58005 -* [12.x] Add commandFileFinder method and exclude test files from command discovery by [@davidhemphill](https://github.com/davidhemphill) in https://github.com/laravel/framework/pull/58017 -* [12.x] Fix Cache spy not working with memoized cache by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/57996 -* Respect --quiet and --silent in queue:work command by [@MatusBoa](https://github.com/MatusBoa) in https://github.com/laravel/framework/pull/58024 -* [12.x] Improve Blueprint docblocks with concrete value ranges for integer and text columns by [@nguyentranchung](https://github.com/nguyentranchung) in https://github.com/laravel/framework/pull/58019 -* [12.x] Modernize typecasting by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58037 -* [12.x] Fix `required` and `sometimes` validation of `Password` rule by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/58034 -* [12.x] Add support as a depdency for container by [@adrum](https://github.com/adrum) in https://github.com/laravel/framework/pull/58026 -* fix autoloading StringableObjectStub class in tests/Support/SupportStringableTest.php by [@angus-mcritchie](https://github.com/angus-mcritchie) in https://github.com/laravel/framework/pull/58030 -* [12.x] Remove calls to `optional()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58027 -* [12.x] Add `newRequest()` to Pool and Batch by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58038 -* [12.x] Align Listener docblock and add unit test for query shape by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/58040 -* [12.x] feat: add pre-migration hook when setting up databases in parallel tests by [@philipheimboeck](https://github.com/philipheimboeck) in https://github.com/laravel/framework/pull/58011 -* [12.x] Supports PHPUnit 12.5 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58042 -* [12.x] Add support for Enums in Translator replacements by [@hosni](https://github.com/hosni) in https://github.com/laravel/framework/pull/58048 -* [12.x] Fix `PendingRequest@pool()` && `batch()` concurrency by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57973 -* [12.x] New `illuminate/reflections` component from `illuminate/support` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58052 -* Make queue commands' option descriptions more consistent by [@jasonlbeggs](https://github.com/jasonlbeggs) in https://github.com/laravel/framework/pull/58058 -* [12.x] Add LICENSE, auto close for PRs and `.gitattributes` to `illuminate/reflection` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/58055 -* [12.x] `PendingRequest@withRequestContext()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/58054 - -## [v12.41.1](https://github.com/laravel/framework/compare/v12.41.0...v12.41.1) - 2025-12-03 - -## [v12.41.0](https://github.com/laravel/framework/compare/v12.40.2...v12.41.0) - 2025-12-03 - -* [12.x] Add `throwUnless()` to `Illuminate\Http\Client\Response` by [@CasEbb](https://github.com/CasEbb) in https://github.com/laravel/framework/pull/57951 -* [12.x] Fix deprecation error in `HasAttributes::addDateAttributesToArray()` when `UPDATED_AT = null` and model is cast to array by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57945 -* [12.x] Reduce indentation in `PendingRequest@send()` with an early return by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57943 -* [12.x] PendingRequest HTTP methods may also return promises by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57941 -* [12.x] Fix unable to flush redis tagged cache when prefix contains `-` instead of `_` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57927 -* fix: hasMorph/whereDoesntHaveMorph OR grouping with nullable morphs by [@hannrei](https://github.com/hannrei) in https://github.com/laravel/framework/pull/57937 -* [12.x] Flush only active buffers while streaming response using a generator by [@vaishnavyogesh](https://github.com/vaishnavyogesh) in https://github.com/laravel/framework/pull/57952 -* Fix substrReplace to be multibyte safe by [@Nasim25](https://github.com/Nasim25) in https://github.com/laravel/framework/pull/57936 -* [12.x] fixes static analysis error by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57953 -* [12.x] Pass `LoggerInterface` when constructing `RoundrobinTransport` instance by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/57956 -* [12.x] Optimize cache and cache_locks timeout by [@chrisnetonline](https://github.com/chrisnetonline) in https://github.com/laravel/framework/pull/57966 -* [12.x] Introduce `FluentPromise` to allow for cleaner chaining in Pool by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57967 -* [12.x] Add Dependency in JsonSchema by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/framework/pull/57942 -* Introduce `lazy` object and `proxy` object support helpers by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/57831 -* [12.x] Add reload command and allow services to register by [@barryvdh](https://github.com/barryvdh) in https://github.com/laravel/framework/pull/57923 -* [12.x] Ensure pretending flag is always reset in `pretend()` method by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/57968 -* [12.x] Always restore missing-attribute flag in `offsetExists()` by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/57970 -* [12.x] Fix Arr::first for ArrayObject and AsArrayObject values by [@prince-noman](https://github.com/prince-noman) in https://github.com/laravel/framework/pull/57969 -* [12.x] Use displayName() for custom job identification by [@hxnk](https://github.com/hxnk) in https://github.com/laravel/framework/pull/57499 -* [12.x] Expand Redis DurationLimiter tests by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/57947 -* [12.x] Fix grammar in event dispatcher comment by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/57983 -* Modernize email template by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57987 -* [12.x] Improve event types by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/57986 -* [12.x] Add ability to ignore queuePaused \ queueShouldRestart cache checks by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57975 -* [12.x] Fix cache:clear command exit code on failure by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/57988 -* Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/57994 -* [12.x] Add `milliseconds`, `weeks`, and `months` duration helpers to `Illuminate\Support` by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/57997 -* [12.x] Add wildcard pattern support to TrimStrings middleware by [@overtrue](https://github.com/overtrue) in https://github.com/laravel/framework/pull/57982 - -## [v12.40.2](https://github.com/laravel/framework/compare/v12.40.1...v12.40.2) - 2025-11-26 - -* [12.x] Modernize type casting by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/57914 -* [12.x] Improve missing attribute violation callable typehints by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/57910 -* [12.x] Improve discarded attribute violation callable typehints by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/57909 -* [12.x] add support for no mode in postgres full text search by [@opheus2](https://github.com/opheus2) in https://github.com/laravel/framework/pull/57915 -* [12.x] Guard RedisStore::scan() results against boolean failures by [@CicerBro](https://github.com/CicerBro) in https://github.com/laravel/framework/pull/57911 -* [12.x] Fix CallQueuedClosure::displayName after batch chain (#57597) by [@CreareWorks](https://github.com/CreareWorks) in https://github.com/laravel/framework/pull/57881 -* Pass Laravel context through with schedule tasks by [@jradtilbrook](https://github.com/jradtilbrook) in https://github.com/laravel/framework/pull/57918 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57926 -* [12.x] Eloquent Builder: completion for HigherOrderBuilderProxy fields by [@adelf](https://github.com/adelf) in https://github.com/laravel/framework/pull/57928 -* [12.x] fix: continue route matching rather than returning second fallbackRoute by [@ryzr](https://github.com/ryzr) in https://github.com/laravel/framework/pull/57922 -* [12.x] Pause a queue for given seconds by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/57917 -* Json Schema Contract by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57934 - -## [v12.40.1](https://github.com/laravel/framework/compare/v12.40.0...v12.40.1) - 2025-11-25 - -* Add support for instant column additions by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57907 - -## [v12.40.0](https://github.com/laravel/framework/compare/v12.39.0...v12.40.0) - 2025-11-25 - -* [12.x] Improve return type of `Str::replace()` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/57820 -* [12.x] Fixup PHP 8.5 deprecations in `SupportArrTest` by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/57822 -* Add daysOfMonth() method to schedule tasks on specific days by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/57817 -* [12.x] Add `encoding` validation rule for uploaded files by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/57823 -* [12.x] Allow CachedState properties to be nullable by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57826 -* [12.x] Resolve failing encoding test by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57827 -* [12.x] Fixing MemoizedStore with Redis Cluster by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57828 -* [12.x] Update encoding validation message by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57834 -* [12.x] Use `scopedIf` in `CacheManager::memo()` by [@angelej](https://github.com/angelej) in https://github.com/laravel/framework/pull/57833 -* [12.x] Fixing RedisTaggedCache::flushStale with PhpRedisClusterConnection by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57837 -* [12.x] Add default parameter support to `Request::fluent()` method by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/57840 -* [12.x] Fix embedded image Content-ID inconsistency in cloned emails by [@yinheli](https://github.com/yinheli) in https://github.com/laravel/framework/pull/57726 -* [12.x] PredisClusterConnection::keys() by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57841 -* [12.x] PHP 8.5 Compatibility by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57835 -* [12.x] RateLimiter remaining to 0 to prevent negative values. by [@Button99](https://github.com/Button99) in https://github.com/laravel/framework/pull/57851 -* [12.x] Update RequestException@report() to return false by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57847 -* Time by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57856 -* Feature/queue pause resume by [@yousefkadah](https://github.com/yousefkadah) in https://github.com/laravel/framework/pull/57800 -* [12.x] Fixing RedisTaggedCache::flushValues with PredisClusterConnection by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57848 -* [12.x] Improve typehints for `QueriesRelationships` by [@CasEbb](https://github.com/CasEbb) in https://github.com/laravel/framework/pull/57830 -* [12.x] Fix flaky test by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57864 -* [12.x] optimize `AbstractRouteCollection@toSymfonyRouteCollection()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57870 -* [12.x] optimize `AbstractRouteCollection@matchAgainstRoute()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57871 -* [12.x] Moving redis integration tests by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57860 -* [12.x] Incorrect result of MemoizedStore::many with numeric keys and empty prefix by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57862 -* [12.x] Add testWrapEdgeCases for Str::wrap edge cases by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/57861 -* [12.x] Add parameter validation to Collection::sliding() method. by [@Button99](https://github.com/Button99) in https://github.com/laravel/framework/pull/57875 -* [12.x] Update `path` method return type to always be a non-null string by [@IsmailBourbie](https://github.com/IsmailBourbie) in https://github.com/laravel/framework/pull/57873 -* Added Google's antigravity IDE support in ResolvesDumpSource.php by [@yeasherarafath](https://github.com/yeasherarafath) in https://github.com/laravel/framework/pull/57885 -* [12.x] Clean up queue pausing by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57863 -* [12.x] Simplify `ParsesQueue@parseQueue` logic by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/57886 -* [12.x] Improve lazy loading violation callable typehints by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57896 -* [12.x] Fix Accept header cache invalidation when header is modified by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/57874 -* [12.x] Fix flaky test in CacheArrayStore (increment) by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57905 -* [12.x] Fix flaky test in ArraySessionHandler (garbage collection) by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57904 -* [12.x] Fix flaky test in ArraySessionHandler (almost expired session) by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57903 -* [12.x] Fix flaky test in ArraySessionHandler (expired session) by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57902 - -## [v12.39.0](https://github.com/laravel/framework/compare/v12.38.1...v12.39.0) - 2025-11-18 - -* [12.x] `ApplicationBuilder@withExceptions()` improvements by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57778 -* Carry `--force` to `make:test` for generators with `--test` by [@CasEbb](https://github.com/CasEbb) in https://github.com/laravel/framework/pull/57777 -* [12.x] Fix `Request::getAcceptableContentTypes()` changes in Symfony 7.4 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57783 -* [12.x] Accept string bindings in give attribute by [@pjotrvdh](https://github.com/pjotrvdh) in https://github.com/laravel/framework/pull/57747 -* [12.x] Fix `WithCachedConfig` to work with parallel test runs by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57785 -* [12.x] Tailwind pagination styling/accessibility updates by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57793 -* [12.x] `RequestException`: attempt to summarize message before reporting by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57767 -* [12.x] create new `@hasStack` Blade directive by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57788 -* Add `--middleware` filter to `route:list` by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/57797 -* [12.x] Fix stale in-memory SQLite connections after re-migration in RefreshDatabase by [@PouyaPour](https://github.com/PouyaPour) in https://github.com/laravel/framework/pull/57716 -* [12.x] Type-hint the `ResourceCollection::$collection` property as nullable by [@lorenzolosa](https://github.com/lorenzolosa) in https://github.com/laravel/framework/pull/57807 -* [12.x] Fix `Factory@insert()` to allow for array casts by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57794 -* [12.x] Improve typehints for `Http::pool()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57811 - -## [v12.38.1](https://github.com/laravel/framework/compare/v12.38.0...v12.38.1) - 2025-11-13 - -* [12.x] Fix `GeneratorCommand` missing `possibleModels()` method by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57769 - -## [v12.38.0](https://github.com/laravel/framework/compare/v12.37.0...v12.38.0) - 2025-11-12 - -* [12.x] Cache the result of `configurationIsCached()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57665 -* [12.x] model:show command prompt for missing input with model suggestion by [@mrazinshaikh](https://github.com/mrazinshaikh) in https://github.com/laravel/framework/pull/57671 -* Don't call Model::toArray() to get attributes for factory insert by [@riesjart](https://github.com/riesjart) in https://github.com/laravel/framework/pull/57670 -* [12.x] Introduce `WithCachedRoutes` testing trait by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57623 -* [12.x] Add missing separators to `Stringable::ucwords` by [@kichetof](https://github.com/kichetof) in https://github.com/laravel/framework/pull/57688 -* [12.x] Cache result of `Application@routesAreCached()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57687 -* fix phpdoc return of HasAttributes::getArrayAttributeWithValue by [@chuckadams](https://github.com/chuckadams) in https://github.com/laravel/framework/pull/57691 -* [12.x] Remove unnecessary imports from BackgroundQueue and DeferredQueue. by [@seriquynh](https://github.com/seriquynh) in https://github.com/laravel/framework/pull/57699 -* [12.x] add SQLite support for whereNotMorphedTo method by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/57698 -* [12.x] Handle AWS ElasticCache failover by reconnecting when READONLY by [@wsamoht](https://github.com/wsamoht) in https://github.com/laravel/framework/pull/57685 -* [12.x] Introduce `WithCachedConfig` testing trait by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57663 -* [12.x] Fix WithCachedConfig@tearDownWithCachedConfig() by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57708 -* [12.x] Reorder some core aliases in alphabetical order. by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/57706 -* Allow Resend ^1.0 by [@ziming](https://github.com/ziming) in https://github.com/laravel/framework/pull/57713 -* [12.x] memoize result of `Application@eventsAreCached()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57709 -* [12.x] Test `Factory@insert()` with hidden by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57722 -* [12.x] Separate workflow for Redis integration tests by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57710 -* [12.x] Types: HasDatabaseNotifications read/unread notifications by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/57718 -* [12.x] Supports Symfony 7.4 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57724 -* [12.x] Revert lowercasing validation message placeholders by [@florianraith](https://github.com/florianraith) in https://github.com/laravel/framework/pull/57733 -* [12.x] try another way to activate Broadcast routes by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57734 -* [12.x] Add environment information to json output of schedule:list command by [@mmachatschek](https://github.com/mmachatschek) in https://github.com/laravel/framework/pull/57741 -* [12.x] Make DumpCommand prohibitable by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57735 -* [12.x] Clean ConsoleApplicationTest by [@seriquynh](https://github.com/seriquynh) in https://github.com/laravel/framework/pull/57761 -* [12.x] Fix the docblock of the BroadcastManager::purge method. by [@seriquynh](https://github.com/seriquynh) in https://github.com/laravel/framework/pull/57758 -* [12.x] Fix setting request exception truncating doesn't work on HTTP layer when configured inside `bootstrap/app.php` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57759 - -## [v12.37.0](https://github.com/laravel/framework/compare/v12.36.1...v12.37.0) - 2025-11-04 - -* [12.x] allow passing custom "depth" to `files()` and `directories()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57573 -* [12.x] EnumerateValues::value() support objects and return negative values by [@rafaelqueiroz](https://github.com/rafaelqueiroz) in https://github.com/laravel/framework/pull/57570 -* [12.x] Move duplicated logic to separate method to be reusable by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/57564 -* [12.x] Refactor unreleased data_has helper by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/57580 -* feat: added detailed about for cache failover driver by [@chinmaypurav](https://github.com/chinmaypurav) in https://github.com/laravel/framework/pull/57579 -* [12.x] fix data_has empty check by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/57586 -* [12.x] Fix: use trim before check empty string by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57583 -* feat: added detailed about for queue failover driver by [@chinmaypurav](https://github.com/chinmaypurav) in https://github.com/laravel/framework/pull/57582 -* Feat: add mailers detail for failover or roundrobin by [@chinmaypurav](https://github.com/chinmaypurav) in https://github.com/laravel/framework/pull/57590 -* [12.x] Refactor: remove un use var by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57617 -* [12.x] `Factory@insert()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57600 -* Fix ScheduleRunCommandTest failure on Windows by using OS-specific success command by [@Tina-1300](https://github.com/Tina-1300) in https://github.com/laravel/framework/pull/57621 -* [12.x] Add ucwords to Str and Stringable by [@braxey](https://github.com/braxey) in https://github.com/laravel/framework/pull/57581 -* [12.x] improve `Connection@listen()` docblock by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57633 -* [12.x] Fix: Correctly fallback to notification's connection/queue when using viaConnections/viaQueues by [@aydinfatih](https://github.com/aydinfatih) in https://github.com/laravel/framework/pull/57625 -* [12.x] Remove unused closure parameters in DatabaseServiceProvider by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57644 -* [12.x] Queue tests for Redis Cluster missing QUEUE_CONNECTION by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57641 -* refactor: remove unused parameter in ArtisanServiceProvider by [@omarchouman](https://github.com/omarchouman) in https://github.com/laravel/framework/pull/57658 -* [12.x] Ensure custom validation messages work for the File rule by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57656 -* Feature/json schema improvements by [@Anticom](https://github.com/Anticom) in https://github.com/laravel/framework/pull/57609 -* Process queue jobs in background (Concurrently::defer()) by [@barryvdh](https://github.com/barryvdh) in https://github.com/laravel/framework/pull/57648 -* [12.x] ChainedBatch keeps queue and connection of wrapped batch by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/57630 - -## [v12.36.1](https://github.com/laravel/framework/compare/v12.36.0...v12.36.1) - 2025-10-29 - -* [12.x] EnumerateValues::value() support and return negative values if exists #54910 by [@rafaelqueiroz](https://github.com/rafaelqueiroz) in https://github.com/laravel/framework/pull/57566 -* [12.x] always use the `operator` argument for `version_compare()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57569 -* [12.x] add `allDirectories()` method to `Filesytem` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57565 -* Revert "[12.x] EnumerateValues::value() support and return negative values if exists #54910" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57572 - -## [v12.36.0](https://github.com/laravel/framework/compare/v12.35.1...v12.36.0) - 2025-10-28 - -* [12.x] Remove return void from Http\Client\Batch's constructor by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/57518 -* [12.x] Namespace file cache lock keys by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/57516 -* [12.x] Remove [@return](https://github.com/return) tag from constructor by [@noir4y](https://github.com/noir4y) in https://github.com/laravel/framework/pull/57536 -* [12.x] Add missing [@throws](https://github.com/throws) annotation by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57535 -* [12.x] allow chaining on setters by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57532 -* [12.x] Stop double prefixing S3 filesystem paths by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57534 -* [12.x] test `Uri` builder methods by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57549 -* [12.x] Refactor `jsonSerialize()` method to use match expression by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/57552 -* [12.x] redirect response enforce same origin by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57533 -* [12.x] Add Eloquent Collection methods: `setAppends` && `withoutAppends` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57561 -* [12.x] Fix buffer overflow when flushing Redis cache tags with many keys by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/57562 -* [12.x] Allow validator message placeholers to be capitalized by [@florianraith](https://github.com/florianraith) in https://github.com/laravel/framework/pull/57556 -* Exclude property hooks on return of Model::__sleep() by [@rafaelqueiroz](https://github.com/rafaelqueiroz) in https://github.com/laravel/framework/pull/57557 -* [12.x] Add concurrency control to Http::pool and Http::batch by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/57555 - -## [v12.35.1](https://github.com/laravel/framework/compare/v12.35.0...v12.35.1) - 2025-10-23 - -* Store previous route name in session by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57477 -* [12.x] Update CSS in minimal exception view. by [@FoksVHox](https://github.com/FoksVHox) in https://github.com/laravel/framework/pull/57490 -* [12.x] Ensure HTTP batch results are returned in the same order as requested by [@jessarcher](https://github.com/jessarcher) in https://github.com/laravel/framework/pull/57483 -* [12.x] Correct `Response` namespace in `Batch` by [@simon-tma](https://github.com/simon-tma) in https://github.com/laravel/framework/pull/57481 -* [12.x] Rename NamedScope to Scope by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57478 -* Fix S3 adapter to use correct path separator and update related tests by [@Kleppinger](https://github.com/Kleppinger) in https://github.com/laravel/framework/pull/57497 -* [12.x] Replace Bootcamp with Laravel Learn by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57506 -* [12.x] Pass exception to `QueueFailedOver` event by [@jessarcher](https://github.com/jessarcher) in https://github.com/laravel/framework/pull/57503 -* [12.x] Add warning when server workers cannot be respected by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/57482 -* [12.x] Emit underlying store name in cache events when using failover by [@jessarcher](https://github.com/jessarcher) in https://github.com/laravel/framework/pull/57505 - -## [v12.35.0](https://github.com/laravel/framework/compare/v12.34.0...v12.35.0) - 2025-10-21 - -* [12.x] Fix `DB::update()` with subqueries is not supported for all databases by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57381 -* [12.x] Ensure custom validation messages work for AnyOf, Can and Enum by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57356 -* Added Neovim editor support in ResolvesDumpSource by [@cseknowledge](https://github.com/cseknowledge) in https://github.com/laravel/framework/pull/57392 -* Add clickable file reference for thrown exception by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/57400 -* [12.x] Render newlines in query tooltip by [@faisuc](https://github.com/faisuc) in https://github.com/laravel/framework/pull/57310 -* [12.x] Add SQS FIFO and fair queue messageGroup() method support by [@patrickcarlohickman](https://github.com/patrickcarlohickman) in https://github.com/laravel/framework/pull/57421 -* [12.x] Use MariaDB idiomatic `json_value()` by [@crishoj](https://github.com/crishoj) in https://github.com/laravel/framework/pull/57417 -* Deferred queue by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57428 -* Fix validate integer php doc type annotation by [@tei0110](https://github.com/tei0110) in https://github.com/laravel/framework/pull/57435 -* [12.x] Fix passing countable to Number::format() by [@riesjart](https://github.com/riesjart) in https://github.com/laravel/framework/pull/57434 -* [12.x] Update `Route::middleware` to accept null by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57436 -* Only replace first basePath occurrence in dump source href by [@fritz-c](https://github.com/fritz-c) in https://github.com/laravel/framework/pull/57458 -* [12.x] Add missing [@throws](https://github.com/throws) annotations to Encrypter class by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57451 -* [12.x] Add missing [@throws](https://github.com/throws) annotations to Database Connection class by [@sumaiazaman](https://github.com/sumaiazaman) in https://github.com/laravel/framework/pull/57452 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57461 -* Prevent TypeError in validateDigits when attribute value is an array by [@elyass-dehghan](https://github.com/elyass-dehghan) in https://github.com/laravel/framework/pull/57471 -* Bump vite from 7.1.6 to 7.1.11 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/57460 -* Failover cache by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57430 -* Must collect the unparsed event and payload when deferring events by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/57453 - -## [v12.34.0](https://github.com/laravel/framework/compare/v12.33.0...v12.34.0) - 2025-10-14 - -* [12.x] PostgreSQL virtual columns by [@tpetry](https://github.com/tpetry) in https://github.com/laravel/framework/pull/57290 -* [12.x] Make Vite asset path generation extendable via inheritance by [@daun](https://github.com/daun) in https://github.com/laravel/framework/pull/57292 -* [12.x] Improve `Str` docblocks related to factories by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57297 -* Add missing waitUntil method to FakeInvokedProcess by [@yondifon](https://github.com/yondifon) in https://github.com/laravel/framework/pull/57030 -* Add support for Zed Editor in ResolvesDumpSource by [@miguilimzero](https://github.com/miguilimzero) in https://github.com/laravel/framework/pull/57298 -* [12.x] Remove leftover workaround by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57306 -* Fix return type order in view function signature by [@MadBox-99](https://github.com/MadBox-99) in https://github.com/laravel/framework/pull/57304 -* Adds support for `Trae IDE` in the local exception page by [@sajjadhossainshohag](https://github.com/sajjadhossainshohag) in https://github.com/laravel/framework/pull/57300 -* [12.x] Add enum support to `Schedule::useCache()` by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/57311 -* [12.x] Fix remaining PHP 8.5 null index array deprecations by [@IonBazan](https://github.com/IonBazan) in https://github.com/laravel/framework/pull/57308 -* Regenerate session during Auth::login() by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/57204 -* [12.x] Formatting by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57321 -* Update text color in minimal error view to ensure better accessibility by [@FoksVHox](https://github.com/FoksVHox) in https://github.com/laravel/framework/pull/57318 -* [12.x] Fix text truncation on syntax-highlighted queries by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57315 -* [12.x] Fix email rule helper message by [@erik-perri](https://github.com/erik-perri) in https://github.com/laravel/framework/pull/57323 -* [12.x] Do not assume `Str::uuid()` returns `Stringable` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57340 -* [12.x] Add missing [@throws](https://github.com/throws) annotation to Arr by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57336 -* [12.x] Use FQCN in docblocks by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57335 -* [12.x] feat: Support custom response without modifying the exception handler by [@chuoke](https://github.com/chuoke) in https://github.com/laravel/framework/pull/57342 -* [12.X] add support for windsurf IDE in ResolvesDumpSource by [@Sajid-al-islam](https://github.com/Sajid-al-islam) in https://github.com/laravel/framework/pull/57359 -* [12.x] Expand single-line array into multiline by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57350 -* [12.x] Added Kiro editor support in `ResolvesDumpSource` by [@OmarFaruk-0x01](https://github.com/OmarFaruk-0x01) in https://github.com/laravel/framework/pull/57363 -* [12.x] fix schedule list cli format in multibye locale by [@jamessa](https://github.com/jamessa) in https://github.com/laravel/framework/pull/57367 -* Prototype failover queue by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57341 -* Add support for Fleet editor in ResolvesDumpSource by [@Rakib01](https://github.com/Rakib01) in https://github.com/laravel/framework/pull/57377 -* Allow closures when calling throw_if by [@chrispage1](https://github.com/chrispage1) in https://github.com/laravel/framework/pull/57349 -* [12.x] Add defer method to HTTP batch by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/57387 -* [12.x] Supports PHPUnit 12.4 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57388 -* [12.x] Http::batch - fix issue that non valid URL not triggering catch hook by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/57386 - -## [v12.33.0](https://github.com/laravel/framework/compare/v12.32.5...v12.33.0) - 2025-10-07 - -* Fix compiling queries that use orderByRaw with expressions by [@LukeTowers](https://github.com/LukeTowers) in https://github.com/laravel/framework/pull/57228 -* [12.x] Narrow type after `Str::is*(...)` check by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/57230 -* [12.x] Fix invalid docblock by [@tm1000](https://github.com/tm1000) in https://github.com/laravel/framework/pull/57240 -* [12.x] Refactor switch to match by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/57236 -* [12.x] Refactor switch to match by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57237 -* [12.x] Fix rounded issue in exception frame component by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57239 -* [12.x] Ensure calling job within a group works as expected by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57224 -* fix: remove duplicated word in `Str::apa` method by [@balboacodes](https://github.com/balboacodes) in https://github.com/laravel/framework/pull/57254 -* refactor: add |null in docblock by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57253 -* [12.x] Improve `php artisan config:cache` and `php artisan optimize` error messages for non-serializable values by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/57249 -* [12.x] Ensure cookie lifetime matches session lifetime in StartSession middleware by [@michaelcontento](https://github.com/michaelcontento) in https://github.com/laravel/framework/pull/57266 -* Run tests on PostgreSQL version 18 by [@JurianArie](https://github.com/JurianArie) in https://github.com/laravel/framework/pull/57232 -* [12x.] reduce repeated inserts in tests by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57273 -* [12.x] Fix using pushIf blade directive with complex conditions (#57264) by [@hosni](https://github.com/hosni) in https://github.com/laravel/framework/pull/57274 -* [12.x] Add Stringable::doesntContain() to match API symmetry by [@michaelcontento](https://github.com/michaelcontento) in https://github.com/laravel/framework/pull/57279 -* [12.x] Improve BroadcastManager error messages when trying to get a Broadcaster by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/57275 -* [12.x] HTTP Client: add mergeUrlParameters() to combine URL parameters without overwriting by [@leek](https://github.com/leek) in https://github.com/laravel/framework/pull/57282 - -## [v12.32.5](https://github.com/laravel/framework/compare/v12.32.4...v12.32.5) - 2025-09-30 - -## [v12.32.4](https://github.com/laravel/framework/compare/v12.32.3...v12.32.4) - 2025-09-30 - -* [12.x] Use `Container::getInstance()` in `ComposerScripts::prePackageUninstall()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57226 - -## [v12.32.3](https://github.com/laravel/framework/compare/v12.32.2...v12.32.3) - 2025-09-30 - -* [12.x] Define LARAVEL_START if not already defined by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57222 -* [12.x] Clean up redundant type hints in docblocks by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57219 - -## [v12.32.2](https://github.com/laravel/framework/compare/v12.32.1...v12.32.2) - 2025-09-30 - -## [v12.32.1](https://github.com/laravel/framework/compare/v12.32.0...v12.32.1) - 2025-09-30 - -* [13.x] Fix scopedBy attribute not following inheritance chain by [@Muffinman](https://github.com/Muffinman) in https://github.com/laravel/framework/pull/57213 -* [12.x] Fix AWS S3 adapter's constructor not allowing decorated adapter instances by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57217 - -## [v12.32.0](https://github.com/laravel/framework/compare/v12.31.1...v12.32.0) - 2025-09-30 - -* [12.x] fix static analysis error by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57162 -* Fix: Handle non-string returns from Htmlable::toHtml() in e() helper by [@Carnicero90](https://github.com/Carnicero90) in https://github.com/laravel/framework/pull/57157 -* [12.x] Fix pending attributes in schedule group by [@jamessa](https://github.com/jamessa) in https://github.com/laravel/framework/pull/57156 -* Remove Request overview from Exceptions by [@barryvdh](https://github.com/barryvdh) in https://github.com/laravel/framework/pull/57158 -* [12.x] Pass "throw" option from scoped to parent disk by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57163 -* [12.x] Make docblock return type in line with actual return type by [@parijke](https://github.com/parijke) in https://github.com/laravel/framework/pull/57164 -* [12.x] Adjust `Arr` typehints by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57165 -* [12.x] Track filesystem adapter decoration by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57167 -* [12.x] Batch Job Failure Callbacks Support by [@yitzwillroth](https://github.com/yitzwillroth) in https://github.com/laravel/framework/pull/55916 -* [12.x] Fix operator precedence by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57169 -* [12.x] Clean up after filesystem manager tests by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57168 -* Fix: Improve validateInteger ergonomics and fix BC break by [@ntm-dev](https://github.com/ntm-dev) in https://github.com/laravel/framework/pull/57175 -* [12.x] Fix nested `can` and inherit models on route groups by [@bonroyage](https://github.com/bonroyage) in https://github.com/laravel/framework/pull/57172 -* [12.x] Syntax highlight on the frontend by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57184 -* [12.x] Add missing Closure type to Collection::pluck() docblock by [@Bariss61](https://github.com/Bariss61) in https://github.com/laravel/framework/pull/57178 -* Add database afterRollback callback support and tests by [@maltekuhr](https://github.com/maltekuhr) in https://github.com/laravel/framework/pull/57180 -* fix: add return type by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57192 -* [12.x] Adds support enums for `ThrottleRequests::using` method by [@sethsandaru](https://github.com/sethsandaru) in https://github.com/laravel/framework/pull/57190 -* [12.x] Introduce "after" rate limiting by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/57125 -* [12.x] Json schema nullable by [@Katalam](https://github.com/Katalam) in https://github.com/laravel/framework/pull/57181 -* [12.x] Dispatch framework events on composer `pre-package-uninstall` event by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57144 -* [12.x] Add Http::batch by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/56946 -* [12.x] [Mail] Update `queue` PHPDoc according to function behavior by [@MrYamous](https://github.com/MrYamous) in https://github.com/laravel/framework/pull/57207 -* [12.x] Remove unnecessary parentheses by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57212 -* [12.x] Remove unnecessary parentheses by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57210 -* [12.x] Fixes error renderer report page by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/57208 -* [12.x] Extend SQS FIFO and fair queue support by [@patrickcarlohickman](https://github.com/patrickcarlohickman) in https://github.com/laravel/framework/pull/57187 - -## [v12.31.1](https://github.com/laravel/framework/compare/v12.31.0...v12.31.1) - 2025-09-23 - -* Revert "[12.x] Reintroduce short-hand "false" syntax for Blade component props" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57151 - -## [v12.31.0](https://github.com/laravel/framework/compare/v12.30.1...v12.31.0) - 2025-09-23 - -* Bump vite from 7.1.2 to 7.1.6 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/57114 -* [12.x] Reintroduce short-hand "false" syntax for Blade component props by [@PerryvanderMeer](https://github.com/PerryvanderMeer) in https://github.com/laravel/framework/pull/57104 -* [12.x] Allow Number parse helpers to return false by [@platoindebugmode](https://github.com/platoindebugmode) in https://github.com/laravel/framework/pull/57127 -* [12.x] Refactor `RedisTaggedCache@flush()` to allow for custom connections by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57122 -* [12.x] Use light-dark scheme for exception renderer by [@pxlrbt](https://github.com/pxlrbt) in https://github.com/laravel/framework/pull/57128 -* [12.x] Replace logger helper and log function concrete return type ?LogManager with abstract ?LoggerInterface by [@abdelrahmenAyman](https://github.com/abdelrahmenAyman) in https://github.com/laravel/framework/pull/57028 -* [12.x] Fix session value is missing assertion by [@barclaymichael](https://github.com/barclaymichael) in https://github.com/laravel/framework/pull/57134 -* median() div swapped for intdiv() by [@artumi-richard](https://github.com/artumi-richard) in https://github.com/laravel/framework/pull/57148 -* [12.x] Fix PHP 8.5 null-key deprecations by [@IonBazan](https://github.com/IonBazan) in https://github.com/laravel/framework/pull/57137 - -## [v12.30.1](https://github.com/laravel/framework/compare/v12.30.0...v12.30.1) - 2025-09-18 - -* [12.x] Fix: Apply intl extension check to ordinal position to prevent issues by [@BinaryKitten](https://github.com/BinaryKitten) in https://github.com/laravel/framework/pull/57112 - -## [v12.30.0](https://github.com/laravel/framework/compare/v12.29.0...v12.30.0) - 2025-09-18 - -* [12.x] Allow newer versions for phiki/phiki than 2.0.0 by [@hebbet](https://github.com/hebbet) in https://github.com/laravel/framework/pull/57075 -* [12.x] Use null coalescing for memoryExceededExitCode by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57090 -* [12.x] Fix 'can' function that was defined in RouterRegistrar in #54648 by [@pdewit](https://github.com/pdewit) in https://github.com/laravel/framework/pull/57072 -* [12.x] Fix SQS FIFO and fair queue support by [@patrickcarlohickman](https://github.com/patrickcarlohickman) in https://github.com/laravel/framework/pull/57080 -* atomically flush redis cache tags by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57098 -* [12.x] Add type hints to `\Illuminate\Support\Str` by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/57096 -* Doc: Update Database Connection getElapsedTime comment to specify unit by [@glensc](https://github.com/glensc) in https://github.com/laravel/framework/pull/57099 -* [12.x] Add support for Ordinal Position in validation messages by [@BinaryKitten](https://github.com/BinaryKitten) in https://github.com/laravel/framework/pull/57109 -* [12.x] Fix exception frame file path on Windows by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57103 -* Add fallback to copy buttons on new exception page by [@joaokamun](https://github.com/joaokamun) in https://github.com/laravel/framework/pull/57092 -* [12.x] Adds `Macroable` trait to `Illuminate/Support/Benchmark` by [@1tim22](https://github.com/1tim22) in https://github.com/laravel/framework/pull/57107 - -## [v12.29.0](https://github.com/laravel/framework/compare/v12.28.1...v12.29.0) - 2025-09-16 - -* Ensure cached and uncached routes share same precedence when resolving actions and names by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/56920 -* [12.x] Re-enable previously commented assertions by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56930 -* [12.x] Reorder .gitignore entries for consistency and readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56963 -* [12.x] SQLite: Allow setting any pragmas by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/56962 -* refactor: remove unused array from docblock by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/56961 -* PendingResourceRegistration withoutMiddleware never returns array by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/56959 -* [12.x] Allow not having "fakerphp/faker" installed by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/56953 -* [12.x] Fix Validator placeholderHash PHPDoc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56947 -* [12.x] Handle MariaDB innodb_snapshot_isolation=ON by [@Muffinman](https://github.com/Muffinman) in https://github.com/laravel/framework/pull/56945 -* [12.x] Add PhpRedis pack ignore numbers option by [@tuandp](https://github.com/tuandp) in https://github.com/laravel/framework/pull/56941 -* test(support): add edge-case tuples for preg_replace_array by [@realpvz](https://github.com/realpvz) in https://github.com/laravel/framework/pull/56937 -* [12.x] Allow for BackedEnum on dynamic blade component by [@gehrisandro](https://github.com/gehrisandro) in https://github.com/laravel/framework/pull/56940 -* [12.x] Remove one redundant array access by [@vincentvanhoven](https://github.com/vincentvanhoven) in https://github.com/laravel/framework/pull/56931 -* [12.x] Add withoutGlobalScopesExcept() to keep only specified global scopes by [@theHocineSaad](https://github.com/theHocineSaad) in https://github.com/laravel/framework/pull/56957 -* [12.x] Make visibility consistent by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56970 -* [12.x] Change list to tuple in PHPDoc block by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56967 -* [12.x] Improve `AggregateServiceProvider` docblocks by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56968 -* [12.x] add --whisper option to schedule:work command by [@thojo0](https://github.com/thojo0) in https://github.com/laravel/framework/pull/56969 -* [12.x] Update Faker suggestion to match skeleton version by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56974 -* Refactor: use str_contains() instead of strpos() for clarity by [@arshidkv12](https://github.com/arshidkv12) in https://github.com/laravel/framework/pull/56979 -* [12.x] remove unnecessary `with()` helper call by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56975 -* [12.x] Config: Move some items into pragmas by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56980 -* Add callback support to takeUntilTimeout in LazyCollection by [@kamilkozak](https://github.com/kamilkozak) in https://github.com/laravel/framework/pull/56981 -* [12.x] Utilize the is_finite() PHP function by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56990 -* [12.x] Use property promotion in `MessageLogged` and narrow `$level` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56989 -* [12.x] do not use `with()` helper when no second argument is passed by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56986 -* [12.x] Correct the type of $handler from Connection::whenQueryingForLongerThan by [@sethsandaru](https://github.com/sethsandaru) in https://github.com/laravel/framework/pull/56987 -* [12.x] Some quick fixes by [@theHocineSaad](https://github.com/theHocineSaad) in https://github.com/laravel/framework/pull/56991 -* tests: Ensure transaction callbacks run in FIFO order by [@realpvz](https://github.com/realpvz) in https://github.com/laravel/framework/pull/56973 -* Pass $attributes and $parent arguments to Factory Sequence by [@fritz-c](https://github.com/fritz-c) in https://github.com/laravel/framework/pull/56972 -* [12.x] - Support `Castable` on `Enum` by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/56977 -* [12.x] add trailing commas in multiline method signatures by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56992 -* [12.x] Improve docblocks for nullable parameters by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56995 -* [12.x] Improve docblocks for nullable parameters by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56996 -* [12.x] Improve docblocks for nullable parameters by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56997 -* Revert "[12.x] Config: Move some items into pragmas" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57003 -* [12.x]: Cache Session Driver by [@joaopalopes24](https://github.com/joaopalopes24) in https://github.com/laravel/framework/pull/56887 -* [12.x] Add support for #[UseResource(...)] and #[UseResourceCollection(...)] attributes on models by [@Lukasss93](https://github.com/Lukasss93) in https://github.com/laravel/framework/pull/56966 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57010 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57031 -* [12.x] Infinite method chaining in contextual binding builder by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57026 -* [12.x] Improved manager typehints by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57024 -* Bump vite from 5.4.19 to 5.4.20 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/57009 -* [12.x] Correct APC cache store docblock types by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/57020 -* [12.x] Enable dynamic tries() method on Queueable Listeners by [@glioympas](https://github.com/glioympas) in https://github.com/laravel/framework/pull/57014 -* [12.x] Add --json option to ScheduleListCommand by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/57006 -* [12.x] `with()` helper call simplification by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57041 -* [12.x] handle all Enum types for default values by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57040 -* [12.x] Refactor chained method calls for readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57050 -* [12.x] Improve docblock wording by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57056 -* [12.x] Refactor chained method calls for readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57054 -* [12.x] Update local exception page by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57036 -* [12.x] Add ability to control QueueWorker memory exceeded exit code by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57044 -* [12.x] Ensure `laravel-cloud-socket` respects `LOG_LEVEL` by [@PeteBishwhip](https://github.com/PeteBishwhip) in https://github.com/laravel/framework/pull/57071 - -## [v12.28.1](https://github.com/laravel/framework/compare/v12.28.0...v12.28.1) - 2025-09-04 - -* [12.x] Rename `group` to `messageGroup` property by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56919 -* Fix PHP_CLI_SERVER_WORKERS inside laravel/sail by [@akyrey](https://github.com/akyrey) in https://github.com/laravel/framework/pull/56923 -* Allow RouteRegistrar to be Macroable by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/56921 -* [12.x] Fix SesV2Transport docblock by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/56917 -* [12.x] Prevent unnecessary query logging on exceptions with a custom renderer by [@luanfreitasdev](https://github.com/luanfreitasdev) in https://github.com/laravel/framework/pull/56874 -* [12.x] Reduce meaningless intermediate variables by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56927 - -## [v12.28.0](https://github.com/laravel/framework/compare/v12.27.1...v12.28.0) - 2025-09-03 - -* [11.x] Correct how base options for missing config files are preloaded by [@u01jmg3](https://github.com/u01jmg3) in https://github.com/laravel/framework/pull/56216 -* [11.x] backport #56235 by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56236 -* [11.x] Consistent use of `mb_split()` to split strings into words by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56617 -* [11.x] `CacheSchedulingMutex` should use lock connection by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56614 -* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56630 -* [11.x] Update `orchestra/testbench-core` deps by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56636 -* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56649 -* [11.x] Fix exception page not preparing SQL bindings by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56651 -* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56849 -* [11.x] Chore: Decouple Str::random() from Validator by [@michaeldyrynda](https://github.com/michaeldyrynda) in https://github.com/laravel/framework/pull/56852 -* [11.x] Allow a wider range of `brick/math` versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56890 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56894 -* [12.x] Switch back to ternaries in `DatabaseManager` to allow for empty named connections by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56906 -* [12.x] Update config/database.php to match the latest skeleton configuration by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56905 -* Update fluent() helper by [@tanthammar](https://github.com/tanthammar) in https://github.com/laravel/framework/pull/56900 -* [12.x] Add method to retrieve the command on InvokedProcess by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/56886 -* [12.x] provide a default slot name when compiling by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56883 -* [12.x] Allow enums on model connection property and methods by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56896 -* [12.x] Adds internal class by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/56903 - -## [v12.27.1](https://github.com/laravel/framework/compare/v12.27.0...v12.27.1) - 2025-09-02 - -* [12.x] Allow a wider range of `brick/math` versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56891 -* [12.x] Fix secure_url() breaking changes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56885 - -## [v12.27.0](https://github.com/laravel/framework/compare/v12.26.4...v12.27.0) - 2025-09-02 - -* [12.x] Add prepend option for Str::plural() by [@caseydwyer](https://github.com/caseydwyer) in https://github.com/laravel/framework/pull/56802 -* [12.x] Fix multi-line embedded image replacement in mail views by [@iammursal](https://github.com/iammursal) in https://github.com/laravel/framework/pull/56828 -* [12.x] Add supports for SQS Fair Queue by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56763 -* [12.x] Support enum values in `Collection` `countBy` method by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56830 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56838 -* [12.x] Fix docblocks and all() method in ArrayStore for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56845 -* [12.x] Improve Grammar in ArrayLock by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56844 -* [12.x] Normalize comments for timestampsTz() and nullableTimestampsTz() by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56840 -* [12.x] Reduce meaningless intermediate variables by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56843 -* [12.x] Simpler and consistent `Arr::collapse()` by [@weshooper](https://github.com/weshooper) in https://github.com/laravel/framework/pull/56842 -* [12.x] Improving readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56847 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56850 -* [12.x] Remove extra space before line number in exception trace by [@mtbossa](https://github.com/mtbossa) in https://github.com/laravel/framework/pull/56863 -* [12.x] Remove unused variable by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56861 -* [12.x] Add support for `UnitEnum` in `Collection` `groupBy` method by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56857 -* [12.x] Add missing void return type to test methods by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56860 -* [12.x] Improve `countBy` docblock in `Collection` to allow for enum callback by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56856 -* [12.x] Improve `InteractsWithContainer` return types by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/56853 -* [12.x] Allow mass assignment for value object casting. by [@AbdelElrafa](https://github.com/AbdelElrafa) in https://github.com/laravel/framework/pull/56871 -* [12.x] Allows `APP_BASE_PATH` from `$_SERVER` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56868 -* [12.x] Fix typo in docblock by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/56867 -* [12.x] Allow enums in other DatabaseManager methods by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56878 -* Add health score badge to README by [@jonathimer](https://github.com/jonathimer) in https://github.com/laravel/framework/pull/56875 -* [12.x] Let `toPrettyJson()` accepts options by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/56876 - -## [v12.26.4](https://github.com/laravel/framework/compare/v12.26.3...v12.26.4) - 2025-08-29 - -* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56792 -* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56794 -* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56795 -* [12.x] Add support for nested array notation within `loadMissing` by [@angus-mcritchie](https://github.com/angus-mcritchie) in https://github.com/laravel/framework/pull/56711 -* [12.x] Colocate Container build functions with the `SelfBuilding` interface by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56731 -* perf: optimize loop performance by pre-calculating array counts in Str::apa() and fileSize() methods by [@AmadulHaque](https://github.com/AmadulHaque) in https://github.com/laravel/framework/pull/56796 -* fix: Helper function secure_url not always returning a string by [@SOD96](https://github.com/SOD96) in https://github.com/laravel/framework/pull/56807 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56803 -* [12.x] Parse Redis "friendly" algorithm names into integers by [@mateusjatenee](https://github.com/mateusjatenee) in https://github.com/laravel/framework/pull/56800 -* [12.x] Remove [@return](https://github.com/return) tags from constructors by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56814 -* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56813 -* [12.x] Use FQCN for [@mixin](https://github.com/mixin) annotation for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56811 -* [12.x] Remove leftover `method_exists` checks by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/56821 -* [12.x] Fix use array_first and array_last by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56820 -* Support enum in Collection -> keyBy() by [@zKoz210](https://github.com/zKoz210) in https://github.com/laravel/framework/pull/56786 -* Adds make:config command by [@inmanturbo](https://github.com/inmanturbo) in https://github.com/laravel/framework/pull/56819 - -## [v12.26.3](https://github.com/laravel/framework/compare/v12.26.2...v12.26.3) - 2025-08-27 - -* [12.x] add back return type by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56774 -* fix: base class guard in return types is breaking custom guards by [@phadaphunk](https://github.com/phadaphunk) in https://github.com/laravel/framework/pull/56779 -* [12.x] Standardise polyfill dependencies by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56781 -* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56790 -* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56789 -* [12.x] Improve output grammar in `ScheduleRunCommand` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56776 - -## [v12.26.2](https://github.com/laravel/framework/compare/v12.26.1...v12.26.2) - 2025-08-26 - -* [12.x] fix: csrf_token can return null by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56768 -* [12.x] Fix `date_format` validation on DST Timezone by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56767 -* [12.x] Fix event helper by [@jasonvarga](https://github.com/jasonvarga) in https://github.com/laravel/framework/pull/56773 - -## [v12.26.1](https://github.com/laravel/framework/compare/v12.26.0...v12.26.1) - 2025-08-26 - -* [12.x] fix: add polyfill requirement to illuminate packages by [@erikgaal](https://github.com/erikgaal) in https://github.com/laravel/framework/pull/56765 -* [12.x] revert changes to `old()` helper by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56769 - -## [v12.26.0](https://github.com/laravel/framework/compare/v12.25.0...v12.26.0) - 2025-08-26 - -* [12.x] feat: add native return types to helper functions by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56684 -* [12.x] Allow passing enum to `Database` attribute by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56688 -* [12.x] Clean up redundant type hints in docblocks by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56690 -* Add ability to specify a transaction mode for SQLite connection by [@panda-madness](https://github.com/panda-madness) in https://github.com/laravel/framework/pull/56681 -* [12.x] Fix `spliceIntoPosition` docblock to allow `string|int` values by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56698 -* [12.x] Use array_first and array_last polyfills by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/56703 -* [12.x] Fix path to Str in exception markdown by [@apreiml](https://github.com/apreiml) in https://github.com/laravel/framework/pull/56705 -* [12.x] Add `withHeartbeat` method to `LazyCollection` by [@JosephSilber](https://github.com/JosephSilber) in https://github.com/laravel/framework/pull/56477 -* [12.x] Add toPrettyJson method by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/56697 -* [12.x] Use `array_first` and `array_last` by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/56706 -* [12.x] Do not dispatch `MessageLogged` twice by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56713 -* [12.x] Order classes alphabetically by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56743 -* [12.x] Normalize file path separators for commands on Windows by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56734 -* [12.x] Improve `queue:prune-failed` tests coverage by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56732 -* [12.x] Align trait usage for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56727 -* [12.x] Fix composer suggests for illuminate/container by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56722 -* Add nullableTimestampsTz method to Blueprint by [@mohamedhabibwork](https://github.com/mohamedhabibwork) in https://github.com/laravel/framework/pull/56720 -* Add possibility to override symbol when using currency format by [@PhilippeThouvenot](https://github.com/PhilippeThouvenot) in https://github.com/laravel/framework/pull/56749 -* [12.x] Revert #56608 by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56752 -* Revert "Add possibility to override symbol when using currency format" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/56753 -* [12.x] Support `null` parameter in `BusFake::chain()` method by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/56750 -* [12.x] Remove unnecessary return in ddBody for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56759 -* [12.x] Make interface accept UnitEnum by [@parijke](https://github.com/parijke) in https://github.com/laravel/framework/pull/56758 -* [12.x] Fix concurrency closure invocation: use base64 encoding by [@sashko-guz](https://github.com/sashko-guz) in https://github.com/laravel/framework/pull/56757 -* [12.x] `ArrayStore::all()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56751 -* [12.x] Fix: Add `$forceWrap` property to JsonResource for consistent API response #56724 by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/56736 -* [12.x] Ensures casts objects can be transformed into strings by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/56687 - -## [v12.25.0](https://github.com/laravel/framework/compare/v12.24.0...v12.25.0) - 2025-08-18 - -* [12.x] Prioritize Current Schema When Resolving the Table Name in `db:table` Command by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/56646 -* [12.x] Add `allowedUrls` through `preventStrayRequests` by [@rabrowne85](https://github.com/rabrowne85) in https://github.com/laravel/framework/pull/56645 -* [12.x] Add "Copy as Markdown" button to error page by [@mpociot](https://github.com/mpociot) in https://github.com/laravel/framework/pull/56657 -* [12.x] Indicate that `Context@scope()` may throw by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56655 -* [12.x] Remove [@throws](https://github.com/throws) phpDocs in the TransformToResource trait by [@adelf](https://github.com/adelf) in https://github.com/laravel/framework/pull/56667 -* [12.x] Improve docblocks for InteractsWithDatabase by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56666 -* [12.x] Fix prevent group attribute pollution in schedule by [@People-Sea](https://github.com/People-Sea) in https://github.com/laravel/framework/pull/56677 -* Add new `mergeVisible`, `mergeHidden` and `mergeAppends` methods. by [@jonerickson](https://github.com/jonerickson) in https://github.com/laravel/framework/pull/56678 - -## [v12.24.0](https://github.com/laravel/framework/compare/v12.23.1...v12.24.0) - 2025-08-13 - -* [8.4] Use PHP 8.4 array helpers in Arr utils by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56631 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56635 -* [12.x] Update `orchestra/testbench-core` deps by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56637 -* refactor: update cid param name by [@cpenned](https://github.com/cpenned) in https://github.com/laravel/framework/pull/56634 -* [12.x] Cache Singleton/Scoped attribute checks by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56633 -* [12.x] Add `Arr::push()` by [@inxilpro](https://github.com/inxilpro) in https://github.com/laravel/framework/pull/56632 -* [12.x] Add error message for `doesnt_contain` rule by [@apih](https://github.com/apih) in https://github.com/laravel/framework/pull/56644 - -## [v12.23.1](https://github.com/laravel/framework/compare/v12.23.0...v12.23.1) - 2025-08-12 - -## [v12.23.0](https://github.com/laravel/framework/compare/v12.22.1...v12.23.0) - 2025-08-12 - -* [12.x] Prevent unintended sleep on early failure of assertSequence by [@xHeaven](https://github.com/xHeaven) in https://github.com/laravel/framework/pull/56583 -* [12.x] Redis cluster broadcaster by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/56581 -* [12.x] Alias Benchmark class by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/56594 -* [12.x] Add support for drop patterns to the `make:migration` command's `TableGuesser`. by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56608 -* [12.x] Improve collection return types by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/56599 -* [12.x] Fix collection typo by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/56597 -* Fix return type docblock for resetAttempts method in RateLimiter by [@jonagoldman](https://github.com/jonagoldman) in https://github.com/laravel/framework/pull/56596 -* Add 'page' field to paginator links by [@compico](https://github.com/compico) in https://github.com/laravel/framework/pull/56603 -* [12.x] Add support for inline attachments in Resend transport by [@jayanratna](https://github.com/jayanratna) in https://github.com/laravel/framework/pull/56598 -* Fix test failures in PHPUnit 12.3.2 by [@KentarouTakeda](https://github.com/KentarouTakeda) in https://github.com/laravel/framework/pull/56610 -* [12.x] Use new error and exception handler getters by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56623 -* [12.x] Use PHP 8.4 array helpers by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56619 -* [12.x] Prefer Symfony PHP polyfills over `function_exists` calls by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56621 -* [12.x] `Bind` attribute accepts UnitEnum by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56616 -* [12.x] Add Vitess-specific safe to retry errors by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56615 -* [12.x] Handle null as a falsy condition by [@negoziator](https://github.com/negoziator) in https://github.com/laravel/framework/pull/56612 -* Added "after" support for morphs and nullableMorphs Blueprint by [@marcogermani87](https://github.com/marcogermani87) in https://github.com/laravel/framework/pull/56613 -* [12.x] Fix usage of `Scoped` and `Singleton` on interfaces by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56620 -* [12.x] Online (concurrently) index creation for PostgreSQL and SqlServer by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/56625 - -## [v12.22.1](https://github.com/laravel/framework/compare/v12.21.0...v12.22.1) - 2025-08-08 - -* [12.x] Improved assertion message by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56579 -* [12.x] Fixed version increment by [@dciancu](https://github.com/dciancu) in https://github.com/laravel/framework/pull/56588 -* [12.x] Normalize file path separators in `make:migration` command on Windows by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56591 -* Revert "[12.x] Improve PHPDoc blocks for array of arguments in Gate" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/56593 - -## [v12.21.0](https://github.com/laravel/framework/compare/v12.20.0...v12.21.0) - 2025-07-22 - -* fix(vite): #55793 add explicit as-script to link tag for script modul… by [@midsonlajeanty](https://github.com/midsonlajeanty) in https://github.com/laravel/framework/pull/55794 -* [12.x] Allow globally disabling Factory parent relationships via `Factory::dontExpandRelationshipsByDefault()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56154 -* [12.x] Adds checking if a value is between two columns by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/56119 -* [12.x] Ensure database connection is always restored by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56258 -* [12.x] Fix handling of `Htmlable` objects in `Js::convertDataToJavaScriptExpression()` by [@jj15asmr](https://github.com/jj15asmr) in https://github.com/laravel/framework/pull/56253 -* Reduce meaningless intermediate variables. by [@LjjGit](https://github.com/LjjGit) in https://github.com/laravel/framework/pull/56265 -* [12.x] Improve typehints for `AbstractCursorPaginator@through()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56267 -* Use `Date` facade instead of `time()` for `password_confirmed_at` check by [@dylanbr](https://github.com/dylanbr) in https://github.com/laravel/framework/pull/56270 -* [12.x] fix: Collection::transform() and Paginator::through() return types by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56273 -* [12.x] Merge 11.x into 12.x by [@u01jmg3](https://github.com/u01jmg3) in https://github.com/laravel/framework/pull/56289 -* [12.x] Reduce meaningless intermediate variables by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56288 -* [12.x] Refactor build Method to Use Null Coalescing Assignment for Default C… by [@Ashot1995](https://github.com/Ashot1995) in https://github.com/laravel/framework/pull/56283 -* [12.x] minor code formatting improvements by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56296 -* [12.x] Use more specific route binding exception message for child routes by [@jessekoerhuis](https://github.com/jessekoerhuis) in https://github.com/laravel/framework/pull/56298 -* [12.x] Fix Possible Undefined Variables by [@calfc](https://github.com/calfc) in https://github.com/laravel/framework/pull/56292 -* [12.x] Fix: Ensure scheduler `dailyAt()` method parses minutes and ignores seconds when seconds are provided by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56308 -* [12.x] Allows for strict boolean validation by [@peterfox](https://github.com/peterfox) in https://github.com/laravel/framework/pull/56313 -* Improve `SeedCommand` console output by [@Jehong-Ahn](https://github.com/Jehong-Ahn) in https://github.com/laravel/framework/pull/56310 -* [12.x] Add unified enum support across framework docs by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56271 -* [12.x] Allows for strict numeric validation by [@peterfox](https://github.com/peterfox) in https://github.com/laravel/framework/pull/56328 -* [12.x] Update PHPDoc annotations in `Validation` by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/56321 -* [12.x] Add operator class support for PostgreSQL GiST spatial indexes by [@joteejotee](https://github.com/joteejotee) in https://github.com/laravel/framework/pull/56324 -* Fix multipart array value parsing in HTTP client (#55732) by [@joteejotee](https://github.com/joteejotee) in https://github.com/laravel/framework/pull/56302 -* Fixes bug with ShouldBeUniqueUntilProcessing locks getting stuck due to Middleware by [@TWithers](https://github.com/TWithers) in https://github.com/laravel/framework/pull/56318 -* [12.x] add prompts based expectations to PendingCommand by [@BinaryKitten](https://github.com/BinaryKitten) in https://github.com/laravel/framework/pull/56260 -* [12.x] Add Singleton and Scoped attributes to Container by [@riasvdv](https://github.com/riasvdv) in https://github.com/laravel/framework/pull/56334 -* Fix unsetting model castable attribute when cast to object (#56335) by [@guram-vashakidze](https://github.com/guram-vashakidze) in https://github.com/laravel/framework/pull/56343 -* [12.x] Fix/memory improvement by [@CharrafiMed](https://github.com/CharrafiMed) in https://github.com/laravel/framework/pull/56345 -* [12.x] Add hasMailer method to the mailable class by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/56340 -* [12.x] Consistent use of `mb_split()` to split strings into words by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56338 -* [12.x] Add toStringable to Uri by [@Kyrch](https://github.com/Kyrch) in https://github.com/laravel/framework/pull/56359 -* [12.x] Fix PHPStan Integrations by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56369 -* Add 'isEmpty' and 'isNotEmpty' to Fluent by [@cworreschk](https://github.com/cworreschk) in https://github.com/laravel/framework/pull/56370 -* [12.x] Add mergeMetadata method to the Mailable class by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/56376 -* Add 'dontReportUsing' to filter exceptions to be reported by [@pelmered](https://github.com/pelmered) in https://github.com/laravel/framework/pull/56361 - -## [v12.20.0](https://github.com/laravel/framework/compare/v12.19.3...v12.20.0) - 2025-07-08 - -* [12.x] Pass TransportException to NotificationFailed event by [@hackel](https://github.com/hackel) in https://github.com/laravel/framework/pull/56061 -* [12.x] use `offset()` in place of `skip()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56081 -* [12.x] use `limit()` in place of `take()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56080 -* [12.x] Display job queue names when running queue:work with --verbose option by [@seriquynh](https://github.com/seriquynh) in https://github.com/laravel/framework/pull/56086 -* [12.x] use `offset()` and `limit()` in tests by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56089 -* [12.x] Localize “Pagination Navigation” aria-label by [@andylolz](https://github.com/andylolz) in https://github.com/laravel/framework/pull/56103 -* [12.x] Enhance the test coverage for Pipeline::through() by [@azim-kordpour](https://github.com/azim-kordpour) in https://github.com/laravel/framework/pull/56100 -* [12.x] Added `JsonSerializable` interface to `Uri` Class by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/56097 -* [12.x] Display job connection name when running queue:work with --verbose option by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56095 -* [12.x] Fix PHPDoc for Arr::sole method by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56096 -* [12.x] when a method returns `$this` set the return type to `static` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56092 -* [12.x] Use `int<0, max>` as docblock return type for database operations that return a count by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56117 -* [12.x] Add missing [@throws](https://github.com/throws) annotation to Number by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56116 -* [12.x] Correct PHPDoc for Arr::sole callable type to avoid return type ambiguity by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56108 -* Change return types of through (pagination) and transform (collection) by [@glamorous](https://github.com/glamorous) in https://github.com/laravel/framework/pull/56105 -* [12.x] Add maintenance mode facade for easier driver extension by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/56090 -* [12.x] Cache isSoftDeletable(), isPrunable(), and isMassPrunable() directly in model by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56078 -* [12.x] Throws not throw by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56120 -* [12.x] Fix [@param](https://github.com/param) docblock to allow string by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56121 -* [11.x] Pass the limiter to the when & report callbacks by [@jimmypuckett](https://github.com/jimmypuckett) in https://github.com/laravel/framework/pull/56129 -* [12.x] remove the "prefix" option for cache password resets by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56127 -* [12.x] Make Model::currentEncrypter public by [@JaZo](https://github.com/JaZo) in https://github.com/laravel/framework/pull/56130 -* [12.x] Add throws docblock by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56137 -* [12.x] Narrow integer range for `Collection` methods by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56135 -* [12.x] Allows using `--model` and `--except` via `PruneCommand` command by [@hosni](https://github.com/hosni) in https://github.com/laravel/framework/pull/56140 -* [12.x] Support Passing `Htmlable` Instances to `Js::from()` by [@jj15asmr](https://github.com/jj15asmr) in https://github.com/laravel/framework/pull/56159 -* #56124 Properly escape column defaults by [@asmecher](https://github.com/asmecher) in https://github.com/laravel/framework/pull/56158 -* [12.x] Return early on belongs-to-many relationship `syncWithoutDetaching` method when empty values are given by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/56157 -* [12.x] Add fakeFor and fakeExceptFor methods to Queue facade by [@MrPunyapal](https://github.com/MrPunyapal) in https://github.com/laravel/framework/pull/56149 -* [11.x] Backport test fixes by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56183 -* Revert "[11.x] Pass the limiter to the when & report callbacks" by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56184 -* Add failWhen method to ThrottlesExceptions job middleware by [@michaeldzjap](https://github.com/michaeldzjap) in https://github.com/laravel/framework/pull/56180 -* [12.x] Update Castable contract to accept string array by [@hosmelq](https://github.com/hosmelq) in https://github.com/laravel/framework/pull/56177 -* Feature: doesntStartWith() and doesntEndWith() string methods by [@balboacodes](https://github.com/balboacodes) in https://github.com/laravel/framework/pull/56168 -* [12.x] Add context remember functions by [@btaskew](https://github.com/btaskew) in https://github.com/laravel/framework/pull/56156 -* [12.x] Fix queue fake cleanup to always restore original queue manager by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56165 -* [12.x] Pass the limiter to the when & report callbacks by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56187 -* [12.x] Add `Closure`-support to `$key`/`$value` in Collection `pluck()` method by [@ralphjsmit](https://github.com/ralphjsmit) in https://github.com/laravel/framework/pull/56188 -* [12.x] Add `collection()` to Config repository by [@KennedyTedesco](https://github.com/KennedyTedesco) in https://github.com/laravel/framework/pull/56200 -* Add int to allowed types of value in DatabaseRule by [@vkarchevskyi](https://github.com/vkarchevskyi) in https://github.com/laravel/framework/pull/56199 -* [12.x] Fix Event fake cleanup to always restore original event dispatcher by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56189 -* [12.x] Align PHPDoc style in Number::parseFloat with the rest of the class by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56206 -* [12.x] Inconsistent use of [@return](https://github.com/return) type by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56207 -* [12.x] Resolve issue with Factory make when automatic eager loading by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/56211 -* [12.x] Refactor driver initialization using null coalescing assignment in Manager by [@Ashot1995](https://github.com/Ashot1995) in https://github.com/laravel/framework/pull/56210 -* [12.x] Add URL signature macros to `Request` docblock by [@duncanmcclean](https://github.com/duncanmcclean) in https://github.com/laravel/framework/pull/56230 -* [12.x] Update PHPDoc for dataForSometimesIteration by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/56229 -* [12.x] Avoid unnecessary filtering when no callback is provided by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56225 -* [12.x] Make `Fluent` class iterable by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56218 -* Improve Mailable assertion error messages with expected vs actual values by [@ahinkle](https://github.com/ahinkle) in https://github.com/laravel/framework/pull/56221 -* [12.x] Add `@​context` Blade directive by [@martinbean](https://github.com/martinbean) in https://github.com/laravel/framework/pull/56146 -* [12.x] fix: AsCommand properties not being set on commands by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56235 -* [12.x] Ensure `withLocale` and `withCurrency` always restore previous state by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56234 - -## [v12.19.3](https://github.com/laravel/framework/compare/v12.19.2...v12.19.3) - 2025-06-18 - -* [12.x] Fix model pruning when non model files are in the same directory by [@rojtjo](https://github.com/rojtjo) in https://github.com/laravel/framework/pull/56071 - -## [v12.19.2](https://github.com/laravel/framework/compare/v12.19.1...v12.19.2) - 2025-06-17 - -## [v12.19.1](https://github.com/laravel/framework/compare/v12.19.0...v12.19.1) - 2025-06-17 - -* Revert "[12.x] Check if file exists before trying to delete it" by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56072 - -## [v12.19.0](https://github.com/laravel/framework/compare/v12.18.0...v12.19.0) - 2025-06-17 - -* [11.x] Fix validation to not throw incompatible validation exception by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55963 -* [12.x] Correct testEncryptAndDecrypt to properly test new methods by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/55985 -* [12.x] Check if file exists before trying to delete it by [@Jellyfrog](https://github.com/Jellyfrog) in https://github.com/laravel/framework/pull/55994 -* Clear cast caches when discarding changes by [@willtj](https://github.com/willtj) in https://github.com/laravel/framework/pull/55992 -* [12.x] Handle Null Check in Str::contains by [@Jellyfrog](https://github.com/Jellyfrog) in https://github.com/laravel/framework/pull/55991 -* [12.x] Remove call to deprecated `getDefaultDescription` method by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/55990 -* Bump brace-expansion from 2.0.1 to 2.0.2 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55999 -* Enhance error handling in PendingRequest to convert TooManyRedirectsE… by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55998 -* [12.x] fix: remove Model intersection from UserProvider contract by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56013 -* [12.x] Remove the only [@return](https://github.com/return) tag left on a constructor by [@JordanchoEftimov](https://github.com/JordanchoEftimov) in https://github.com/laravel/framework/pull/56001 -* [12.x] Introduce `ComputesOnceableHashInterface` by [@Jacobs63](https://github.com/Jacobs63) in https://github.com/laravel/framework/pull/56009 -* [12.x] Add assertRedirectBackWithErrors to TestResponse by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55987 -* [12.x] collapseWithKeys - Prevent exception in base case by [@DeanWunder](https://github.com/DeanWunder) in https://github.com/laravel/framework/pull/56002 -* [12.x] Standardize size() behavior and add extended queue metrics support by [@sylvesterdamgaard](https://github.com/sylvesterdamgaard) in https://github.com/laravel/framework/pull/56010 -* [11.x] Fix `symfony/console:7.4` compatibility by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56015 -* [12.x] Improve constructor PHPDoc for controller middleware definition by [@JordanchoEftimov](https://github.com/JordanchoEftimov) in https://github.com/laravel/framework/pull/56021 -* Remove `@return` tags from constructors by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/56024 -* [12.x] sort helper functions in alphabetic order by [@gigabites19](https://github.com/gigabites19) in https://github.com/laravel/framework/pull/56031 -* [12.x] add Attachment::fromUploadedFile method by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/56027 -* [12.x]: Add UseEloquentBuilder attribute to register custom Eloquent Builder by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/56025 -* [12.x] Improve PHPDoc for the Illuminate\Cache folder files by [@JordanchoEftimov](https://github.com/JordanchoEftimov) in https://github.com/laravel/framework/pull/56028 -* [12.x] Add a new model cast named asFluent by [@azim-kordpour](https://github.com/azim-kordpour) in https://github.com/laravel/framework/pull/56046 -* [12.x] Introduce `FailOnException` job middleware by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56037 -* [12.x] isSoftDeletable(), isPrunable(), and isMassPrunable() to model class by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56060 - -## [v12.18.0](https://github.com/laravel/framework/compare/v12.17.0...v12.18.0) - 2025-06-10 - -* document `through()` method in interfaces to fix IDE warnings by [@harryqt](https://github.com/harryqt) in https://github.com/laravel/framework/pull/55925 -* [12.x] Add encrypt and decrypt Str helper methods by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/55931 -* [12.x] Add a command option for making batchable jobs by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/55929 -* [12.x] fix: intersect Authenticatable with Model in UserProvider phpdocs by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/54061 -* [12.x] feat: create UsePolicy attribute by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55882 -* [12.x] `ScheduledTaskFailed` not dispatched on scheduled forground task fails by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55624 -* [12.x] Add generics to `Model::unguarded()` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/55932 -* [12.x] Fix SSL Certificate and Connection Errors Leaking as Guzzle Exceptions by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55937 -* Fix deprecation warning in PHP 8.3 by ensuring string type in explode() by [@Khuthaily](https://github.com/Khuthaily) in https://github.com/laravel/framework/pull/55939 -* revert: #55939 by [@NickSdot](https://github.com/NickSdot) in https://github.com/laravel/framework/pull/55943 -* [12.x] feat: Add WorkerStarting event when worker daemon starts by [@Orrison](https://github.com/Orrison) in https://github.com/laravel/framework/pull/55941 -* [12.x] Allow setting the `RequestException` truncation limit per request by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55897 -* [12.x] feat: Make custom eloquent castings comparable for more granular isDirty check by [@SanderSander](https://github.com/SanderSander) in https://github.com/laravel/framework/pull/55945 -* [12.x] fix alphabetical order by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55965 -* [12.x] Use native named parameter instead of unused variable by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55964 -* [12.x] add generics to Model attribute related methods and properties by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55962 -* [12.x] Supports PHPUnit 12.2 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55961 -* [12.x] feat: Add ability to override SendQueuedNotifications job class by [@Orrison](https://github.com/Orrison) in https://github.com/laravel/framework/pull/55942 -* [12.x] Fix timezone validation test for PHP 8.3+ by [@platoindebugmode](https://github.com/platoindebugmode) in https://github.com/laravel/framework/pull/55956 -* Broadcasting Utilities by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55967 -* [12.x] Remove unused $guarded parameter from testChannelNameNormalization method by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55973 -* [12.x] Validate that `outOf` is greater than 0 in `Lottery` helper by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/55969 -* [12.x] Allow retrieving all reported exceptions from `ExceptionHandlerFake` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55972 - -## [v12.17.0](https://github.com/laravel/framework/compare/v12.16.0...v12.17.0) - 2025-06-03 - -* [11.x] Backport `TestResponse::assertRedirectBack` by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/55780 -* Add support for sending raw (non-encoded) attachments in Resend mail by [@Roywcm](https://github.com/Roywcm) in https://github.com/laravel/framework/pull/55837 -* [12.x] chore: return Collection from timestamps methods by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55871 -* [12.x] fix: fully qualify collection return type by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55873 -* [12.x] Fix Blade nested default component resolution for custom namespaces by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55874 -* [12.x] Fix return types in console command handlers to void by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55876 -* [12.x] Ability to perform higher order static calls on collection items by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55880 -* Adds Resource helpers to cursor paginator by [@jsandfordhughescoop](https://github.com/jsandfordhughescoop) in https://github.com/laravel/framework/pull/55879 -* Add reorderDesc() to Query Builder by [@ghabriel25](https://github.com/ghabriel25) in https://github.com/laravel/framework/pull/55885 -* [11.x] Fixes Symfony Console 7.3 deprecations on closure command by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55888 -* [12.x] Add `AsUri` model cast by [@ash-jc-allen](https://github.com/ash-jc-allen) in https://github.com/laravel/framework/pull/55909 -* [12.x] feat: Add Contextual Implementation/Interface Binding via PHP8 Attribute by [@yitzwillroth](https://github.com/yitzwillroth) in https://github.com/laravel/framework/pull/55904 -* [12.x] Add tests for the `AuthenticateSession` Middleware by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55900 -* [12.x] Allow brick/math ^0.13 by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/54964 -* [12.x] fix: Factory::state and ::prependState generics by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55915 - -## [v12.16.0](https://github.com/laravel/framework/compare/v12.15.0...v12.16.0) - 2025-05-27 - -* [12.x] Change priority in optimize:clear by [@amirmohammadnajmi](https://github.com/amirmohammadnajmi) in https://github.com/laravel/framework/pull/55792 -* [12.x] Fix `TestResponse::assertSessionMissing()` when given an array of keys by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55800 -* [12.x] Allowing `Context` Attribute to Interact with Hidden by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55799 -* Add support for sending raw (non-encoded) attachments in Resend mail driver by [@Roywcm](https://github.com/Roywcm) in https://github.com/laravel/framework/pull/55803 -* [12.x] Added option to always defer for flexible cache by [@Zwartpet](https://github.com/Zwartpet) in https://github.com/laravel/framework/pull/55802 -* [12.x] style: Use null coalescing assignment (??=) for cleaner code by [@mohsenetm](https://github.com/mohsenetm) in https://github.com/laravel/framework/pull/55823 -* [12.x] Introducing `Arr::hasAll` by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55815 -* [12.x] Restore lazy loading check by [@decadence](https://github.com/decadence) in https://github.com/laravel/framework/pull/55817 -* [12.x] Minor language update by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55812 -* fix(cache/redis): use connectionAwareSerialize in RedisStore::putMany() by [@superbiche](https://github.com/superbiche) in https://github.com/laravel/framework/pull/55814 -* [12.x] Fix `ResponseFactory` should also accept `null` callback by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55833 -* [12.x] Add template variables to scope by [@wietsewarendorff](https://github.com/wietsewarendorff) in https://github.com/laravel/framework/pull/55830 -* [12.x] Introducing `toUri` to the `Stringable` Class by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55862 -* [12.x] Remove remaining [@return](https://github.com/return) tags from constructors by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55858 -* [12.x] Replace alias `is_integer()` with `is_int()` to comply with Laravel Pint by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55851 -* Fix argument types for Illuminate/Database/Query/Builder::upsert() by [@jellisii](https://github.com/jellisii) in https://github.com/laravel/framework/pull/55849 -* [12.x] Add `in_array_keys` validation rule to check for presence of specified array keys by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/55807 -* [12.x] Add `Rule::contains` by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/55809 - -## [v12.15.0](https://github.com/laravel/framework/compare/v12.14.1...v12.15.0) - 2025-05-20 - -* [12.x] Add locale-aware number parsing methods to Number class by [@informagenie](https://github.com/informagenie) in https://github.com/laravel/framework/pull/55725 -* [12.x] Add a default option when retrieving an enum from data by [@elbojoloco](https://github.com/elbojoloco) in https://github.com/laravel/framework/pull/55735 -* Revert "[12.x] Update "Number::fileSize" to use correct prefix and add prefix param" by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/55741 -* [12.x] Remove apc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55745 -* [12.x] Add param type for `assertJsonStructure` & `assertExactJsonStructure` methods by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/55743 -* [12.x] Fix type casting for environment variables in config files by [@adamwhp](https://github.com/adamwhp) in https://github.com/laravel/framework/pull/55737 -* [12.x] Preserve "previous" model state by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55729 -* [12.x] Passthru `getCountForPagination` on an Eloquent\Builder by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55752 -* [12.x] Add `assertClientError` method to `TestResponse` by [@shane-zeng](https://github.com/shane-zeng) in https://github.com/laravel/framework/pull/55750 -* Install Broadcasting Command Fix for Livewire Starter Kit by [@joshcirre](https://github.com/joshcirre) in https://github.com/laravel/framework/pull/55774 -* Clarify units for benchmark value for IDE accessibility by [@mike-healy](https://github.com/mike-healy) in https://github.com/laravel/framework/pull/55781 -* Improved PHPDoc Return Types for Eloquent's Original Attribute Methods by [@clementbirkle](https://github.com/clementbirkle) in https://github.com/laravel/framework/pull/55779 -* [12.x] Prevent `preventsLazyLoading` exception when using `automaticallyEagerLoadRelationships` by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55771 -* [12.x] Add `hash` string helper by [@istiak-tridip](https://github.com/istiak-tridip) in https://github.com/laravel/framework/pull/55767 -* [12.x] Update `assertSessionMissing()` signature to match `assertSessionHas()` by [@nexxai](https://github.com/nexxai) in https://github.com/laravel/framework/pull/55763 -* Fix: php artisan db command if no password by [@mr-chetan](https://github.com/mr-chetan) in https://github.com/laravel/framework/pull/55761 -* [12.x] Types: InteractsWithPivotTable::sync by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55762 -* [12.x] feat: Add `current_page_url` to Paginator by [@mariomka](https://github.com/mariomka) in https://github.com/laravel/framework/pull/55789 -* Correct return type in PhpDoc for command fail method by [@Muetze42](https://github.com/Muetze42) in https://github.com/laravel/framework/pull/55783 -* [12.x] Add `assertRedirectToAction` method to test redirection to controller actions by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55788 -* [12.x] Add Context contextual attribute by [@martinbean](https://github.com/martinbean) in https://github.com/laravel/framework/pull/55760 - -## [v12.14.1](https://github.com/laravel/framework/compare/v12.14.0...v12.14.1) - 2025-05-13 - -* [10.x] Refine error messages for detecting lost connections (Debian bookworm compatibility) by [@mfn](https://github.com/mfn) in https://github.com/laravel/framework/pull/53794 -* [10.x] Bump minimum `league/commonmark` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/53829 -* [10.x] Backport 11.x PHP 8.4 fix for str_getcsv deprecation by [@aka-tpayne](https://github.com/aka-tpayne) in https://github.com/laravel/framework/pull/54074 -* [10.x] Fix attribute name used on `Validator` instance within certain rule classes by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54943 -* Add `Illuminate\Support\EncodedHtmlString` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54737 -* [11.x] Fix missing `return $this` for `assertOnlyJsonValidationErrors` by [@LeTamanoir](https://github.com/LeTamanoir) in https://github.com/laravel/framework/pull/55099 -* [11.x] Fix `Illuminate\Support\EncodedHtmlString` from causing breaking change by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55149 -* [11.x] Respect custom path for cached views by the `AboutCommand` by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/55179 -* [11.x] Include all invisible characters in Str::trim by [@laserhybiz](https://github.com/laserhybiz) in https://github.com/laravel/framework/pull/54281 -* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55302 -* [11.x] Remove incorrect syntax from mail's `message` template by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55530 -* [11.x] Allows to toggle markdown email encoding by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55539 -* [11.x] Fix `EncodedHtmlString` to ignore instance of `HtmlString` by [@jbraband](https://github.com/jbraband) in https://github.com/laravel/framework/pull/55543 -* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55549 -* [11.x] Install Passport 13.x by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/55621 -* [11.x] Bump minimum league/commonmark by [@andrextor](https://github.com/andrextor) in https://github.com/laravel/framework/pull/55660 -* Backporting Timebox fixes to 11.x by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/55705 -* Test SQLServer 2017 on Ubuntu 22.04 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55716 -* [11.x] Fix Symfony 7.3 deprecations by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55711 -* Easily implement broadcasting in a React/Vue Typescript app (Starter Kits) by [@tnylea](https://github.com/tnylea) in https://github.com/laravel/framework/pull/55170 - -## [v12.14.0](https://github.com/laravel/framework/compare/v12.13.0...v12.14.0) - 2025-05-13 - -* [12.x] Support `useCurrent` on date and year column types by [@nicholasbrantley](https://github.com/nicholasbrantley) in https://github.com/laravel/framework/pull/55619 -* [12.x] Update "Number::fileSize" to use correct prefix and add prefix param by [@Boy132](https://github.com/Boy132) in https://github.com/laravel/framework/pull/55678 -* [12.x] Update PHPDoc for whereRaw to allow Expression as $sql by [@mitoop](https://github.com/mitoop) in https://github.com/laravel/framework/pull/55674 -* Revert "[12.x] Make Blueprint Resolver Statically" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55690 -* [12.x] Support Virtual Properties When Serializing Models by [@beschoenen](https://github.com/beschoenen) in https://github.com/laravel/framework/pull/55691 -* [12.X] Fix `Http::preventStrayRequests` error propagation when using `Http::pool` by [@LeTamanoir](https://github.com/LeTamanoir) in https://github.com/laravel/framework/pull/55689 -* [12.x] incorrect use of generics in Schema\Builder by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55687 -* [12.x] Add option to disable MySQL ssl when restoring or squashing migrations by [@andersonls](https://github.com/andersonls) in https://github.com/laravel/framework/pull/55683 -* [12.x] Add `except` and `exceptHidden` methods to `Context` class by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55692 -* [12.x] Container `currentlyResolving` utility by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/55684 -* [12.x] Container `currentlyResolving` test by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/55694 -* [12.x] Fix handling of default values for route parameters with a binding field by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/55697 -* Move Timebox for Authentication and add to password resets by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/55701 -* [12.x] perf: Optimize BladeCompiler by [@rzv-me](https://github.com/rzv-me) in https://github.com/laravel/framework/pull/55703 -* [12.x] perf: support iterables for event discovery paths by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55699 -* [12.x] Types: AuthorizesRequests::resourceAbilityMap by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55706 -* [12.x] Add flexible support to memoized cache store by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55709 -* [12.x] Introduce Arr::from() by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55715 -* [12.x] Fix the `getCurrentlyAttachedPivots` wrong `morphClass` for morph to many relationships by [@amir9480](https://github.com/amir9480) in https://github.com/laravel/framework/pull/55721 -* [12.x] Improve typehints for Http classes by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54783 -* Add deleteWhen for throttle exceptions job middleware by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/55718 - -## [v12.13.0](https://github.com/laravel/framework/compare/v12.12.0...v12.13.0) - 2025-05-07 - -* [12.x] fix no arguments return type in request class by [@olivernybroe](https://github.com/olivernybroe) in https://github.com/laravel/framework/pull/55631 -* [12.x] Add support for callback evaluation in containsOneItem method by [@fernandokbs](https://github.com/fernandokbs) in https://github.com/laravel/framework/pull/55622 -* [12.x] add generics to aggregate related methods and properties by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55628 -* [12.x] Fix typo in PHPDoc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55636 -* [12.x] Allow naming queued closures by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55634 -* [12.x] Add `assertRedirectBack` assertion method by [@ryangjchandler](https://github.com/ryangjchandler) in https://github.com/laravel/framework/pull/55635 -* [12.x] Typehints for bindings by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55633 -* [12.x] add PHP Doc types to arrays for methods in Database\Grammar by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55629 -* fix trim null arg deprecation by [@apreiml](https://github.com/apreiml) in https://github.com/laravel/framework/pull/55649 -* [12.x] Support predis/predis 3.x by [@gabrielrbarbosa](https://github.com/gabrielrbarbosa) in https://github.com/laravel/framework/pull/55641 -* Bump vite from 5.4.18 to 5.4.19 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55655 -* [12.x] Fix predis versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/55654 -* [12.x] Bump minimum league/commonmark by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55659 -* [12.x] Fix typo in MemoizedStoreTest by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55662 -* [12.x] Queue event listeners with enum values by [@wgriffioen](https://github.com/wgriffioen) in https://github.com/laravel/framework/pull/55656 -* [12.x] Implement releaseAfter method in RateLimited middleware by [@adamjgriffith](https://github.com/adamjgriffith) in https://github.com/laravel/framework/pull/55671 -* [12.x] Improve Cache Tests by [@nuernbergerA](https://github.com/nuernbergerA) in https://github.com/laravel/framework/pull/55670 -* [12.x] Only pass model IDs to Eloquent `whereAttachedTo` method by [@ashleyshenton](https://github.com/ashleyshenton) in https://github.com/laravel/framework/pull/55666 -* feat(bus): allow adding multiple jobs to chain by [@dallyger](https://github.com/dallyger) in https://github.com/laravel/framework/pull/55668 -* [12.x] add generics to QueryBuilder’s column related methods by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55663 - -## [v12.12.0](https://github.com/laravel/framework/compare/v12.11.1...v12.12.0) - 2025-05-01 - -* [12.x] Make Blueprint Resolver Statically by [@finagin](https://github.com/finagin) in https://github.com/laravel/framework/pull/55607 -* [12.x] Allow limiting number of assets to preload by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55618 -* [12.x] Set job instance on "failed" command instance by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55617 - -## [v12.11.1](https://github.com/laravel/framework/compare/v12.11.0...v12.11.1) - 2025-04-30 - -* Revert "[12.x]`ScheduledTaskFailed` not dispatched on scheduled task failing" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55612 -* [12.x] Resolve issue with BelongsToManyRelationship factory by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/55608 - -## [v12.11.0](https://github.com/laravel/framework/compare/v12.10.2...v12.11.0) - 2025-04-29 - -* Add payload creation and original delay info to job payload by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55529 -* Add config option to ignore view cache timestamps by [@pizkaz](https://github.com/pizkaz) in https://github.com/laravel/framework/pull/55536 -* [12.x] Dispatch NotificationFailed when sending fails by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55507 -* [12.x] Option to disable dispatchAfterResponse in a test by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55456 -* [12.x] Pass flags to custom Json::$encoder by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55548 -* [12.x] Use pendingAttributes of relationships when creating relationship models via model factories by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55558 -* [12.x] Fix double query in model relation serialization by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55547 -* [12.x] Improve circular relation check in Automatic Relation Loading by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55542 -* [12.x] Prevent relation autoload context from being serialized by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55582 -* Remove `@internal` Annotation from `$components` Property in `InteractsWithIO` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55580 -* Ensure fake job implements job contract by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55574 -* [12.x] Fix `AnyOf` constructor parameter type by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/55577 -* Sync changes to Illuminate components before release by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/55591 -* [12.x] Set class-string generics on `Enum` rule by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55588 -* [12.x] added detailed doc types to bindings related methods by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55576 -* [12.x] Improve [@use](https://github.com/use) directive to support function and const modifiers by [@rodolfosrg](https://github.com/rodolfosrg) in https://github.com/laravel/framework/pull/55583 -* 12.x scheduled task failed not dispatched on scheduled task failing by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55572 -* [12.x] Introduce Reflector methods for accessing class attributes by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55568 -* [12.x] Typed getters for Arr helper by [@tibbsa](https://github.com/tibbsa) in https://github.com/laravel/framework/pull/55567 - -## [v12.10.2](https://github.com/laravel/framework/compare/v12.10.1...v12.10.2) - 2025-04-24 - -* [12.x] Address Model@relationLoaded when relation is null by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55531 - -## [v12.10.1](https://github.com/laravel/framework/compare/v12.10.0...v12.10.1) - 2025-04-23 - -* Revert "Use value() helper in 'when' method to simplify code" #55465 by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55514 -* [12.x] Use xxh128 when comparing views for changes by [@shawnlindstrom](https://github.com/shawnlindstrom) in https://github.com/laravel/framework/pull/55517 -* [12.x] Ensure related models is iterable on `HasRelationships@relationLoaded()` by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55519 -* [12.x] Add Enum support for assertJsonPath in AssertableJsonString.php by [@azim-kordpour](https://github.com/azim-kordpour) in https://github.com/laravel/framework/pull/55516 - -## [v12.10.0](https://github.com/laravel/framework/compare/v12.9.2...v12.10.0) - 2025-04-22 - -* Use value() helper in 'when' method by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55465 -* [12.x] Test `@use` directive without quotes by [@osbre](https://github.com/osbre) in https://github.com/laravel/framework/pull/55462 -* [12.x] Enhance Broadcast Events Test Coverage by [@roshandelpoor](https://github.com/roshandelpoor) in https://github.com/laravel/framework/pull/55458 -* [12.x] Add `Conditionable` Trait to `Fluent` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55455 -* [12.x] Fix relation auto loading with manually set relations by [@patrickweh](https://github.com/patrickweh) in https://github.com/laravel/framework/pull/55452 -* Add missing types to RateLimiter by [@ClaudioEyzaguirre](https://github.com/ClaudioEyzaguirre) in https://github.com/laravel/framework/pull/55445 -* [12.x] Fix for global autoload relationships not working in certain cases by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55443 -* [12.x] Fix adding `setTags` method on new cache flush events by [@erikn69](https://github.com/erikn69) in https://github.com/laravel/framework/pull/55405 -* Fix: Unique lock not being released after transaction rollback in ShouldBeUnique jobs with afterCommit() by [@toshitsuna-otsuka](https://github.com/toshitsuna-otsuka) in https://github.com/laravel/framework/pull/55420 -* [12.x] Extends `AsCollection` to map items into objects or other values by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/55383 -* [12.x] Fix group imports in Blade `@use` directive by [@osbre](https://github.com/osbre) in https://github.com/laravel/framework/pull/55461 -* chore(tests): align test names with idiomatic naming style by [@kauffinger](https://github.com/kauffinger) in https://github.com/laravel/framework/pull/55496 -* Update compiled views only if they actually changed by [@pizkaz](https://github.com/pizkaz) in https://github.com/laravel/framework/pull/55450 -* Improve performance of Arr::dot method - 300x in some cases by [@cyppe](https://github.com/cyppe) in https://github.com/laravel/framework/pull/55495 -* [12.x] Add tests for `CacheBasedSessionHandler` by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55487 -* [12.x] Add tests for `FileSessionHandler` by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55484 -* [12.x] Add tests for `DatabaseSessionHandler` by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55485 -* [12.x] Fix many to many detach without IDs broken with custom pivot class by [@amir9480](https://github.com/amir9480) in https://github.com/laravel/framework/pull/55490 -* [12.x] Support nested relations on `relationLoaded` method by [@tmsperera](https://github.com/tmsperera) in https://github.com/laravel/framework/pull/55471 -* Bugfix for Cache::memo()->many() returning the wrong value with an integer key type by [@bmckay959](https://github.com/bmckay959) in https://github.com/laravel/framework/pull/55503 -* [12.x] Allow Container to build `Migrator` from class name by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55501 - -## [v12.9.2](https://github.com/laravel/framework/compare/v12.9.1...v12.9.2) - 2025-04-16 - -* [12.x] Fixed a bug in using `illuminate/console` in external apps by [@andrey-helldar](https://github.com/andrey-helldar) in https://github.com/laravel/framework/pull/55430 -* Disable SQLServer 2017 CI as `ubuntu-20.24` has been removed by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55425 - -## [v12.9.1](https://github.com/laravel/framework/compare/v12.9.0...v12.9.1) - 2025-04-16 - -* [12.x] Forward only passed arguments into Illuminate\Database\Eloquent\Collection::partition method by [@MarekVikartovsky](https://github.com/MarekVikartovsky) in https://github.com/laravel/framework/pull/55422 -* [12.x] Add test for complex context manipulation in Logger by [@roshandelpoor](https://github.com/roshandelpoor) in https://github.com/laravel/framework/pull/55423 -* [12.x] Remove unused var from `DumpCommand` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55431 -* [12.x] Fix the serve command sometimes fails to destructure the request pool array by [@tonysm](https://github.com/tonysm) in https://github.com/laravel/framework/pull/55427 -* [12.x] Changes to `package-lock.json` should trigger `npm run build` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55426 - -## [v12.9.0](https://github.com/laravel/framework/compare/v12.8.1...v12.9.0) - 2025-04-15 - -* Add types to ViewErrorBag by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/55329 -* Add types to MessageBag by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/55327 -* [12.x] add generics to commonly used methods in Schema/Builder by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55330 -* Return frozen time for easier testing by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/55323 -* Enhance DetectsLostConnections to Support AWS Aurora Credential Rotation Scenario by [@msaifmfz](https://github.com/msaifmfz) in https://github.com/laravel/framework/pull/55331 -* [12.x] Rename test method of failedRequest() by [@LKaemmerling](https://github.com/LKaemmerling) in https://github.com/laravel/framework/pull/55332 -* feat: Add a callback to be called on transaction failure by [@dshafik](https://github.com/dshafik) in https://github.com/laravel/framework/pull/55338 -* [12.x] Add withRelationshipAutoloading method to model by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55344 -* [12.x] Enable HTTP client retries when middleware throws an exception by [@27pchrisl](https://github.com/27pchrisl) in https://github.com/laravel/framework/pull/55343 -* [12.x] Fix Closure serialization error in automatic relation loading by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55345 -* Add test for Unique validation rule with WhereIn constraints by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55351 -* Add [@throws](https://github.com/throws) in doc-blocks by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55361 -* [12.x] Update `propagateRelationAutoloadCallbackToRelation` method doc-block by [@derian-all-win-software](https://github.com/derian-all-win-software) in https://github.com/laravel/framework/pull/55363 -* [12.x] - Redis - Establish connection first, before set the options by [@alexmontoanelli](https://github.com/alexmontoanelli) in https://github.com/laravel/framework/pull/55370 -* [12.x] Fix translation FileLoader overrides with a missing key by [@fabio-ivona](https://github.com/fabio-ivona) in https://github.com/laravel/framework/pull/55342 -* [12.x] Fix pivot model events not working when using the `withPivotValue` by [@amir9480](https://github.com/amir9480) in https://github.com/laravel/framework/pull/55280 -* [12.x] Introduce memoized cache driver by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55304 -* [12.x] Add test for Filesystem::lastModified() method by [@roshandelpoor](https://github.com/roshandelpoor) in https://github.com/laravel/framework/pull/55389 -* [12.x] Supports `pda/pheanstalk` 7 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55397 -* [12.x] Add comprehensive filesystem operation tests to FilesystemTest by [@roshandelpoor](https://github.com/roshandelpoor) in https://github.com/laravel/framework/pull/55399 -* Bump vite from 5.4.17 to 5.4.18 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55402 -* Add descriptive error messages to assertViewHas() by [@3Descape](https://github.com/3Descape) in https://github.com/laravel/framework/pull/55392 -* Use Generic Types Annotations for LazyCollection Methods by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55380 -* [12.x] Add test coverage for Process sequence with multiple env variables by [@roshandelpoor](https://github.com/roshandelpoor) in https://github.com/laravel/framework/pull/55406 -* [12.x] Fix cc/bcc/replyTo address merging in `MailMessage` by [@onlime](https://github.com/onlime) in https://github.com/laravel/framework/pull/55404 -* [12.x] Add a `make` function in the `Fluent` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55417 - -## [v12.8.1](https://github.com/laravel/framework/compare/v12.8.0...v12.8.1) - 2025-04-08 - -## [v12.8.0](https://github.com/laravel/framework/compare/v12.7.2...v12.8.0) - 2025-04-08 - -* [12.x] only check for soft deletes once when mass-pruning by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55274 -* [12.x] Add createMany mass-assignment variants to `HasOneOrMany` relation by [@onlime](https://github.com/onlime) in https://github.com/laravel/framework/pull/55262 -* cosmetic: include is_array() case in match construct of getArrayableItems by [@epic-64](https://github.com/epic-64) in https://github.com/laravel/framework/pull/55275 -* Add tests for InvokeSerializedClosureCommand by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/55281 -* [12.x] Temporarily prevents PHPUnit 12.1 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55297 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55306 -* Bump vite from 5.4.12 to 5.4.17 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55301 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55307 -* [12.x] add generics to array types for Schema Grammars by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55314 -* [12.x] fix missing nullable for Query/Grammar::compileInsertGetId by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55311 -* [12.x] Adds `fromJson()` to Collection by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/55310 -* [12.x] Fix `illuminate/database` usage as standalone package by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55309 -* Correct array key in InteractsWithInput by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/55287 -* [12.x] Fix support for adding custom observable events from traits by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55286 -* [12.x] Added Automatic Relation Loading (Eager Loading) Feature by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/53655 -* [12.x] Modify PHPDoc for Collection::chunkWhile functions to support preserving keys by [@jsvdvis](https://github.com/jsvdvis) in https://github.com/laravel/framework/pull/55324 -* [12.x] Introduce Rule::anyOf() for Validating Against Multiple Rule Sets by [@brianferri](https://github.com/brianferri) in https://github.com/laravel/framework/pull/55191 - -## [v12.7.2](https://github.com/laravel/framework/compare/v12.7.1...v12.7.2) - 2025-04-03 - -## [v12.7.1](https://github.com/laravel/framework/compare/v12.7.0...v12.7.1) - 2025-04-03 - -## [v12.7.0](https://github.com/laravel/framework/compare/v12.6.0...v12.7.0) - 2025-04-03 - -* [12.x] `AbstractPaginator` should implement `CanBeEscapedWhenCastToString` by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55256 -* [12.x] Add `whereAttachedTo()` Eloquent builder method by [@bakerkretzmar](https://github.com/bakerkretzmar) in https://github.com/laravel/framework/pull/55245 -* Make Illuminate\Support\Uri Macroable by [@riesjart](https://github.com/riesjart) in https://github.com/laravel/framework/pull/55260 -* [12.x] Add resource helper functions to Model/Collections by [@TimKunze96](https://github.com/TimKunze96) in https://github.com/laravel/framework/pull/55107 -* [12.x]: Use char(36) for uuid type on MariaDB < 10.7.0 by [@boedah](https://github.com/boedah) in https://github.com/laravel/framework/pull/55197 -* [12.x] Introducing `toArray` to `ComponentAttributeBag` class by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55258 - -## [v12.6.0](https://github.com/laravel/framework/compare/v12.5.0...v12.6.0) - 2025-04-02 - -* [12.x] Dont stop pruning if pruning one model fails by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55237 -* [12.x] Update Date Facade Docblocks by [@fdalcin](https://github.com/fdalcin) in https://github.com/laravel/framework/pull/55235 -* Make `db:seed` command prohibitable by [@spawnia](https://github.com/spawnia) in https://github.com/laravel/framework/pull/55238 -* [12.x] Introducing `Rules\Password::appliedRules` Method by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55206 -* [12.x] Allowing merging model attributes before insert via `Model::fillAndInsert()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55038 -* [12.x] Fix type hints for DateTimeZone and DateTimeInterface on DateFactory by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55243 -* [12.x] Fix DateFactory docblock type hints by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55244 -* List missing `migrate:rollback` in DB::prohibitDestructiveCommands PhpDoc by [@spawnia](https://github.com/spawnia) in https://github.com/laravel/framework/pull/55252 -* [12.x] Add `Http::requestException()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55241 -* New: Uri `pathSegments()` helper method by [@chester-sykes](https://github.com/chester-sykes) in https://github.com/laravel/framework/pull/55250 -* [12.x] Do not require returning a Builder instance from a local scope method by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55246 - -## [v12.5.0](https://github.com/laravel/framework/compare/v12.4.1...v12.5.0) - 2025-04-01 - -* Correct misspellings by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55218 -* [12.x] Add ability to flush state on Vite helper by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55228 -* [12.x] Support taggeable store flushed cache events by [@erikn69](https://github.com/erikn69) in https://github.com/laravel/framework/pull/55223 -* Revert "[12.x] Support taggeable store flushed cache events" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55232 -* [12.x] Allow configuration of retry period for RoundRobin and Failover mail transports by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/55222 -* [12.x] Add --json option to EventListCommand by [@hotsaucejake](https://github.com/hotsaucejake) in https://github.com/laravel/framework/pull/55207 - -## [v12.4.1](https://github.com/laravel/framework/compare/v12.4.0...v12.4.1) - 2025-03-30 - -* [12.x] Add `Expression` type to param `$value` of `QueryBuilder` `orHaving()` method by [@faissaloux](https://github.com/faissaloux) in https://github.com/laravel/framework/pull/55202 -* [12.x] Fix URL generation with optional parameters (regression in #54811) by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/55213 -* [12.x] Fix failing tests on windows OS by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55210 - -## [v12.4.0](https://github.com/laravel/framework/compare/v12.3.0...v12.4.0) - 2025-03-29 - -* [12.x] Reset PHP’s peak memory usage when resetting scope for queue worker by [@TimWolla](https://github.com/TimWolla) in https://github.com/laravel/framework/pull/55069 -* [12.x] Add `AsHtmlString` cast by [@ralphjsmit](https://github.com/ralphjsmit) in https://github.com/laravel/framework/pull/55071 -* [12.x] Add `Arr::sole()` method by [@ralphjsmit](https://github.com/ralphjsmit) in https://github.com/laravel/framework/pull/55070 -* Improve warning message in `ApiInstallCommand` by [@sajjadhossainshohag](https://github.com/sajjadhossainshohag) in https://github.com/laravel/framework/pull/55081 -* [12.x] use already determined `related` property by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55075 -* [12.x] use "class-string" where appropriate in relations by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55074 -* [12.x] `QueueFake::listenersPushed()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55063 -* [12.x] Added except() method to Model class for excluding attributes by [@vishal2931](https://github.com/vishal2931) in https://github.com/laravel/framework/pull/55072 -* [12.x] fix: add TPivotModel default and define pivot property in {Belongs,Morph}ToMany by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55086 -* [12.x] remove `@return` docblocks on constructors by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55076 -* [12.x] Add NamedScope attribute by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/54450 -* [12.x] Improve syntax highlighting for stub type files by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/55094 -* [12.x] Prefer `new Collection` over `Collection::make` by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55091 -* [12.x] Fix except() method to support casted values by [@vishal2931](https://github.com/vishal2931) in https://github.com/laravel/framework/pull/55124 -* [12.x] Add testcase for findSole method by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/55115 -* [12.x] Types: PasswordBroker::reset by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55109 -* [12.x] assertThrowsNothing by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55100 -* [12.x] Fix type nullability on PasswordBroker.events property by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/55097 -* [12.x] Fix return type annotation in decrementPendingJobs method by [@shane-zeng](https://github.com/shane-zeng) in https://github.com/laravel/framework/pull/55133 -* [12.x] Fix return type annotation in compile method by [@shane-zeng](https://github.com/shane-zeng) in https://github.com/laravel/framework/pull/55132 -* [12.x] feat: Add `whereNull` and `whereNotNull` to `Assertablejson` by [@faissaloux](https://github.com/faissaloux) in https://github.com/laravel/framework/pull/55131 -* [12.x] fix: use contextual bindings in class dependency resolution by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55090 -* Better return types for `Illuminate\Queue\Jobs\Job::getJobId()` and `Illuminate\Queue\Jobs\DatabaseJob::getJobId()` methods by [@petrknap](https://github.com/petrknap) in https://github.com/laravel/framework/pull/55138 -* Remove remaining [@return](https://github.com/return) tags from constructors by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55136 -* [12.x] Various URL generation bugfixes by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/54811 -* Add an optional `shouldRun` method to migrations. by [@danmatthews](https://github.com/danmatthews) in https://github.com/laravel/framework/pull/55011 -* [12.x] `Uri` prevent empty query string by [@rojtjo](https://github.com/rojtjo) in https://github.com/laravel/framework/pull/55146 -* [12.x] Only call the ob_flush function if there is active buffer in eventStream by [@tonysm](https://github.com/tonysm) in https://github.com/laravel/framework/pull/55141 -* [12.x] Add CacheFlushed Event by [@tech-wolf-tw](https://github.com/tech-wolf-tw) in https://github.com/laravel/framework/pull/55142 -* [12.x] Update DateFactory method annotations for Carbon v3 compatibility by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/55151 -* [12.x] Improve docblocks for file related methods of InteractsWithInput by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/55156 -* [12.x] Enhance `FileViewFinder` doc-blocks by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55183 -* Support using null-safe operator with `null` value by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55175 -* [12.x] Fix: Make Paginated Queries Consistent Across Pages by [@tomchkk](https://github.com/tomchkk) in https://github.com/laravel/framework/pull/55176 -* [12.x] Add `pipe` method query builders by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55171 -* [12.x] fix: one of many subquery constraints by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55168 -* [12.x] fix(postgres): missing parentheses in whereDate/whereTime for json columns by [@saibotk](https://github.com/saibotk) in https://github.com/laravel/framework/pull/55159 -* Fix factory creation through attributes by [@davidstoker](https://github.com/davidstoker) in https://github.com/laravel/framework/pull/55190 -* [12.x] Fix Concurrency::run to preserve callback result order by [@chaker2710](https://github.com/chaker2710) in https://github.com/laravel/framework/pull/55161 -* [12.x] Log: Add optional keys parameter to `Log::withoutContext` to remove selected context from future logs by [@mattroylloyd](https://github.com/mattroylloyd) in https://github.com/laravel/framework/pull/55181 -* [12.x] Add `Expression` type to param `$value` of `QueryBuilder` `having()` method by [@faissaloux](https://github.com/faissaloux) in https://github.com/laravel/framework/pull/55200 -* [12.x] Add flag to disable where clauses for `withAttributes` method on Eloquent Builder by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55199 - -## [v12.3.0](https://github.com/laravel/framework/compare/v12.2.0...v12.3.0) - 2025-03-18 - -* [12.x] fixes https://github.com/laravel/octane/issues/1010 by [@mihaileu](https://github.com/mihaileu) in https://github.com/laravel/framework/pull/55008 -* Added the missing 'trashed' event to getObservablesEvents() by [@duemti](https://github.com/duemti) in https://github.com/laravel/framework/pull/55004 -* [12.x] Enhance PHPDoc for Manager classes with `@param-closure-this` by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/55002 -* [12.x] Fix `PendingRequest` typehints for `post`, `patch`, `put`, `delete` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54998 -* [12.x] Add test for untested methods in LazyCollection by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54996 -* [12.x] fix indentation by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54995 -* [12.x] apply final Pint fixes by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55014 -* Enhance validation tests: Add test for connection name detection in Unique rule by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54993 -* [12.x] Add json:unicode cast to support JSON_UNESCAPED_UNICODE encoding by [@fuwasegu](https://github.com/fuwasegu) in https://github.com/laravel/framework/pull/54992 -* [12.x] Add “Storage Linked” to the `about` command by [@adampatterson](https://github.com/adampatterson) in https://github.com/laravel/framework/pull/54949 -* [12.x] Add support for native JSON/JSONB column types in SQLite Schema builder by [@fuwasegu](https://github.com/fuwasegu) in https://github.com/laravel/framework/pull/54991 -* [12.x] Fix `LogManager::configurationFor()` typehint by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55016 -* [12.x] Add missing tests for LazyCollection methods by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/55022 -* [12.x] Refactor: Structural improvement for clarity by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55018 -* Improve `toKilobytes` to handle spaces and case-insensitive units by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/55019 -* [12.x] Fix mistake in `asJson` call in `HasAttributes.php` that was recently introduced by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55017 -* [12.x] reapply Pint style changes by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55015 -* Add validation test for forEach with null and empty array values by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/55047 -* [12.x] Types: EnumeratesValues Sum by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55044 -* [12.x] Ensure Consistent Formatting in Generated Invokable Classes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55034 -* Add element type to return array in Filesystem by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/55031 -* [12.x] Add support for PostgreSQL "unique nulls not distinct" by [@thierry2015](https://github.com/thierry2015) in https://github.com/laravel/framework/pull/55025 -* [12.x] standardize multiline ternaries by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55056 -* [12.x] improved readability for `aliasedPivotColumns` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55055 -* [12.x] remove progress bar from PHPStan output by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55054 -* [12.x] Fixes how the fluent Date rule builder handles `date_format` by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55052 -* Adding SSL encryption and support for MySQL connection by [@mdiktushar](https://github.com/mdiktushar) in https://github.com/laravel/framework/pull/55048 -* Revert "Adding SSL encryption and support for MySQL connection" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55057 -* Ensure queue property is nullable by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55058 -* [12.x] return `$this` for chaining by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55060 -* [12.x] prefer `new Collection` over `collect()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55059 -* [12.x] use "class-string" type for `using` pivot model by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55053 -* [12.x] multiline chaining on Collections by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/55061 - -## [v12.2.0](https://github.com/laravel/framework/compare/v12.1.1...v12.2.0) - 2025-03-12 - -* Add dates to allowed PHPDoc types of Builder::having() by [@miken32](https://github.com/miken32) in https://github.com/laravel/framework/pull/54899 -* [11.x] Fix double negative in `whereNotMorphedTo()` query by [@owenvoke](https://github.com/owenvoke) in https://github.com/laravel/framework/pull/54902 -* Add test for Arr::partition by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/54913 -* [11.x] Expose process checkTimeout method by [@mattmcdev](https://github.com/mattmcdev) in https://github.com/laravel/framework/pull/54912 -* [12.x] Compilable for Validation Contract by [@peterfox](https://github.com/peterfox) in https://github.com/laravel/framework/pull/54882 -* [11.x] Backport "Change `paginate()` method return types to `\Illuminate\Pagination\LengthAwarePaginator`" by [@carestad](https://github.com/carestad) in https://github.com/laravel/framework/pull/54917 -* [11.x] Revert faulty change to `EnumeratesValues::ensure()` doc block by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/54919 -* Ensure ValidationEmailRuleTest skips tests requiring the intl extension when unavailable by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54918 -* ✅ Ensure Enum validation is case-sensitive by adding a new test case. by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54922 -* [12.x] Feature: Collection chunk without preserving keys by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/54916 -* [12.x] Add test coverage for Uri::withQueryIfMissing method by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54923 -* Fix issue with using RedisCluster with compression or serialization by [@rzv-me](https://github.com/rzv-me) in https://github.com/laravel/framework/pull/54934 -* [12.x] Add test coverage for Str::replaceMatches method by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54930 -* [12.x] Types: Collection chunk without preserving keys by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/54924 -* [12.x] Add `ddBody` method to TestResponse for dumping various response payloads by [@Sammyjo20](https://github.com/Sammyjo20) in https://github.com/laravel/framework/pull/54933 -* [11.x] Backport "Fix issue with using `RedisCluster` with compression or serialization" by [@rzv-me](https://github.com/rzv-me) in https://github.com/laravel/framework/pull/54935 -* [12.x] feat: add `CanBeOneOfMany` support to `HasOneThrough` by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/54759 -* [12.x] Hotfix - Add function_exists check to ddBody in TestResponse by [@Sammyjo20](https://github.com/Sammyjo20) in https://github.com/laravel/framework/pull/54937 -* [12.x] Refactor: Remove unnecessary variables in Str class methods by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54963 -* Add Tests for Str::pluralPascal Method by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54957 -* [12.x] Fix visibility of setUp and tearDown in tests by [@naopusyu](https://github.com/naopusyu) in https://github.com/laravel/framework/pull/54950 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54944 -* Fix missing return in `assertOnlyInvalid` by [@parth391](https://github.com/parth391) in https://github.com/laravel/framework/pull/54941 -* Handle case when migrate:install command is called and table exists by [@joe-tito](https://github.com/joe-tito) in https://github.com/laravel/framework/pull/54938 -* [11.x] Fix callOnce in Seeder so it handles arrays properly by [@lbovit](https://github.com/lbovit) in https://github.com/laravel/framework/pull/54985 -* Change "exceptoin" spelling mistake to "exception" by [@hvlucas](https://github.com/hvlucas) in https://github.com/laravel/framework/pull/54979 -* [12.x] Add test for after method in LazyCollection by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54978 -* [12.x] Add `increment` and `decrement` methods to `Context` by [@mattmcdev](https://github.com/mattmcdev) in https://github.com/laravel/framework/pull/54976 -* Ensure ExcludeIf correctly rejects a null value as an invalid condition by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54973 -* [12.x] apply Pint rule "no_spaces_around_offset" by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54970 -* [12.x] apply Pint rule "single_line_comment_style" by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54969 -* [12.x] do not use mix of newline and inline formatting by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54967 -* [12.x] use single indent for multiline ternaries by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/54971 - -## [v12.1.1](https://github.com/laravel/framework/compare/v12.1.0...v12.1.1) - 2025-03-05 - -* [11.x] Add valid values to ensure method by [@lancepioch](https://github.com/lancepioch) in https://github.com/laravel/framework/pull/54840 -* Fix attribute name used on `Validator` instance within certain rule classes by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54845 -* [11.x] Fix `Application::interBasePath()` fails to resolve application when project name is "vendor" by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54871 -* [11.x] Test improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54879 -* [12.x] DocBlock: Changed typehint for `Arr::partition` method by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/54896 -* Enhance Email and Image Dimensions Validation Tests by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54897 -* [12.x] Apply default styling rules to the notification stub by [@ahinkle](https://github.com/ahinkle) in https://github.com/laravel/framework/pull/54895 - -## [v12.1.0](https://github.com/laravel/framework/compare/v12.0.1...v12.1.0) - 2025-03-04 - -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54782 -* [12.x] Fix incorrect typehints in `BuildsWhereDateClauses` traits by [@mohprilaksono](https://github.com/mohprilaksono) in https://github.com/laravel/framework/pull/54784 -* [12.x] Improve queries readablility by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/54791 -* [12.x] Enhance eventStream to Support Custom Events and Start Messages by [@devhammed](https://github.com/devhammed) in https://github.com/laravel/framework/pull/54776 -* [12.x] Make the PendingCommand class tappable. by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/54801 -* [12.x] Add missing union type in event stream docblock by [@devhammed](https://github.com/devhammed) in https://github.com/laravel/framework/pull/54800 -* Change return types of `paginage()` methods to `\Illuminate\Pagination\LengthAwarePaginator` by [@carestad](https://github.com/carestad) in https://github.com/laravel/framework/pull/54826 -* [12.x] Check if internal `Hasher::verifyConfiguration()` method exists on driver before forwarding call by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/54833 -* [11.x] Fix using `AsStringable` cast on Notifiable's key by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54818 -* Add Tests for Handling Null Primary Keys and Special Values in Unique Validation Rule by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54823 -* Improve docblock for with() method to clarify it adds to existing eag… by [@igorlealantunes](https://github.com/igorlealantunes) in https://github.com/laravel/framework/pull/54838 -* [12.x] Fix dropping schema-qualified prefixed tables by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/54834 -* [12.x] Add `Context::scope()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54799 -* Allow Http requests to be recorded without requests being faked by [@kemp](https://github.com/kemp) in https://github.com/laravel/framework/pull/54850 -* [12.x] Adds a new method "getRawSql" (with embedded bindings) to the QueryException class by [@erickcomp](https://github.com/erickcomp) in https://github.com/laravel/framework/pull/54849 -* Update Inspiring.php by [@ju-gow](https://github.com/ju-gow) in https://github.com/laravel/framework/pull/54846 -* [12.x] Correct use of named argument in `Date` facade and fix a return type. by [@lmottasin](https://github.com/lmottasin) in https://github.com/laravel/framework/pull/54847 -* Add additional tests for Rule::array validation scenarios by [@alikhosravidev](https://github.com/alikhosravidev) in https://github.com/laravel/framework/pull/54844 -* [12.x] Remove return statement by [@mohprilaksono](https://github.com/mohprilaksono) in https://github.com/laravel/framework/pull/54842 -* Fix typos by [@co63oc](https://github.com/co63oc) in https://github.com/laravel/framework/pull/54839 -* [12.x] Do not loop through middleware when excluded is empty by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54837 -* Add test for Arr::reject method in Illuminate Support by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/laravel/framework/pull/54863 -* [12.x] Feature: Array partition by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/54859 -* [12.x] Introduce `ContextLogProcessor` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54851 - -## [v12.0.1](https://github.com/laravel/framework/compare/v12.0.0...v12.0.1) - 2025-02-24 - -## [v12.0.0](https://github.com/laravel/framework/compare/v11.44.0..v12.0.0...v12.0.0) - 2025-02-24 - -* [12.x] Prep Laravel v12 by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50406 -* [12.x] Make `Str::is()` match multiline strings by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/51196 -* [12.x] Use native MariaDB CLI commands by [@staudenmeir](https://github.com/staudenmeir) in https://github.com/laravel/framework/pull/51505 -* [12.x] Adds missing streamJson() to ResponseFactory contract by [@wilsenhc](https://github.com/wilsenhc) in https://github.com/laravel/framework/pull/51544 -* [12.x] Preserve numeric keys on the first level of the validator rules by [@Tofandel](https://github.com/Tofandel) in https://github.com/laravel/framework/pull/51516 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/52248 -* [12.x] mergeIfMissing allows merging with nested arrays by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/52242 -* [12.x] Fix chunked queries not honoring user-defined limits and offsets by [@tonysm](https://github.com/tonysm) in https://github.com/laravel/framework/pull/52093 -* [12.x] Replace md5 with much faster xxhash by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/52301 -* [12.x] Switch models to UUID v7 by [@staudenmeir](https://github.com/staudenmeir) in https://github.com/laravel/framework/pull/52433 -* [12.x] Improved algorithm for Number::pairs() by [@hotmeteor](https://github.com/hotmeteor) in https://github.com/laravel/framework/pull/52641 -* Removed Duplicated Prefix on DynamoDbStore.php by [@felipehertzer](https://github.com/felipehertzer) in https://github.com/laravel/framework/pull/52986 -* [12.x] feat: configure default datetime precision on per-grammar basis by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/51821 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/53150 -* [12.x] Fix laravel/prompt dependency version constraint for illuminate/console by [@wouterj](https://github.com/wouterj) in https://github.com/laravel/framework/pull/53146 -* [12.x] Add generic return type to Container::instance() by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/53161 -* Map output of concurrecy calls to the index of the input by [@ovp87](https://github.com/ovp87) in https://github.com/laravel/framework/pull/53135 -* Change Composer hasPackage to public by [@buihanh2304](https://github.com/buihanh2304) in https://github.com/laravel/framework/pull/53282 -* [12.x] force `Eloquent\Collection::partition` to return a base `Collection` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53304 -* [12.x] Better support for multi-dbs in the `RefreshDatabase` trait by [@tonysm](https://github.com/tonysm) in https://github.com/laravel/framework/pull/53231 -* [12.x] Validate UUID's version optionally by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/53341 -* [12.x] Validate UUID version 2 and max by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/53368 -* [12.x] Add step parameter to LazyCollection range method by [@Ashot1995](https://github.com/Ashot1995) in https://github.com/laravel/framework/pull/53473 -* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/53524 -* [12.x] Avoid breaking change `RefreshDatabase::usingInMemoryDatabase()` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/53587 -* [12.x] fix: container resolution order when resolving class dependencies by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/53522 -* [12.x] Change the default for scheduled command `emailOutput()` to only send email if output exists by [@onlime](https://github.com/onlime) in https://github.com/laravel/framework/pull/53774 -* [12.x] Add `hasMorePages()` to `CursorPaginator` contract by [@KennedyTedesco](https://github.com/KennedyTedesco) in https://github.com/laravel/framework/pull/53762 -* [12.x] modernize `DatabaseTokenRepository` and make consistent with `CacheTokenRepository` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53746 -* [12.x] chore: remove support for Carbon v2 by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/53825 -* [12.x] use promoted properties for Auth events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53847 -* [12.x] use promoted properties for Database events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53848 -* [12.x] use promoted properties for Console events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53851 -* [12.x] use promoted properties for Mail events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53852 -* [12.x] use promoted properties for Notification events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53853 -* [12.x] use promoted properties for Routing events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53854 -* [12.x] use promoted properties for Queue events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53855 -* [12.x] Restore database token repository property documentation by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/53908 -* [12.x] Use reject() instead of a negated filter() by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/53925 -* [12.x] Use first-class callable syntax to improve static analysis by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/53924 -* [12.x] add type declarations for Console Events by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53947 -* [12.x] use type declaration on property by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53970 -* [12.x] Update Symfony and PHPUnit dependencies by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54019 -* [12.x] Allow `when()` helper to accept Closure condition parameter by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/54005 -* [12.x] Add test for collapse in collections by [@amirmohammadnajmi](https://github.com/amirmohammadnajmi) in https://github.com/laravel/framework/pull/54032 -* [12.x] Add test for benchmark utilities by [@amirmohammadnajmi](https://github.com/amirmohammadnajmi) in https://github.com/laravel/framework/pull/54055 -* [12.x] Fix once() cache when used in extended static class by [@FrittenKeeZ](https://github.com/FrittenKeeZ) in https://github.com/laravel/framework/pull/54094 -* [12.x] Ignore querystring parameters using closure when validating signed url by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/54104 -* Make `dropForeignIdFor` method complementary to `foreignIdFor` by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/54102 -* Allow scoped disks to be scoped from other scoped disks by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/54124 -* [12.x] Add test for Util::getParameterClassName() by [@amirmohammadnajmi](https://github.com/amirmohammadnajmi) in https://github.com/laravel/framework/pull/54209 -* Improve eloquent attach parameter consistency by [@fabpl](https://github.com/fabpl) in https://github.com/laravel/framework/pull/54225 -* [12.x] Enhance multi-database support by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/54274 -* [12.x] Fix Session's `getCookieExpirationDate` incompatibility with Carbon 3 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54313 -* [12.x] Update minimum PHPUnit versions by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54323 -* [12.x] Prevent XSS vulnerabilities by excluding SVGs by default in image validation by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/54331 -* [12.x] Convert interfaces from docblock to method by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54348 -* [12.x] Validate paths for UTF-8 characters by [@Jubeki](https://github.com/Jubeki) in https://github.com/laravel/framework/pull/54370 -* [12.x] Fix aggregate alias when using expression by [@iamgergo](https://github.com/iamgergo) in https://github.com/laravel/framework/pull/54418 -* Added flash method to Session interface to fix IDE issues by [@eldair](https://github.com/eldair) in https://github.com/laravel/framework/pull/54421 -* Adding the withQueryString method to the paginator interface. by [@dvlpr91](https://github.com/dvlpr91) in https://github.com/laravel/framework/pull/54462 -* [12.x] feat: --memory=0 should mean skip memory exceeded verification (Breaking Change) by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/54393 -* Auto-discover nested policies following conventional, parallel hierarchy by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/54493 -* [12.x] Reintroduce PHPUnit 10.5 supports by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54490 -* [12.x] Allow limiting bcrypt hashing to 72 bytes to prevent insecure hashes. by [@waxim](https://github.com/waxim) in https://github.com/laravel/framework/pull/54509 -* [12.x] Fix accessing `Connection` property in `Grammar` classes by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/54487 -* [12.x] Configure connection on SQLite connector by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/54588 -* [12.x] Introduce Job@resolveQueuedJobClass() by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54613 -* [12.x] Bind abstract from concrete's return type by [@peterfox](https://github.com/peterfox) in https://github.com/laravel/framework/pull/54628 -* [12.x] Query builder PDO fetch modes by [@bert-w](https://github.com/bert-w) in https://github.com/laravel/framework/pull/54443 -* [12.x] Fix Illuminate components `composer.json` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54700 -* [12.x] Bump minimum `brick/math` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54694 -* [11.x] Fix parsing `PHP_CLI_SERVER_WORKERS` as `string` instead of `int` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54724 -* [11.x] Rename Redis parse connection for cluster test method to follow naming conventions by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/54721 -* [11.x] Allow `readAt` method to use in database channel by [@utsavsomaiya](https://github.com/utsavsomaiya) in https://github.com/laravel/framework/pull/54729 -* [11.x] Fix: Custom Exceptions with Multiple Arguments does not properly rein… by [@pandiselvamm](https://github.com/pandiselvamm) in https://github.com/laravel/framework/pull/54705 -* [11.x] Update ConcurrencyTest exception reference to use namespace by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/54732 -* [11.x] Deprecate `Factory::$modelNameResolver` by [@samlev](https://github.com/samlev) in https://github.com/laravel/framework/pull/54736 -* Update `config/app.php` to reflect laravel/laravel change for compatibility by [@askdkc](https://github.com/askdkc) in https://github.com/laravel/framework/pull/54752 -* [11x.] Improved typehints for `InteractsWithDatabase` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54748 -* [11.x] Improved typehints for `InteractsWithExceptionHandling` && `ExceptionHandlerFake` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54747 -* Add Env::extend to support custom adapters when loading environment variables by [@andrii-androshchuk](https://github.com/andrii-androshchuk) in https://github.com/laravel/framework/pull/54756 -* [12.x] Sync `filesystem.disk.local` configurations by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54764 From b9bc1c834b6a665656ddf42e71a07104903f8ed6 Mon Sep 17 00:00:00 2001 From: Richard van Baarsen Date: Tue, 5 May 2026 15:00:12 +0200 Subject: [PATCH 298/596] Add enum support to QueueFake assertPushedOn method (#59990) --- src/Illuminate/Support/Testing/Fakes/QueueFake.php | 8 ++++++-- tests/Support/SupportTestingQueueFakeTest.php | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index 41e39a41b377..d1602f5fa9f8 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -17,6 +17,8 @@ use Illuminate\Support\Traits\ReflectsClosures; use PHPUnit\Framework\Assert as PHPUnit; +use function Illuminate\Support\enum_value; + /** * @phpstan-type RawPushType array{"payload": string, "queue": string|null, "options": array} */ @@ -149,7 +151,7 @@ public function assertPushedTimes($job, $times = 1) /** * Assert if a job was pushed based on a truth-test callback. * - * @param string $queue + * @param \UnitEnum|string $queue * @param string|\Closure $job * @param callable|null $callback * @return void @@ -160,8 +162,10 @@ public function assertPushedOn($queue, $job, $callback = null) [$job, $callback] = [$this->firstClosureParameterType($job), $job]; } + $queue = enum_value($queue); + $this->assertPushed($job, function ($job, $pushedQueue) use ($callback, $queue) { - if ($pushedQueue !== $queue) { + if (enum_value($pushedQueue) !== $queue) { return false; } diff --git a/tests/Support/SupportTestingQueueFakeTest.php b/tests/Support/SupportTestingQueueFakeTest.php index 82d48c38ae97..3cea9a85ebb3 100644 --- a/tests/Support/SupportTestingQueueFakeTest.php +++ b/tests/Support/SupportTestingQueueFakeTest.php @@ -130,12 +130,14 @@ public function testAssertPushedOn() try { $this->fake->assertPushedOn('bar', JobStub::class); + $this->fake->assertPushedOn(QueueNameEnumStub::Bar, JobStub::class); $this->fail(); } catch (ExpectationFailedException $e) { $this->assertStringContainsString('The expected [Illuminate\Tests\Support\JobStub] job was not pushed.', $e->getMessage()); } $this->fake->assertPushedOn('foo', JobStub::class); + $this->fake->assertPushedOn(QueueNameEnumStub::Foo, JobStub::class); } public function testAssertPushedOnWithClosure() @@ -534,6 +536,12 @@ public function testPushedRaw() } } +enum QueueNameEnumStub: string +{ + case Foo = 'foo'; + case Bar = 'bar'; +} + class JobStub { public function handle() From 6e9190ccd6099fe2761fbdec1885729e6bc9ec5f Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 5 May 2026 13:00:47 +0000 Subject: [PATCH 299/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index e420c087bba6..46579a96bec6 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -55,7 +55,7 @@ * @method static \Illuminate\Support\Testing\Fakes\QueueFake except(array|string $jobsToBeQueued) * @method static void assertPushed(string|\Closure $job, callable|int|null $callback = null) * @method static void assertPushedTimes(string $job, int $times = 1) - * @method static void assertPushedOn(string $queue, string|\Closure $job, callable|null $callback = null) + * @method static void assertPushedOn(\UnitEnum|string $queue, string|\Closure $job, callable|null $callback = null) * @method static void assertPushedWithChain(string $job, array $expectedChain = [], callable|null $callback = null) * @method static void assertPushedWithoutChain(string $job, callable|null $callback = null) * @method static void assertClosurePushed(callable|int|null $callback = null) From c12fad4fae83b88913ead68709c6ccbf904f464f Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 5 May 2026 15:31:10 +0200 Subject: [PATCH 300/596] [13.x] Improvements to collection sort docblocks (#59988) * Consistently use int-mask identifier for sort $options argument * Update sort docblocks to allow SortDirection and tighten string to asc/desc only * Fixup static tests for sort operations --- src/Illuminate/Collections/Collection.php | 18 +++++++++--------- src/Illuminate/Collections/Enumerable.php | 14 +++++++------- types/Support/Collection.php | 6 ++++-- types/Support/LazyCollection.php | 6 ++++-- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 2067cc34b55d..18de341e4bf4 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -1561,7 +1561,7 @@ public function sort($callback = null) /** * Sort items in descending order. * - * @param int $options + * @param int-mask-of $options * @return static */ public function sortDesc($options = SORT_REGULAR) @@ -1576,8 +1576,8 @@ public function sortDesc($options = SORT_REGULAR) /** * Sort the collection using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string|int $callback - * @param int $options + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options * @param SortDirection|bool $descending * @return static */ @@ -1616,8 +1616,8 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) /** * Sort the collection using multiple comparisons. * - * @param array $comparisons - * @param int $options + * @param array $comparisons + * @param int-mask-of $options * @return static */ protected function sortByMany(array $comparisons = [], int $options = SORT_REGULAR) @@ -1676,8 +1676,8 @@ protected function sortByMany(array $comparisons = [], int $options = SORT_REGUL /** * Sort the collection in descending order using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string|int $callback - * @param int $options + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options * @return static */ public function sortByDesc($callback, $options = SORT_REGULAR) @@ -1698,7 +1698,7 @@ public function sortByDesc($callback, $options = SORT_REGULAR) /** * Sort the collection keys. * - * @param int $options + * @param int-mask-of $options * @param SortDirection|bool $descending * @return static */ @@ -1717,7 +1717,7 @@ public function sortKeys($options = SORT_REGULAR, $descending = false) /** * Sort the collection keys in descending order. * - * @param int $options + * @param int-mask-of $options * @return static */ public function sortKeysDesc($options = SORT_REGULAR) diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 731cefc8d03a..1f193b13baca 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -1069,7 +1069,7 @@ public function sort($callback = null); /** * Sort items in descending order. * - * @param int $options + * @param int-mask-of $options * @return static */ public function sortDesc($options = SORT_REGULAR); @@ -1077,8 +1077,8 @@ public function sortDesc($options = SORT_REGULAR); /** * Sort the collection using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string|int $callback - * @param int $options + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options * @param bool $descending * @return static */ @@ -1087,8 +1087,8 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false); /** * Sort the collection in descending order using the given callback. * - * @param array|(callable(TValue, TKey): mixed)|string|int $callback - * @param int $options + * @param array|(callable(TValue, TKey): mixed)|string|int $callback + * @param int-mask-of $options * @return static */ public function sortByDesc($callback, $options = SORT_REGULAR); @@ -1096,7 +1096,7 @@ public function sortByDesc($callback, $options = SORT_REGULAR); /** * Sort the collection keys. * - * @param int $options + * @param int-mask-of $options * @param bool $descending * @return static */ @@ -1105,7 +1105,7 @@ public function sortKeys($options = SORT_REGULAR, $descending = false); /** * Sort the collection keys in descending order. * - * @param int $options + * @param int-mask-of $options * @return static */ public function sortKeysDesc($options = SORT_REGULAR); diff --git a/types/Support/Collection.php b/types/Support/Collection.php index fe7a48365e27..76effbf0de4a 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -851,7 +851,8 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection->sortBy('string')); assertType('Illuminate\Support\Collection', $collection->sortBy('string', 1, false)); assertType('Illuminate\Support\Collection', $collection->sortBy([ - ['string', 'string'], + ['string', 'asc'], + ['foo', SortDirection::Descending], ])); assertType('Illuminate\Support\Collection', $collection->sortBy([function ($user, $int) { // assertType('User', $user); @@ -869,7 +870,8 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection->sortByDesc('string')); assertType('Illuminate\Support\Collection', $collection->sortByDesc('string', 1)); assertType('Illuminate\Support\Collection', $collection->sortByDesc([ - ['string', 'string'], + ['string', 'asc'], + ['foo', SortDirection::Descending], ])); assertType('Illuminate\Support\Collection', $collection->sortByDesc([function ($user, $int) { // assertType('User', $user); diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index 7f4b94f72331..1ba973b22427 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -711,7 +711,8 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection', $collection->sortBy('string')); assertType('Illuminate\Support\LazyCollection', $collection->sortBy('string', 1, false)); assertType('Illuminate\Support\LazyCollection', $collection->sortBy([ - ['string', 'string'], + ['string', 'asc'], + ['foo', SortDirection::Descending], ])); assertType('Illuminate\Support\LazyCollection', $collection->sortBy([function ($user, $int) { // assertType('User', $user); @@ -729,7 +730,8 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection', $collection->sortByDesc('string')); assertType('Illuminate\Support\LazyCollection', $collection->sortByDesc('string', 1)); assertType('Illuminate\Support\LazyCollection', $collection->sortByDesc([ - ['string', 'string'], + ['string', 'asc'], + ['foo', SortDirection::Descending], ])); assertType('Illuminate\Support\LazyCollection', $collection->sortByDesc([function ($user, $int) { // assertType('User', $user); From 2de120293227e5001fced3eb865faffa8916f063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Fleury?= <23384755+noefleury@users.noreply.github.com> Date: Tue, 5 May 2026 22:47:39 +0200 Subject: [PATCH 301/596] [12.x] Fix infinite recursion when middleware group referencing itself (#60002) * [12.x] Fix infinite recursion when middleware group referencing itself * Fix style ci * Update MiddlewareNameResolver.php * Update MiddlewareNameResolver.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Routing/MiddlewareNameResolver.php | 6 ++++++ tests/Routing/RoutingRouteTest.php | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Illuminate/Routing/MiddlewareNameResolver.php b/src/Illuminate/Routing/MiddlewareNameResolver.php index 87ab42d17ed7..b60cd52e3939 100644 --- a/src/Illuminate/Routing/MiddlewareNameResolver.php +++ b/src/Illuminate/Routing/MiddlewareNameResolver.php @@ -3,6 +3,8 @@ namespace Illuminate\Routing; use Closure; +use LogicException; +use Throwable; class MiddlewareNameResolver { @@ -49,6 +51,8 @@ public static function resolve($name, $map, $middlewareGroups) * @param array $map * @param array $middlewareGroups * @return array + * + * @throws Throwable */ protected static function parseMiddlewareGroup($name, $map, $middlewareGroups) { @@ -59,6 +63,8 @@ protected static function parseMiddlewareGroup($name, $map, $middlewareGroups) // merge its middleware into the results. This allows groups to conveniently // reference other groups without needing to repeat all their middlewares. if (isset($middlewareGroups[$middleware])) { + throw_if($name === $middleware, fn () => new LogicException("[$name] middleware group is referencing itself.")); + $results = array_merge($results, static::parseMiddlewareGroup( $middleware, $map, $middlewareGroups )); diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index ad240fe15d30..42ac2206561d 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -386,6 +386,21 @@ public function testMiddlewareGroupsCanReferenceOtherGroups() unset($_SERVER['__middleware.group']); } + public function testMiddlewareGroupsCannotReferenceItself() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('[web] middleware group is referencing itself.'); + + $router = $this->getRouter(); + $router->get('foo/bar', ['middleware' => 'web', function () { + return 'hello'; + }]); + + $router->middlewareGroup('web', ['web']); + + $router->dispatch(Request::create('foo/bar', 'GET')); + } + public function testFluentRouteNamingWithinAGroup() { $router = $this->getRouter(); From 4e6833a26c8d6ccbdca909bcaf927c064e7b4c80 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 5 May 2026 21:54:37 +0100 Subject: [PATCH 302/596] [13.x] Add all* queue inspection methods (#59997) * 13.x-all*-inspection-methods doc blocks again use between sort docblocks 13.x-allX methods queue fake test empty ones fake database Revert "test" This reverts commit 0c61bb4e2fb30734e6240f15275bb7702836187d. oops Update RedisQueue.php why adjust no need test * Update QueueFake.php --- src/Illuminate/Queue/BeanstalkdQueue.php | 30 +++++++ src/Illuminate/Queue/DatabaseQueue.php | 41 +++++++++ src/Illuminate/Queue/FailoverQueue.php | 30 +++++++ src/Illuminate/Queue/NullQueue.php | 30 +++++++ src/Illuminate/Queue/RedisQueue.php | 49 +++++++++++ src/Illuminate/Queue/SqsQueue.php | 30 +++++++ src/Illuminate/Queue/SyncQueue.php | 30 +++++++ .../Support/Testing/Fakes/QueueFake.php | 39 +++++++++ tests/Integration/Queue/RedisQueueTest.php | 59 +++++++++++++ tests/Queue/QueueDatabaseQueueUnitTest.php | 87 +++++++++++++++++++ tests/Support/SupportTestingQueueFakeTest.php | 13 +++ 11 files changed, 438 insertions(+) diff --git a/src/Illuminate/Queue/BeanstalkdQueue.php b/src/Illuminate/Queue/BeanstalkdQueue.php index be6562858ff9..10fa091548f0 100755 --- a/src/Illuminate/Queue/BeanstalkdQueue.php +++ b/src/Illuminate/Queue/BeanstalkdQueue.php @@ -145,6 +145,36 @@ public function reservedJobs($queue = null): Collection return new Collection; } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return new Collection; + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return new Collection; + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index efd2a5fa3427..ce56a58de0eb 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -179,6 +179,47 @@ public function reservedJobs($queue = null): Collection ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return $this->database->table($this->table) + ->whereNull('reserved_at') + ->where('available_at', '<=', $this->currentTime()) + ->get() + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return $this->database->table($this->table) + ->whereNull('reserved_at') + ->where('available_at', '>', $this->currentTime()) + ->get() + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return $this->database->table($this->table) + ->whereNotNull('reserved_at') + ->get() + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/FailoverQueue.php b/src/Illuminate/Queue/FailoverQueue.php index c8fe73cd232c..68c6f5ff413e 100644 --- a/src/Illuminate/Queue/FailoverQueue.php +++ b/src/Illuminate/Queue/FailoverQueue.php @@ -105,6 +105,36 @@ public function reservedJobs($queue = null): Collection return $this->manager->connection($this->connections[0])->reservedJobs($queue); } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return $this->manager->connection($this->connections[0])->allPendingJobs(); + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return $this->manager->connection($this->connections[0])->allDelayedJobs(); + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return $this->manager->connection($this->connections[0])->allReservedJobs(); + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/NullQueue.php b/src/Illuminate/Queue/NullQueue.php index 3f5830de8d13..a0d2b207ca4e 100644 --- a/src/Illuminate/Queue/NullQueue.php +++ b/src/Illuminate/Queue/NullQueue.php @@ -84,6 +84,36 @@ public function reservedJobs($queue = null): Collection return new Collection; } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return new Collection; + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return new Collection; + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index 3d7423030fb6..bb5777560c07 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -194,6 +194,55 @@ public function reservedJobs($queue = null): Collection ->map(fn ($payload) => InspectedJob::fromPayload($payload)); } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return $this->allQueueNames() + ->flatMap(fn ($name) => $this->getConnection()->lrange($this->getQueueRedisKey($name), 0, -1)) + ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return $this->allQueueNames() + ->flatMap(fn ($name) => $this->getConnection()->zrange($this->getQueueRedisKey($name).':delayed', 0, -1)) + ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return $this->allQueueNames() + ->flatMap(fn ($name) => $this->getConnection()->zrange($this->getQueueRedisKey($name).':reserved', 0, -1)) + ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + } + + /** + * Get the unique queue names. + * + * @return \Illuminate\Support\Collection + */ + protected function allQueueNames(): Collection + { + return (new Collection($this->getConnection()->keys('queues:*'))) + ->map(fn ($key) => Str::between($key, 'queues:', ':')) + ->unique() + ->values(); + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index 3601cf8d3702..7c7619c4b055 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -167,6 +167,36 @@ public function reservedJobs($queue = null): Collection return new Collection; } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return new Collection; + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return new Collection; + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Queue/SyncQueue.php b/src/Illuminate/Queue/SyncQueue.php index b2fff207eb15..20090238dff7 100755 --- a/src/Illuminate/Queue/SyncQueue.php +++ b/src/Illuminate/Queue/SyncQueue.php @@ -105,6 +105,36 @@ public function reservedJobs($queue = null): Collection return new Collection; } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return new Collection; + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return new Collection; + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index d1602f5fa9f8..644caf267c1b 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -504,6 +504,45 @@ public function reservedJobs($queue = null): Collection return new Collection; } + /** + * Get all pending jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allPendingJobs(): Collection + { + return (new Collection($this->jobs)) + ->flatten(1) + ->map(fn ($data) => new InspectedJob( + uuid: null, + name: is_object($data['job']) + ? (method_exists($data['job'], 'displayName') ? $data['job']->displayName() : get_class($data['job'])) + : $data['job'], + attempts: 0, + createdAt: null, + )); + } + + /** + * Get all delayed jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allDelayedJobs(): Collection + { + return new Collection; + } + + /** + * Get all reserved jobs across every queue. + * + * @return \Illuminate\Support\Collection + */ + public function allReservedJobs(): Collection + { + return new Collection; + } + /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * diff --git a/tests/Integration/Queue/RedisQueueTest.php b/tests/Integration/Queue/RedisQueueTest.php index ca6ed36028f0..d3e61d080337 100644 --- a/tests/Integration/Queue/RedisQueueTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -668,6 +668,65 @@ public function testReservedJobs($driver) $this->assertNotNull($reserved->first()->uuid); $this->assertInstanceOf(Carbon::class, $reserved->first()->createdAt); } + + #[DataProvider('redisDriverProvider')] + public function testAllPendingJobs($driver) + { + $default = config('queue.connections.redis.queue', 'default'); + $this->setQueue($driver, $default); + + $this->queue->push(new RedisQueueIntegrationTestJob(1)); + $this->queue->pushOn('emails', new RedisQueueIntegrationTestJob(2)); + + $pending = $this->queue->allPendingJobs(); + + $this->assertCount(2, $pending); + $this->assertInstanceOf(InspectedJob::class, $pending->first()); + $this->assertSame(RedisQueueIntegrationTestJob::class, $pending->first()->name); + $this->assertSame(0, $pending->first()->attempts); + $this->assertNotNull($pending->first()->uuid); + $this->assertInstanceOf(Carbon::class, $pending->first()->createdAt); + } + + #[DataProvider('redisDriverProvider')] + public function testAllDelayedJobs($driver) + { + $default = config('queue.connections.redis.queue', 'default'); + $this->setQueue($driver, $default); + + $this->queue->later(60, new RedisQueueIntegrationTestJob(1)); + $this->queue->laterOn('emails', 60, new RedisQueueIntegrationTestJob(2)); + + $delayed = $this->queue->allDelayedJobs(); + + $this->assertCount(2, $delayed); + $this->assertInstanceOf(InspectedJob::class, $delayed->first()); + $this->assertSame(RedisQueueIntegrationTestJob::class, $delayed->first()->name); + $this->assertSame(0, $delayed->first()->attempts); + $this->assertNotNull($delayed->first()->uuid); + $this->assertInstanceOf(Carbon::class, $delayed->first()->createdAt); + } + + #[DataProvider('redisDriverProvider')] + public function testAllReservedJobs($driver) + { + $default = config('queue.connections.redis.queue', 'default'); + $this->setQueue($driver, $default); + + $this->queue->push(new RedisQueueIntegrationTestJob(1)); + $this->queue->pushOn('emails', new RedisQueueIntegrationTestJob(2)); + $this->queue->pop(); + $this->queue->pop('emails'); + + $reserved = $this->queue->allReservedJobs(); + + $this->assertCount(2, $reserved); + $this->assertInstanceOf(InspectedJob::class, $reserved->first()); + $this->assertSame(RedisQueueIntegrationTestJob::class, $reserved->first()->name); + $this->assertSame(1, $reserved->first()->attempts); + $this->assertNotNull($reserved->first()->uuid); + $this->assertInstanceOf(Carbon::class, $reserved->first()->createdAt); + } } class RedisQueueIntegrationTestJob diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 326594e6186a..9e235c47d63d 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -269,6 +269,93 @@ public function testReservedJobs() $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); } + public function testAllPendingJobs() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer(m::spy(Container::class)); + + $payload1 = json_encode(['uuid' => 'uuid-1', 'displayName' => 'JobA', 'job' => 'foo', 'data' => [], 'createdAt' => 1000000]); + $payload2 = json_encode(['uuid' => 'uuid-2', 'displayName' => 'JobB', 'job' => 'foo', 'data' => [], 'createdAt' => 1000001]); + + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('whereNull')->with('reserved_at')->andReturnSelf(); + $query->shouldReceive('where')->with('available_at', '<=', m::any())->andReturnSelf(); + $query->shouldReceive('get')->andReturn(collect([ + (object) ['id' => 1, 'queue' => 'default', 'payload' => $payload1, 'attempts' => 0, 'reserved_at' => null], + (object) ['id' => 2, 'queue' => 'emails', 'payload' => $payload2, 'attempts' => 0, 'reserved_at' => null], + ])); + + $jobs = $queue->allPendingJobs(); + + $this->assertCount(2, $jobs); + $this->assertInstanceOf(InspectedJob::class, $jobs->first()); + $this->assertSame('JobA', $jobs->first()->name); + $this->assertSame('uuid-1', $jobs->first()->uuid); + $this->assertSame(0, $jobs->first()->attempts); + $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); + $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); + $this->assertSame('JobB', $jobs->last()->name); + $this->assertSame('uuid-2', $jobs->last()->uuid); + } + + public function testAllDelayedJobs() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer(m::spy(Container::class)); + + $payload1 = json_encode(['uuid' => 'uuid-1', 'displayName' => 'JobA', 'job' => 'foo', 'data' => [], 'createdAt' => 1000000]); + $payload2 = json_encode(['uuid' => 'uuid-2', 'displayName' => 'JobB', 'job' => 'foo', 'data' => [], 'createdAt' => 1000001]); + + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('whereNull')->with('reserved_at')->andReturnSelf(); + $query->shouldReceive('where')->with('available_at', '>', m::any())->andReturnSelf(); + $query->shouldReceive('get')->andReturn(collect([ + (object) ['id' => 1, 'queue' => 'default', 'payload' => $payload1, 'attempts' => 0, 'reserved_at' => null], + (object) ['id' => 2, 'queue' => 'emails', 'payload' => $payload2, 'attempts' => 0, 'reserved_at' => null], + ])); + + $jobs = $queue->allDelayedJobs(); + + $this->assertCount(2, $jobs); + $this->assertInstanceOf(InspectedJob::class, $jobs->first()); + $this->assertSame('JobA', $jobs->first()->name); + $this->assertSame('uuid-1', $jobs->first()->uuid); + $this->assertSame(0, $jobs->first()->attempts); + $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); + $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); + $this->assertSame('JobB', $jobs->last()->name); + $this->assertSame('uuid-2', $jobs->last()->uuid); + } + + public function testAllReservedJobs() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer(m::spy(Container::class)); + + $payload1 = json_encode(['uuid' => 'uuid-1', 'displayName' => 'JobA', 'job' => 'foo', 'data' => [], 'createdAt' => 1000000]); + $payload2 = json_encode(['uuid' => 'uuid-2', 'displayName' => 'JobB', 'job' => 'foo', 'data' => [], 'createdAt' => 1000001]); + + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('whereNotNull')->with('reserved_at')->andReturnSelf(); + $query->shouldReceive('get')->andReturn(collect([ + (object) ['id' => 1, 'queue' => 'default', 'payload' => $payload1, 'attempts' => 1, 'reserved_at' => 1000005], + (object) ['id' => 2, 'queue' => 'emails', 'payload' => $payload2, 'attempts' => 2, 'reserved_at' => 1000006], + ])); + + $jobs = $queue->allReservedJobs(); + + $this->assertCount(2, $jobs); + $this->assertInstanceOf(InspectedJob::class, $jobs->first()); + $this->assertSame('JobA', $jobs->first()->name); + $this->assertSame('uuid-1', $jobs->first()->uuid); + $this->assertSame(1, $jobs->first()->attempts); + $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); + $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); + $this->assertSame('JobB', $jobs->last()->name); + $this->assertSame('uuid-2', $jobs->last()->uuid); + $this->assertSame(2, $jobs->last()->attempts); + } + public function testGetLockForPoppingIsCached() { $database = m::mock(Connection::class); diff --git a/tests/Support/SupportTestingQueueFakeTest.php b/tests/Support/SupportTestingQueueFakeTest.php index 3cea9a85ebb3..88b9093f2a38 100644 --- a/tests/Support/SupportTestingQueueFakeTest.php +++ b/tests/Support/SupportTestingQueueFakeTest.php @@ -497,6 +497,19 @@ public function testPendingJobs() $this->assertSame(0, $pending->first()->attempts); } + public function testAllPendingJobs() + { + $this->fake->push($this->job, '', 'foo'); + $this->fake->push(new JobToFakeStub, '', 'bar'); + + $pending = $this->fake->allPendingJobs(); + + $this->assertCount(2, $pending); + $this->assertInstanceOf(InspectedJob::class, $pending->first()); + $this->assertTrue($pending->contains(fn ($job) => $job->name === JobStub::class)); + $this->assertTrue($pending->contains(fn ($job) => $job->name === JobToFakeStub::class)); + } + public function testGetRawPushes() { $this->fake->pushRaw('some-payload', null, ['options' => 'yeah']); From 22444668793569267f2b3366fe5322d4a0accf7c Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 5 May 2026 20:55:12 +0000 Subject: [PATCH 303/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 46579a96bec6..18bab2aa9468 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -70,6 +70,9 @@ * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) * @method static \Illuminate\Support\Collection delayedJobs(string|null $queue = null) * @method static \Illuminate\Support\Collection reservedJobs(string|null $queue = null) + * @method static \Illuminate\Support\Collection allPendingJobs() + * @method static \Illuminate\Support\Collection allDelayedJobs() + * @method static \Illuminate\Support\Collection allReservedJobs() * @method static bool shouldFakeJob(object $job) * @method static array pushedJobs() * @method static array rawPushes() From 3e8e48b7e417af9f342f07483e04da1187a48aa6 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 5 May 2026 22:55:51 +0200 Subject: [PATCH 304/596] [13.x] Add support for `SortDirection` enum to query builder classes (#59865) * Update all Symfony polyfills to use same version * Add PHP 8.6 polyfill to Database * Update query builder chunking methods to support SortDirection enum * Add support for SortDirection to query Builder order methods Also refactor Builder code to leverage this --- composer.json | 4 +-- src/Illuminate/Collections/composer.json | 4 +-- src/Illuminate/Container/composer.json | 4 +-- .../Database/Concerns/BuildsQueries.php | 27 +++++++------- src/Illuminate/Database/Eloquent/Builder.php | 3 +- .../Eloquent/Relations/BelongsToMany.php | 11 +++--- src/Illuminate/Database/Query/Builder.php | 35 +++++++++++-------- src/Illuminate/Database/composer.json | 5 +-- src/Illuminate/Http/composer.json | 2 +- src/Illuminate/Routing/composer.json | 4 +-- src/Illuminate/Support/composer.json | 2 +- .../DatabaseEloquentMorphToManyTest.php | 3 +- 12 files changed, 56 insertions(+), 48 deletions(-) diff --git a/composer.json b/composer.json index 8e42f938879c..71812caf8e53 100644 --- a/composer.json +++ b/composer.json @@ -55,8 +55,8 @@ "symfony/http-kernel": "^7.4.0 || ^8.0.0", "symfony/mailer": "^7.4.0 || ^8.0.0", "symfony/mime": "^7.4.0 || ^8.0.0", - "symfony/polyfill-php84": "^1.34", - "symfony/polyfill-php85": "^1.34", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", "symfony/polyfill-php86": "^1.36", "symfony/process": "^7.4.5 || ^8.0.5", "symfony/routing": "^7.4.0 || ^8.0.0", diff --git a/src/Illuminate/Collections/composer.json b/src/Illuminate/Collections/composer.json index 1f5571a6f3a9..70fb46b46c31 100644 --- a/src/Illuminate/Collections/composer.json +++ b/src/Illuminate/Collections/composer.json @@ -18,8 +18,8 @@ "illuminate/conditionable": "^13.0", "illuminate/contracts": "^13.0", "illuminate/macroable": "^13.0", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", "symfony/polyfill-php86": "^1.36" }, "suggest": { diff --git a/src/Illuminate/Container/composer.json b/src/Illuminate/Container/composer.json index eb6c93bcf431..7f8e1ff14830 100755 --- a/src/Illuminate/Container/composer.json +++ b/src/Illuminate/Container/composer.json @@ -18,8 +18,8 @@ "illuminate/contracts": "^13.0", "illuminate/reflection": "^13.0", "psr/container": "^1.1.1 || ^2.0.1", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33" + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36" }, "provide": { "psr/container-implementation": "1.1 || 2.0" diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index 537594e0858c..541fb9b6e644 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -18,6 +18,7 @@ use Illuminate\Support\Traits\Conditionable; use InvalidArgumentException; use RuntimeException; +use SortDirection; /** * @template TValue @@ -144,7 +145,7 @@ public function chunkById($count, callable $callback, $column = null, $alias = n */ public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null) { - return $this->orderedChunkById($count, $callback, $column, $alias, descending: true); + return $this->orderedChunkById($count, $callback, $column, $alias, descending: SortDirection::Descending); } /** @@ -154,7 +155,7 @@ public function chunkByIdDesc($count, callable $callback, $column = null, $alias * @param callable(\Illuminate\Support\Collection, int): mixed $callback * @param string|null $column * @param string|null $alias - * @param bool $descending + * @param SortDirection|bool $descending * @return bool * * @throws \RuntimeException @@ -185,11 +186,10 @@ public function orderedChunkById($count, callable $callback, $column = null, $al // We'll execute the query for the given page and get the results. If there are // no results we can just break and return from here. When there are results // we will call the callback with the current chunk of these results here. - if ($descending) { - $results = $clone->forPageBeforeId($limit, $lastId, $column)->get(); - } else { - $results = $clone->forPageAfterId($limit, $lastId, $column)->get(); - } + $results = match ($descending) { + SortDirection::Ascending, false => $clone->forPageAfterId($limit, $lastId, $column)->get(), + SortDirection::Descending, true => $clone->forPageBeforeId($limit, $lastId, $column)->get(), + }; $countResults = $results->count(); @@ -302,7 +302,7 @@ public function lazyById($chunkSize = 1000, $column = null, $alias = null) */ public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) { - return $this->orderedLazyById($chunkSize, $column, $alias, true); + return $this->orderedLazyById($chunkSize, $column, $alias, SortDirection::Descending); } /** @@ -311,7 +311,7 @@ public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) * @param int $chunkSize * @param string|null $column * @param string|null $alias - * @param bool $descending + * @param SortDirection|bool $descending * @return \Illuminate\Support\LazyCollection * * @throws \InvalidArgumentException @@ -333,11 +333,10 @@ protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = n while (true) { $clone = clone $this; - if ($descending) { - $results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get(); - } else { - $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get(); - } + $results = match ($descending) { + SortDirection::Ascending, false => $clone->forPageAfterId($chunkSize, $lastId, $column)->get(), + SortDirection::Descending, true => $clone->forPageBeforeId($chunkSize, $lastId, $column)->get(), + }; foreach ($results as $result) { yield $result; diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 4de139b1cebf..8950c81b3816 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -22,6 +22,7 @@ use Illuminate\Support\Traits\ForwardsCalls; use ReflectionClass; use ReflectionMethod; +use SortDirection; /** * @template TModel of \Illuminate\Database\Eloquent\Model @@ -1071,7 +1072,7 @@ public function cursor() protected function enforceOrderBy() { if (empty($this->query->orders) && empty($this->query->unionOrders)) { - $this->orderBy($this->model->getQualifiedKeyName(), 'asc'); + $this->orderBy($this->model->getQualifiedKeyName(), SortDirection::Ascending); } } diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 3b2dfe91cb5f..cd36196894ad 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -17,6 +17,7 @@ use Illuminate\Support\Collection as BaseCollection; use Illuminate\Support\Str; use InvalidArgumentException; +use SortDirection; /** * @template TRelatedModel of \Illuminate\Database\Eloquent\Model @@ -588,10 +589,10 @@ public function orWherePivotNotNull($column) * Add an "order by" clause for a pivot table column. * * @param string|\Illuminate\Contracts\Database\Query\Expression $column - * @param string $direction + * @param SortDirection|'asc'|'desc' $direction * @return $this */ - public function orderByPivot($column, $direction = 'asc') + public function orderByPivot($column, $direction = SortDirection::Ascending) { return $this->orderBy($this->qualifyPivotColumn($column), $direction); } @@ -604,7 +605,7 @@ public function orderByPivot($column, $direction = 'asc') */ public function orderByPivotDesc($column) { - return $this->orderBy($this->qualifyPivotColumn($column), 'desc'); + return $this->orderBy($this->qualifyPivotColumn($column), SortDirection::Descending); } /** @@ -1076,7 +1077,7 @@ public function chunkById($count, callable $callback, $column = null, $alias = n */ public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null) { - return $this->orderedChunkById($count, $callback, $column, $alias, descending: true); + return $this->orderedChunkById($count, $callback, $column, $alias, descending: SortDirection::Descending); } /** @@ -1106,7 +1107,7 @@ public function eachById(callable $callback, $count = 1000, $column = null, $ali * @param callable $callback * @param string|null $column * @param string|null $alias - * @param bool $descending + * @param SortDirection|bool $descending * @return bool */ public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false) diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 129c63b3e9b8..e8d79787d2cd 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -29,6 +29,7 @@ use InvalidArgumentException; use LogicException; use RuntimeException; +use SortDirection; use UnitEnum; use function Illuminate\Support\enum_value; @@ -2962,12 +2963,12 @@ public function orHavingRaw($sql, array $bindings = []) * Add an "order by" clause to the query. * * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $direction + * @param SortDirection|'asc'|'desc' $direction * @return $this * * @throws \InvalidArgumentException */ - public function orderBy($column, $direction = 'asc') + public function orderBy($column, $direction = SortDirection::Ascending) { if ($this->isQueryable($column)) { [$query, $bindings] = $this->createSub($column); @@ -2977,11 +2978,15 @@ public function orderBy($column, $direction = 'asc') $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order'); } - $direction = strtolower($direction); - - if (! in_array($direction, ['asc', 'desc'], true)) { - throw new InvalidArgumentException('Order direction must be "asc" or "desc".'); - } + $direction = match (true) { + $direction instanceof SortDirection => match ($direction) { + SortDirection::Ascending => 'asc', + SortDirection::Descending => 'desc', + }, + strtolower($direction) === 'asc' => 'asc', + strtolower($direction) === 'desc' => 'desc', + default => throw new InvalidArgumentException('Order direction must be a SortDirection, "asc" or "desc".'), + }; $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ 'column' => $column, @@ -2999,7 +3004,7 @@ public function orderBy($column, $direction = 'asc') */ public function orderByDesc($column) { - return $this->orderBy($column, 'desc'); + return $this->orderBy($column, SortDirection::Descending); } /** @@ -3010,7 +3015,7 @@ public function orderByDesc($column) */ public function latest($column = 'created_at') { - return $this->orderBy($column, 'desc'); + return $this->orderBy($column, SortDirection::Descending); } /** @@ -3021,7 +3026,7 @@ public function latest($column = 'created_at') */ public function oldest($column = 'created_at') { - return $this->orderBy($column, 'asc'); + return $this->orderBy($column, SortDirection::Ascending); } /** @@ -3216,7 +3221,7 @@ public function forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id') $this->where($column, '<', $lastId); } - return $this->orderBy($column, 'desc') + return $this->orderBy($column, SortDirection::Descending) ->limit($perPage); } @@ -3238,7 +3243,7 @@ public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id') $this->where($column, '>', $lastId); } - return $this->orderBy($column, 'asc') + return $this->orderBy($column, SortDirection::Ascending) ->limit($perPage); } @@ -3246,10 +3251,10 @@ public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id') * Remove all existing orders and optionally add a new order. * * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string|null $column - * @param string $direction + * @param SortDirection|'asc'|'desc' $direction * @return $this */ - public function reorder($column = null, $direction = 'asc') + public function reorder($column = null, $direction = SortDirection::Ascending) { $this->orders = null; $this->unionOrders = null; @@ -3271,7 +3276,7 @@ public function reorder($column = null, $direction = 'asc') */ public function reorderDesc($column) { - return $this->reorder($column, 'desc'); + return $this->reorder($column, SortDirection::Descending); } /** diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index 33e270c5662e..fe43eed94da9 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -29,8 +29,9 @@ "illuminate/macroable": "^13.0", "illuminate/support": "^13.0", "laravel/serializable-closure": "^2.0.10", - "symfony/polyfill-php84": "^1.34", - "symfony/polyfill-php85": "^1.34" + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36" }, "suggest": { "ext-filter": "Required to use the Postgres database driver.", diff --git a/src/Illuminate/Http/composer.json b/src/Illuminate/Http/composer.json index d1d498d20450..727c1c995653 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -26,7 +26,7 @@ "symfony/http-foundation": "^7.4.0 || ^8.0.0", "symfony/http-kernel": "^7.4.0 || ^8.0.0", "symfony/mime": "^7.4.0 || ^8.0.0", - "symfony/polyfill-php85": "^1.33" + "symfony/polyfill-php85": "^1.36" }, "suggest": { "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." diff --git a/src/Illuminate/Routing/composer.json b/src/Illuminate/Routing/composer.json index 248a15de1ead..91cec8700061 100644 --- a/src/Illuminate/Routing/composer.json +++ b/src/Illuminate/Routing/composer.json @@ -27,8 +27,8 @@ "illuminate/support": "^13.0", "symfony/http-foundation": "^7.4.0 || ^8.0.0", "symfony/http-kernel": "^7.4.0 || ^8.0.0", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", "symfony/routing": "^7.4.0 || ^8.0.0" }, "suggest": { diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index eb8bd0c66b6e..07a3ddae292d 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -25,7 +25,7 @@ "illuminate/macroable": "^13.0", "illuminate/reflection": "^13.0", "nesbot/carbon": "^3.8.4", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php85": "^1.36", "voku/portable-ascii": "^2.0.2" }, "replace": { diff --git a/tests/Database/DatabaseEloquentMorphToManyTest.php b/tests/Database/DatabaseEloquentMorphToManyTest.php index b5f747133d87..e9993fae50ac 100644 --- a/tests/Database/DatabaseEloquentMorphToManyTest.php +++ b/tests/Database/DatabaseEloquentMorphToManyTest.php @@ -9,6 +9,7 @@ use Illuminate\Database\Query\Grammars\Grammar; use Mockery\Adapter\Phpunit\MockeryTestCase as TestCase; use Mockery as m; +use SortDirection; use stdClass; class DatabaseEloquentMorphToManyTest extends TestCase @@ -90,7 +91,7 @@ public function testQueryExpressionCanBePassedToDifferentPivotQueryBuilderClause $builder->shouldReceive('whereNull')->with($column, 'and', false)->once()->andReturnSelf(); $relation->wherePivotNull($column); - $builder->shouldReceive('orderBy')->with($column, 'asc')->once()->andReturnSelf(); + $builder->shouldReceive('orderBy')->with($column, SortDirection::Ascending)->once()->andReturnSelf(); $relation->orderByPivot($column); } From e7db333a025a1e93ebca7744953069d7719f4bcf Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 5 May 2026 21:01:14 +0000 Subject: [PATCH 305/596] Update version to v13.8.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 67e5ea63befd..71ac4c3a4a28 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 = '13.7.0'; + const VERSION = '13.8.0'; /** * The base path for the Laravel installation. From beaea633daaa410912a8d6795245bd6d4ca298e2 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 5 May 2026 21:03:20 +0000 Subject: [PATCH 306/596] Update CHANGELOG --- CHANGELOG.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b2d25424db..c9febb915454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.7.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.8.0...13.x) + +## [v13.8.0](https://github.com/laravel/framework/compare/v13.7.0...v13.8.0) - 2026-05-05 + +* [12.x] `schedule:list` display expression in the correct timezone by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/59307 +* [12.x] Fix validation wildcard array message type error by [@sadique-cws](https://github.com/sadique-cws) in https://github.com/laravel/framework/pull/59339 +* Preserve class type of mocked classes by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59353 +* Preserve types on partialMock() and spy() by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59384 +* Fix missing UnitEnum support in ModelNotFoundException by [@jtheuerkauf](https://github.com/jtheuerkauf) in https://github.com/laravel/framework/pull/59423 +* [12.x] Fix macros with static closures by [@FeBe95](https://github.com/FeBe95) in https://github.com/laravel/framework/pull/59449 +* Correct Storage::fake() return type by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59469 +* [12.x] Fix callable type for freezeTime, freezeSecond, and travelTo by [@nbayramberdiyev](https://github.com/nbayramberdiyev) in https://github.com/laravel/framework/pull/59466 +* [12.x] Support string abstract in mock/partialMock/spy PHPDoc by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/59477 +* Document thrown exceptions in FilesystemAdapter by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59534 +* Hint \Redis `@mixin` on Connection by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/59532 +* [12.x] Use PDO subclass polyfill by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59640 +* [12.x] Fix infinite rate limiter TTL on custom increments by [@paulandroshchuk](https://github.com/paulandroshchuk) in https://github.com/laravel/framework/pull/59693 +* [12.x] Support named credential providers for SQS queue connections by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59754 +* [12.x] Prevent array to string conversion in signature validation by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/59778 +* [12.x] Memoize credentials in SqsConnector by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59867 +* [12.x] Disable pausing on managed queue workers by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59871 +* [DRAFT] Verify merging `12.x` branch by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/59929 +* [13.x] Exclude expired locks in DatabaseLock::isLock by [@JurianArie](https://github.com/JurianArie) in https://github.com/laravel/framework/pull/59948 +* [13.x] Merge attribute-provided middleware with existing middleware by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59944 +* [13.x] Tighten getCurrentSchemaListing [@return](https://github.com/return) in MySQL and SQLite builders by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59942 +* [13.x] Correct Repository::setStore [@return](https://github.com/return) to $this by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59940 +* [13.x] Correct Limit::none() [@return](https://github.com/return) type to Unlimited by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59938 +* [13.x] Add collation to processColumns and getColumns return shape by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59937 +* [13.x] Mark processViews schema field nullable in [@return](https://github.com/return) shape by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59941 +* Improve docblock wording in AurthorizationException by [@Talha-74](https://github.com/Talha-74) in https://github.com/laravel/framework/pull/59930 +* [13.x] Add Worker Pausing/Resuming events by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59895 +* [13.x] Allow PHPStan to infer the pivot type when passing the pivot model directly by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/59959 +* [13.x] Document missing $health param on ApplicationBuilder::withRouting by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59968 +* [13.x] Correct Log\Context\Repository::handleUnserializeExceptionsUsing [@return](https://github.com/return) to $this by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59965 +* [13.x] Correct Attribute caching toggles [@return](https://github.com/return) to $this by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59962 +* [13.x] Correct Factory::configure [@return](https://github.com/return) to $this by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59963 +* [13.x] Correct Translator::handleMissingKeysUsing [@return](https://github.com/return) to $this by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59964 +* [13.x] Correct Password::min [@return](https://github.com/return) to static by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59967 +* [13.x] Mark processIndexes type field nullable in [@return](https://github.com/return) shape by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59961 +* Add `assertSessionMissingInput` by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/59970 +* [13.x] Mark generation type field nullable in processColumns [@return](https://github.com/return) shape by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/59960 +* [13.x] Allow custom on delete/update by [@JurianArie](https://github.com/JurianArie) in https://github.com/laravel/framework/pull/59986 +* Allow mail default driver to accept enums by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/59973 +* [13.x] LocalScope private recursion by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59979 +* [13.x] Add an environment filter to the `schedule:list` command by [@m-fi](https://github.com/m-fi) in https://github.com/laravel/framework/pull/59993 +* [13.x] Add generic result type to collection min/max methods by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59991 +* [13.x] Drop 12.x release notes and update heading by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59987 +* [13.x] Add enum support to QueueFake assertPushedOn method by [@riesjart](https://github.com/riesjart) in https://github.com/laravel/framework/pull/59990 +* [13.x] Improvements to collection sort docblocks by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59988 +* [13.x] Add all* queue inspection methods by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59997 +* [13.x] Add support for `SortDirection` enum to query builder classes by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/59865 ## [v13.7.0](https://github.com/laravel/framework/compare/v13.6.0...v13.7.0) - 2026-04-28 From f9b0bfc72722d2fa7c5a3eb0519c66e1db6d84f0 Mon Sep 17 00:00:00 2001 From: Wade Urry Date: Tue, 5 May 2026 22:15:25 +0100 Subject: [PATCH 307/596] [13.x] Fix issue using custom aws credential providers (#60000) * Early exit if credentials provider is not a string * Check credentials is an array before accessing the provider * Fix code style --- src/Illuminate/Queue/Connectors/SqsConnector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index 0bce9a654a95..55f23add64ed 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -53,10 +53,10 @@ protected function resolveCredentialProvider(array $config) { $credentials = $config['credentials'] ?? null; - $provider = is_string($credentials) ? $credentials : ($credentials['provider'] ?? null); + $provider = is_array($credentials) ? ($credentials['provider'] ?? null) : $credentials; - if (is_null($provider)) { - return null; + if (! is_string($provider)) { + return $provider; } $options = is_array($credentials) ? Arr::except($credentials, ['provider']) : []; From 2b35fa5e505bfe1eabbd0d04a69b956eab71eca1 Mon Sep 17 00:00:00 2001 From: Wade Urry Date: Wed, 6 May 2026 14:28:28 +0100 Subject: [PATCH 308/596] Backport #60000 to 12.x (#60006) --- src/Illuminate/Queue/Connectors/SqsConnector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index 0bce9a654a95..55f23add64ed 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -53,10 +53,10 @@ protected function resolveCredentialProvider(array $config) { $credentials = $config['credentials'] ?? null; - $provider = is_string($credentials) ? $credentials : ($credentials['provider'] ?? null); + $provider = is_array($credentials) ? ($credentials['provider'] ?? null) : $credentials; - if (is_null($provider)) { - return null; + if (! is_string($provider)) { + return $provider; } $options = is_array($credentials) ? Arr::except($credentials, ['provider']) : []; From 328e74ae81f5fbd242039953ebde3386d3268665 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Wed, 6 May 2026 15:38:08 +0200 Subject: [PATCH 309/596] Revert "Correct Factory::configure @return to $this (#59963)" (#60004) This reverts commit 4727569b831d0bb5596889f45ea273d91b2e67de. --- src/Illuminate/Database/Eloquent/Factories/Factory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 8f016849daf1..b2dcba24a34c 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -230,7 +230,7 @@ public static function times(int $count) /** * Configure the factory. * - * @return $this + * @return static */ public function configure() { From 0de76c253050f285e361d28d5a5827d781db6b26 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Wed, 6 May 2026 15:43:32 +0200 Subject: [PATCH 310/596] [13.x] Replace `mb_split` with `preg_split` (#60012) * Replace mb_split with preg_split in Str * Add some additional multibyte string tests --- src/Illuminate/Support/Str.php | 8 ++++---- tests/Support/SupportStrTest.php | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 53c549404669..c28878ddb45b 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1448,7 +1448,7 @@ public static function title($value) */ public static function headline($value) { - $parts = mb_split('\s+', $value); + $parts = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); $parts = count($parts) > 1 ? array_map(static::title(...), $parts) @@ -1468,7 +1468,7 @@ public static function headline($value) */ public static function initials($value, $capitalize = false) { - $parts = mb_split("\s+", $value); + $parts = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); $parts = array_map(fn ($part) => mb_substr($part, 0, 1), $parts); @@ -1499,7 +1499,7 @@ public static function apa($value) $endPunctuation = ['.', '!', '?', ':', '—', ',']; - $words = mb_split('\s+', $value); + $words = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); $wordCount = count($words); for ($i = 0; $i < $wordCount; $i++) { @@ -1719,7 +1719,7 @@ public static function studly($value) return static::$studlyCache[$key]; } - $words = mb_split('\s+', static::replace(['-', '_'], ' ', $value)); + $words = preg_split('/\s+/', static::replace(['-', '_'], ' ', $value), -1, PREG_SPLIT_NO_EMPTY); $studlyWords = array_map(fn ($word) => static::ucfirst($word), $words); diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index bab5090239b3..9afdec875516 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -84,6 +84,8 @@ public function testStringHeadline() $this->assertSame('Sind Öde Und So', Str::headline('sindÖdeUndSo')); + $this->assertSame('❤ Multi Byte ☆', Str::headline('❤_multiByte-☆')); + $this->assertSame('Orwell 1984', Str::headline('orwell 1984')); $this->assertSame('Orwell 1984', Str::headline('orwell 1984')); $this->assertSame('Orwell 1984', Str::headline('-orwell-1984 -')); @@ -101,6 +103,8 @@ public function testStringInitials() $this->assertSame('JB', Str::initials('james bond', true)); $this->assertSame('JBLL', Str::initials('james bond loves laravel', true)); + + $this->assertSame('❤M☆', Str::initials('❤ MULTIByte ☆')); } public function testStringApa() @@ -145,6 +149,8 @@ public function testStringApa() $this->assertSame('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.', Str::apa('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.')); $this->assertSame('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.', Str::apa('УСТНОЕ СЛОВО – НЕ ВОРОБЕЙ. КАК ТОЛЬКО ОН ВЫЛЕТИТ, ЕГО НЕ ПОЙМАЕШЬ.')); + $this->assertSame('❤ Multibyte ☆', Str::apa('❤ MULTIByte ☆')); + $this->assertSame('', Str::apa('')); $this->assertSame(' ', Str::apa(' ')); } @@ -1154,6 +1160,7 @@ public function testStudly() $this->assertSame('FooBarBaz', Str::studly('foo-bar_baz')); $this->assertSame('ÖffentlicheÜberraschungen', Str::studly('öffentliche-überraschungen')); + $this->assertSame('❤MultiByte☆', Str::studly('❤ multi-byte☆')); } public function testPascal() From e74a53de7106c6129cd7fcb2a7c60e1243cd7e28 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Wed, 6 May 2026 15:46:25 +0200 Subject: [PATCH 311/596] Keep calls to implode() consistent (#60013) Co-authored-by: Lucas Michot --- rector.php | 2 -- src/Illuminate/Support/Str.php | 4 ++-- src/Illuminate/View/Concerns/ManagesStacks.php | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/rector.php b/rector.php index 106dd8ec12a3..cd3776f18788 100644 --- a/rector.php +++ b/rector.php @@ -6,7 +6,6 @@ use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector; -use Rector\CodingStyle\Rector\FuncCall\ConsistentImplodeRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\FunctionFirstClassCallableRector; use Rector\Config\RectorConfig; @@ -105,7 +104,6 @@ ClosureDelegatingCallToFirstClassCallableRector::class, ClosureFromCallableToFirstClassCallableRector::class, ClosureToArrowFunctionRector::class, - ConsistentImplodeRector::class, DynamicClassConstFetchRector::class, FunctionFirstClassCallableRector::class, GetDebugTypeRector::class, diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index c28878ddb45b..229574aec74d 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1401,7 +1401,7 @@ public static function remove($search, $subject, $caseSensitive = true) */ public static function reverse(string $value) { - return implode(array_reverse(mb_str_split($value))); + return implode('', array_reverse(mb_str_split($value))); } /** @@ -1723,7 +1723,7 @@ public static function studly($value) $studlyWords = array_map(fn ($word) => static::ucfirst($word), $words); - return static::$studlyCache[$key] = implode($studlyWords); + return static::$studlyCache[$key] = implode('', $studlyWords); } /** diff --git a/src/Illuminate/View/Concerns/ManagesStacks.php b/src/Illuminate/View/Concerns/ManagesStacks.php index aecff30f7178..0991a007acfa 100644 --- a/src/Illuminate/View/Concerns/ManagesStacks.php +++ b/src/Illuminate/View/Concerns/ManagesStacks.php @@ -155,11 +155,11 @@ public function yieldPushContent($section, $default = '') $output = ''; if (isset($this->prepends[$section])) { - $output .= implode(array_reverse($this->prepends[$section])); + $output .= implode('', array_reverse($this->prepends[$section])); } if (isset($this->pushes[$section])) { - $output .= implode($this->pushes[$section]); + $output .= implode('', $this->pushes[$section]); } return $output; From 46a281ea6fe17c766355a3d164af531ae0b3c0ae Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Wed, 6 May 2026 12:49:46 -0500 Subject: [PATCH 312/596] update `rand()` to `mt_rand()` (#60018) as of v7.1 `rand()` has been aliased to `mt_rand()`. we get a present, but mostly insignificant, performance bump from switching, but the big benefit is consistency in the codebase. --- .../Console/Scheduling/ScheduleGroupTest.php | 2 +- tests/Support/OnceTest.php | 30 +++++++++---------- types/Support/Collection.php | 2 +- types/Support/LazyCollection.php | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index d0a9deef3ebf..4753425990b7 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -119,7 +119,7 @@ public static function groupAttributes(): array 'runInBackground' => ['runInBackground', true], 'evenInMaintenanceMode' => ['evenInMaintenanceMode', true], 'evenWhenPaused' => ['evenWhenPaused', true], - 'withoutOverlapping' => ['withoutOverlapping', rand(1000, 1400)], + 'withoutOverlapping' => ['withoutOverlapping', mt_rand(1000, 1400)], ]; } diff --git a/tests/Support/OnceTest.php b/tests/Support/OnceTest.php index f6d90fe29b5d..3c7451dd001c 100644 --- a/tests/Support/OnceTest.php +++ b/tests/Support/OnceTest.php @@ -21,7 +21,7 @@ public function testResultMemoization() { public function rand() { - return once(fn () => rand(1, PHP_INT_MAX)); + return once(fn () => mt_rand(1, PHP_INT_MAX)); } }; @@ -92,7 +92,7 @@ public function testIsNotMemoizedWhenCallableUsesChanges() public function rand(string $letter) { return once(function () use ($letter) { - return $letter.rand(1, 10000000); + return $letter.mt_rand(1, 10000000); }); } }; @@ -111,7 +111,7 @@ public function rand(string $letter) $letter = 'a'; a: - $results[] = once(fn () => $letter.rand(1, 10000000)); + $results[] = once(fn () => $letter.mt_rand(1, 10000000)); if (count($results) < 2) { goto a; @@ -205,7 +205,7 @@ public function testStaticMemoization() public function testMemoizationWhenOnceIsWithinClosure() { - $resolver = fn () => once(fn () => rand(1, PHP_INT_MAX)); + $resolver = fn () => once(fn () => mt_rand(1, PHP_INT_MAX)); $first = $resolver(); $second = $resolver(); @@ -273,15 +273,15 @@ public function testMemoizationOnSameLine() { $this->markTestSkipped('This test shows a limitation of the current implementation.'); - $result = [once(fn () => rand(1, PHP_INT_MAX)), once(fn () => rand(1, PHP_INT_MAX))]; + $result = [once(fn () => mt_rand(1, PHP_INT_MAX)), once(fn () => mt_rand(1, PHP_INT_MAX))]; $this->assertNotSame($result[0], $result[1]); } public function testResultIsDifferentWhenCalledFromDifferentClosures() { - $resolver = fn () => once(fn () => rand(1, PHP_INT_MAX)); - $resolver2 = fn () => once(fn () => rand(1, PHP_INT_MAX)); + $resolver = fn () => once(fn () => mt_rand(1, PHP_INT_MAX)); + $resolver2 = fn () => once(fn () => mt_rand(1, PHP_INT_MAX)); $first = $resolver(); $second = $resolver2(); @@ -295,7 +295,7 @@ public function testResultIsMemoizedWhenCalledFromMethodsWithSameName() { public function rand() { - return once(fn () => rand(1, PHP_INT_MAX)); + return once(fn () => mt_rand(1, PHP_INT_MAX)); } }; @@ -303,7 +303,7 @@ public function rand() { public function rand() { - return once(fn () => rand(1, PHP_INT_MAX)); + return once(fn () => mt_rand(1, PHP_INT_MAX)); } }; @@ -319,7 +319,7 @@ public function testRecursiveOnceCalls() { public function rand() { - return once(fn () => once(fn () => rand(1, PHP_INT_MAX))); + return once(fn () => once(fn () => mt_rand(1, PHP_INT_MAX))); } }; @@ -375,24 +375,24 @@ public function testExtendedStaticClassOnceCalls() $letter = 'a'; -$GLOBALS['onceable1'] = fn () => once(fn () => $letter.rand(1, PHP_INT_MAX)); -$GLOBALS['onceable2'] = fn () => once(fn () => $letter.rand(1, PHP_INT_MAX)); +$GLOBALS['onceable1'] = fn () => once(fn () => $letter.mt_rand(1, PHP_INT_MAX)); +$GLOBALS['onceable2'] = fn () => once(fn () => $letter.mt_rand(1, PHP_INT_MAX)); function my_rand() { - return once(fn () => rand(1, PHP_INT_MAX)); + return once(fn () => mt_rand(1, PHP_INT_MAX)); } class MyClass { public function rand() { - return once(fn () => rand(1, PHP_INT_MAX)); + return once(fn () => mt_rand(1, PHP_INT_MAX)); } public static function staticRand() { - return once(fn () => rand(1, PHP_INT_MAX)); + return once(fn () => mt_rand(1, PHP_INT_MAX)); } public function callRand() diff --git a/types/Support/Collection.php b/types/Support/Collection.php index 76effbf0de4a..86f8a7e20e0e 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -890,7 +890,7 @@ function ($collection, $count) { assertType('int<1, 2>', $collection::make(['string'])->sum(function ($string) { assertType('string', $string); - return rand(1, 2); + return mt_rand(1, 2); })); assertType('Illuminate\Support\Collection', $collection::make([1])->take(1)); diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index 1ba973b22427..d5b382a95edb 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -750,7 +750,7 @@ public function toArray(): array assertType('int<1, 2>', $collection::make(['string'])->sum(function ($string) { assertType('string', $string); - return rand(1, 2); + return mt_rand(1, 2); })); assertType('Illuminate\Support\LazyCollection', $collection::make([1])->take(1)); From 7f488dfbc06bd2b97fbdb4b13216192592587db8 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Wed, 6 May 2026 12:50:00 -0500 Subject: [PATCH 313/596] [13.x] remove `mt_srand()` deprecated "mode" argument (#60020) * remove `mt_srand()` deprecated "mode" argument The "MT_RAND_PHP" mode uses an incorrect Mersenne Twister implementation which was used as the default up till PHP 7.1.0. This mode is only available for backward compatibility. It has also been deprecated in 8.3. This commit removes the deprecated mode, and falls back to the default mode of "MT_RAND_MT19937". https://www.php.net/manual/en/function.mt-srand.php * fix assertions with the new mode, the deterministic "random" output has changed, so we need to adjust. --- tests/Integration/Foundation/FoundationHelpersTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Integration/Foundation/FoundationHelpersTest.php b/tests/Integration/Foundation/FoundationHelpersTest.php index 7ea8ec98ff82..10d1f96adc81 100644 --- a/tests/Integration/Foundation/FoundationHelpersTest.php +++ b/tests/Integration/Foundation/FoundationHelpersTest.php @@ -124,10 +124,10 @@ public function testFakeReturnsSameInstance() public function testFakeUsesLocale() { - mt_srand(12345, MT_RAND_PHP); + mt_srand(12345); // Should fallback to en_US - $this->assertSame('Arkansas', fake()->state()); + $this->assertSame('New Jersey', fake()->state()); $this->assertContains(fake('de_DE')->state(), [ 'Baden-Württemberg', 'Bayern', 'Berlin', 'Brandenburg', 'Bremen', 'Hamburg', 'Hessen', 'Mecklenburg-Vorpommern', 'Niedersachsen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland', 'Sachsen', 'Sachsen-Anhalt', 'Schleswig-Holstein', 'Thüringen', ]); @@ -138,10 +138,10 @@ public function testFakeUsesLocale() ]); config(['app.faker_locale' => 'en_AU']); - mt_srand(4, MT_RAND_PHP); + mt_srand(4); // Should fallback to en_US - $this->assertSame('Australian Capital Territory', fake()->state()); + $this->assertSame('Northern Territory', fake()->state()); } protected function makeManifest($directory = '') From 565c58dcd916efd9f76c249864b7ed3cb3594652 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Wed, 6 May 2026 19:50:13 +0200 Subject: [PATCH 314/596] Remove useless fail-fast option (#60019) Co-authored-by: Lucas Michot --- .github/workflows/databases-nightly.yml | 6 ------ .github/workflows/databases.yml | 27 ------------------------- .github/workflows/facades.yml | 3 --- .github/workflows/queues.yml | 6 ------ 4 files changed, 42 deletions(-) diff --git a/.github/workflows/databases-nightly.yml b/.github/workflows/databases-nightly.yml index b33176a5afb7..c7074e605a03 100644 --- a/.github/workflows/databases-nightly.yml +++ b/.github/workflows/databases-nightly.yml @@ -18,9 +18,6 @@ jobs: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: MySQL 9 steps: @@ -64,9 +61,6 @@ jobs: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: MariaDB Very Latest steps: diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index f97a2fa331a9..43d6ce29dd27 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -22,9 +22,6 @@ jobs: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: MySQL 5.7 steps: @@ -69,9 +66,6 @@ jobs: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: MySQL 8 steps: @@ -115,9 +109,6 @@ jobs: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: MariaDB 10 steps: @@ -162,9 +153,6 @@ jobs: - 5432:5432 options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: PostgreSQL 18 steps: @@ -211,9 +199,6 @@ jobs: - 5432:5432 options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: PostgreSQL 14 steps: @@ -260,9 +245,6 @@ jobs: - 5432:5432 options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: - fail-fast: true - name: PostgreSQL 10 steps: @@ -307,9 +289,6 @@ jobs: ports: - 1433:1433 - strategy: - fail-fast: true - name: SQL Server 2019 steps: @@ -355,9 +334,6 @@ jobs: ports: - 1433:1433 - strategy: - fail-fast: true - name: SQL Server 2017 steps: @@ -394,9 +370,6 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 - strategy: - fail-fast: true - name: SQLite steps: diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index ef7e9c4b84f7..a412b2f74fd1 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -14,9 +14,6 @@ jobs: update: runs-on: ubuntu-24.04 - strategy: - fail-fast: true - name: Facade DocBlocks steps: diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index f437cd52018e..7703c1d5c8d3 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -11,9 +11,6 @@ jobs: sync: runs-on: ubuntu-24.04 - strategy: - fail-fast: true - name: Sync Driver steps: @@ -46,9 +43,6 @@ jobs: database: runs-on: ubuntu-24.04 - strategy: - fail-fast: true - name: Database Driver steps: From 899392e9caf09053d79e2a6ebea45ab8dea8c056 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Wed, 6 May 2026 19:51:10 +0200 Subject: [PATCH 315/596] Prefer spaceship operator when possible (#60015) Co-authored-by: Lucas Michot --- rector.php | 2 -- tests/Support/SupportCollectionTest.php | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/rector.php b/rector.php index cd3776f18788..4b797725c9df 100644 --- a/rector.php +++ b/rector.php @@ -13,7 +13,6 @@ use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; use Rector\Php56\Rector\FuncCall\PowToExpRector; use Rector\Php70\Rector\FuncCall\RandomFunctionRector; -use Rector\Php70\Rector\If_\IfToSpaceshipRector; use Rector\Php70\Rector\MethodCall\ThisCallOnStaticMethodToStaticCallRector; use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector; use Rector\Php70\Rector\Ternary\TernaryToNullCoalescingRector; @@ -107,7 +106,6 @@ DynamicClassConstFetchRector::class, FunctionFirstClassCallableRector::class, GetDebugTypeRector::class, - IfToSpaceshipRector::class, NullToStrictStringFuncCallArgRector::class, PowToExpRector::class, RandomFunctionRector::class, diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index abc13cb0bf5c..daa4aa9adf1e 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -2043,13 +2043,7 @@ public function testSortDesc($collection) #[DataProvider('collectionClassProvider')] public function testSortWithCallback($collection) { - $data = (new $collection([5, 3, 1, 2, 4]))->sort(function ($a, $b) { - if ($a === $b) { - return 0; - } - - return ($a < $b) ? -1 : 1; - }); + $data = (new $collection([5, 3, 1, 2, 4]))->sort(fn ($a, $b) => $a <=> $b); $this->assertEquals(range(1, 5), array_values($data->all())); } From 40443553dae6ad26f125e92ff4f55cfafc0c2ab3 Mon Sep 17 00:00:00 2001 From: Cas Ebbers <617080+CasEbb@users.noreply.github.com> Date: Wed, 6 May 2026 19:51:30 +0200 Subject: [PATCH 316/596] Fix incorrectly opened DocBlocks (#60014) --- src/Illuminate/Console/OutputStyle.php | 2 +- src/Illuminate/Database/Schema/BlueprintState.php | 2 +- src/Illuminate/Database/Schema/Grammars/Grammar.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Console/OutputStyle.php b/src/Illuminate/Console/OutputStyle.php index d88ac78fc301..2b08d605e7b1 100644 --- a/src/Illuminate/Console/OutputStyle.php +++ b/src/Illuminate/Console/OutputStyle.php @@ -125,7 +125,7 @@ public function newLineWritten() return $this->newLineWritten; } - /* + /** * Count the number of trailing new lines in a string. * * @param string|iterable $messages diff --git a/src/Illuminate/Database/Schema/BlueprintState.php b/src/Illuminate/Database/Schema/BlueprintState.php index a4ad1149d479..3f32cabdbeb1 100644 --- a/src/Illuminate/Database/Schema/BlueprintState.php +++ b/src/Illuminate/Database/Schema/BlueprintState.php @@ -145,7 +145,7 @@ public function getForeignKeys() return $this->foreignKeys; } - /* + /** * Update the blueprint's state. * * @param \Illuminate\Support\Fluent $command diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index 4bbee2d78f2e..8b6bf6b12c80 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -438,7 +438,7 @@ protected function getCommandsByName(Blueprint $blueprint, $name) }); } - /* + /** * Determine if a command with a given name exists on the blueprint. * * @param \Illuminate\Database\Schema\Blueprint $blueprint From 724ae9dfc21ed0060ae7e5054637e0629b4cebab Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Wed, 6 May 2026 19:51:44 +0200 Subject: [PATCH 317/596] Ensure that the named arguments are sorted during a call (#60017) Co-authored-by: Lucas Michot --- rector.php | 2 ++ src/Illuminate/Foundation/Console/VendorPublishCommand.php | 2 +- src/Illuminate/Process/Factory.php | 2 +- src/Illuminate/Queue/Console/ListenCommand.php | 4 ++-- src/Illuminate/Support/Testing/Fakes/QueueFake.php | 2 +- tests/Cache/CacheSessionStoreTest.php | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rector.php b/rector.php index 4b797725c9df..9abfdd45c33c 100644 --- a/rector.php +++ b/rector.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Rector\CodeQuality\Rector\FuncCall\SortCallLikeNamedArgsRector; use Rector\CodeQuality\Rector\Identical\StrlenZeroToIdenticalEmptyStringRector; use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector; @@ -123,6 +124,7 @@ ->withRules([ ...$testsuiteRules, CountArrayToEmptyArrayComparisonRector::class, + SortCallLikeNamedArgsRector::class, StrlenZeroToIdenticalEmptyStringRector::class, ]) ->withPreparedSets( diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index 3134deee26a4..cbb4f1b7797f 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -141,11 +141,11 @@ protected function promptForProviderOrTag() ) : search( label: "Which provider or tag's files would you like to publish?", - placeholder: 'Search...', options: fn ($search) => array_values(array_filter( $choices, fn ($choice) => str_contains(strtolower($choice), strtolower($search)) )), + placeholder: 'Search...', scroll: 15, ); diff --git a/src/Illuminate/Process/Factory.php b/src/Illuminate/Process/Factory.php index ac1d5f3150f0..3b2f285e5795 100644 --- a/src/Illuminate/Process/Factory.php +++ b/src/Illuminate/Process/Factory.php @@ -53,9 +53,9 @@ class Factory public function result(array|string $output = '', array|string $errorOutput = '', int $exitCode = 0) { return new FakeProcessResult( + exitCode: $exitCode, output: $output, errorOutput: $errorOutput, - exitCode: $exitCode, ); } diff --git a/src/Illuminate/Queue/Console/ListenCommand.php b/src/Illuminate/Queue/Console/ListenCommand.php index e7979838c64f..d0aeb53db1c2 100755 --- a/src/Illuminate/Queue/Console/ListenCommand.php +++ b/src/Illuminate/Queue/Console/ListenCommand.php @@ -109,9 +109,9 @@ protected function gatherOptions() memory: $this->option('memory'), timeout: $this->option('timeout'), sleep: $this->option('sleep'), - rest: $this->option('rest'), maxTries: $this->option('tries'), - force: $this->option('force') + force: $this->option('force'), + rest: $this->option('rest') ); } diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index 644caf267c1b..afab3ecce0dc 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -473,11 +473,11 @@ public function pendingJobs($queue = null): Collection ->flatten(1) ->filter(fn ($job) => $job['queue'] === $queue) ->map(fn ($data) => new InspectedJob( + uuid: null, name: is_object($data['job']) ? (method_exists($data['job'], 'displayName') ? $data['job']->displayName() : get_class($data['job'])) : $data['job'], attempts: 0, - uuid: null, createdAt: null, )); } diff --git a/tests/Cache/CacheSessionStoreTest.php b/tests/Cache/CacheSessionStoreTest.php index cd3f745b6231..fa71090db271 100755 --- a/tests/Cache/CacheSessionStoreTest.php +++ b/tests/Cache/CacheSessionStoreTest.php @@ -241,9 +241,9 @@ protected static function getSession() { return new Store( name: 'name', - serialization: 'php', handler: new ArraySessionHandler(10), id: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + serialization: 'php', ); } } From 3a5f17eed3adae4c232b63e9ea9fc1f85261e37e Mon Sep 17 00:00:00 2001 From: Romain 'Maz' BILLOIR Date: Wed, 6 May 2026 20:18:46 +0200 Subject: [PATCH 318/596] Upgrade guzzlehttp/psr7 to ^2.9 and let it handle nested arrays in multipart bodies (#59984) --- composer.json | 2 +- src/Illuminate/Http/Client/PendingRequest.php | 17 +++++------------ tests/Http/HttpClientTest.php | 18 +++++++----------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 71812caf8e53..d54ec3eb8ee8 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,7 @@ "ably/ably-php": "^1.0", "aws/aws-sdk-php": "^3.322.9", "fakerphp/faker": "^1.24", - "guzzlehttp/psr7": "^2.4", + "guzzlehttp/psr7": "^2.9", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 56eb19ba53e7..4836d71609fe 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1155,20 +1155,13 @@ protected function parseHttpOptions(array $options) protected function parseMultipartBodyFormat(array $data) { return (new Collection($data)) - ->flatMap(function ($value, $key) { - if (is_array($value)) { - // If the array has 'name' and 'contents' keys, it's already formatted for multipart... - if (isset($value['name'], $value['contents'])) { - return [$value]; - } - - // Otherwise, treat it as multiple values for the same field name... - return (new Collection($value))->map(function ($item) use ($key) { - return ['name' => $key.'[]', 'contents' => $item]; - }); + ->map(function ($value, $key) { + // If the array has 'name' and 'contents' keys, it's already formatted for multipart... + if (is_array($value) && isset($value['name'], $value['contents'])) { + return $value; } - return [['name' => $key, 'contents' => $value]]; + return ['name' => $key, 'contents' => $value]; }) ->values() ->all(); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index db86f7ab8861..ef2860b5d28c 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -849,10 +849,8 @@ public function testCanSendMultipartDataWithArrayValues() Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'name' && $request[0]['contents'] === 'Steve' && - $request[1]['name'] === 'roles[]' && - $request[1]['contents'] === 'Network Administrator' && - $request[2]['name'] === 'roles[]' && - $request[2]['contents'] === 'Janitor'; + $request[1]['name'] === 'roles' && + $request[1]['contents'] === ['Network Administrator', 'Janitor']; }); } @@ -872,13 +870,11 @@ public function testCanSendMultipartDataWithFileAndArrayValues() Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'name' && $request[0]['contents'] === 'Steve' && - $request[1]['name'] === 'roles[]' && - $request[1]['contents'] === 'Network Administrator' && - $request[2]['name'] === 'roles[]' && - $request[2]['contents'] === 'Janitor' && - $request[3]['name'] === 'attachment' && - $request[3]['contents'] === 'photo_content' && - $request[3]['filename'] === 'photo.jpg'; + $request[1]['name'] === 'roles' && + $request[1]['contents'] === ['Network Administrator', 'Janitor'] && + $request[2]['name'] === 'attachment' && + $request[2]['contents'] === 'photo_content' && + $request[2]['filename'] === 'photo.jpg'; }); } From 2f991856f500669a44582f597896084135e0b623 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 6 May 2026 19:50:26 +0100 Subject: [PATCH 319/596] [13.x] Add Preparable interface for Jobs (#59879) * 13.x add InteractsWithDispatch and prepare method use snake, I suppose and implements order mixed up in this clean up a bit think harder wording Revert "tests" This reverts commit 360d8011eee801dea5227a25057c3aa2f0646773. tests Update PendingDispatch.php rename methods rename adjust type Update BeforeDispatch.php damn you mr cs doc block rename.. again * smarten up test names * formatting --------- Co-authored-by: Taylor Otwell --- .../Contracts/Queue/PreparesForDispatch.php | 13 ++++ .../Foundation/Bus/PendingDispatch.php | 5 ++ .../Queue/PreparesForDispatchTest.php | 64 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/Illuminate/Contracts/Queue/PreparesForDispatch.php create mode 100644 tests/Integration/Queue/PreparesForDispatchTest.php diff --git a/src/Illuminate/Contracts/Queue/PreparesForDispatch.php b/src/Illuminate/Contracts/Queue/PreparesForDispatch.php new file mode 100644 index 000000000000..079f00f99df3 --- /dev/null +++ b/src/Illuminate/Contracts/Queue/PreparesForDispatch.php @@ -0,0 +1,13 @@ +job instanceof PreparesForDispatch && $this->job->prepareForDispatch() === false) { + return false; + } + if (! $this->job instanceof ShouldBeUnique) { return true; } diff --git a/tests/Integration/Queue/PreparesForDispatchTest.php b/tests/Integration/Queue/PreparesForDispatchTest.php new file mode 100644 index 000000000000..010ed76828bc --- /dev/null +++ b/tests/Integration/Queue/PreparesForDispatchTest.php @@ -0,0 +1,64 @@ +assertTrue(PreparesForDispatchVoidJob::$ran); + Queue::assertPushed(PreparesForDispatchVoidJob::class); + } +} + +class PreparesForDispatchFalseJob implements PreparesForDispatch, ShouldQueue +{ + use Dispatchable, Queueable; + + public function prepareForDispatch(): bool + { + return false; + } + + public function handle(): void + { + } +} + +class PreparesForDispatchVoidJob implements PreparesForDispatch, ShouldQueue +{ + use Dispatchable, Queueable; + + public static bool $ran = false; + + public function prepareForDispatch(): void + { + static::$ran = true; + } + + public function handle(): void + { + } +} From f9d7d7f95bf40f18cbd780e79f99114f45278735 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 6 May 2026 18:51:07 +0000 Subject: [PATCH 320/596] Update facade docblocks --- src/Illuminate/Support/Facades/Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 2865715dcb98..411ad9739f31 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -175,7 +175,7 @@ * @method static float|int clamp(string $key, int|float $min, int|float $max, int|float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, \UnitEnum|string|null $tz = null) * @method static \Carbon\CarbonInterval|null interval(string $key, \Carbon\Unit|string|null $unit = null) - * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string $enumClass, \BackedEnum|null $default = null) + * @method static \BackedEnum|\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) From ed3b10b803702594b7108021ddf518a7563c4ae0 Mon Sep 17 00:00:00 2001 From: Jeremy Nikolic Date: Thu, 7 May 2026 15:33:15 +0200 Subject: [PATCH 321/596] Add support to scoped filesystem for Cloud (#60030) --- src/Illuminate/Foundation/Cloud.php | 32 ++++++++++++++-------- tests/Integration/Foundation/CloudTest.php | 29 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index de30ca27302e..95db25d590ba 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -51,18 +51,26 @@ public static function configureDisks(Application $app): void $disks = json_decode($_SERVER['LARAVEL_CLOUD_DISK_CONFIG'], true); foreach ($disks as $disk) { - $app['config']->set('filesystems.disks.'.$disk['disk'], [ - 'driver' => 's3', - 'key' => $disk['access_key_id'], - 'secret' => $disk['access_key_secret'], - 'bucket' => $disk['bucket'], - 'url' => $disk['url'], - 'endpoint' => $disk['endpoint'], - 'region' => 'auto', - 'use_path_style_endpoint' => false, - 'throw' => false, - 'report' => false, - ]); + if ($disk['scoped_disk'] ?? false) { + $app['config']->set('filesystems.disks.'.$disk['disk'], [ + 'driver' => 'scoped', + 'disk' => $disk['scoped_disk'], + 'prefix' => $disk['prefix'] ?? '', + ]); + } else { + $app['config']->set('filesystems.disks.'.$disk['disk'], [ + 'driver' => 's3', + 'key' => $disk['access_key_id'], + 'secret' => $disk['access_key_secret'], + 'bucket' => $disk['bucket'], + 'url' => $disk['url'], + 'endpoint' => $disk['endpoint'], + 'region' => 'auto', + 'use_path_style_endpoint' => false, + 'throw' => false, + 'report' => false, + ]); + } if ($disk['is_default'] ?? false) { $app['config']->set('filesystems.default', $disk['disk']); diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index e7d463474462..c16f074122fd 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -54,6 +54,35 @@ public function test_it_can_configure_disks() unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); } + public function test_it_can_configure_scoped_disks() + { + $_SERVER['LARAVEL_CLOUD_DISK_CONFIG'] = json_encode( + [ + [ + 'disk' => 'test-disk', + 'access_key_id' => 'test-access-key-id', + 'access_key_secret' => 'test-access-key-secret', + 'bucket' => 'test-bucket', + 'url' => 'test-url', + 'endpoint' => 'test-endpoint', + ], + [ + 'disk' => 'test-disk-scoped', + 'scoped_disk' => 'test-disk', + 'prefix' => 'test/prefix/', + 'is_default' => true, + ], + ] + ); + + Cloud::configureDisks($this->app); + + $this->assertSame('scoped', $this->app['config']->get('filesystems.disks.test-disk-scoped.driver')); + $this->assertSame('test-disk', $this->app['config']->get('filesystems.disks.test-disk-scoped.disk')); + + unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); + } + public function test_it_disables_queue_restart_polling_for_managed_queues() { Worker::$restartable = true; From 025b8244791e390ec54a39809f880f15851375c4 Mon Sep 17 00:00:00 2001 From: Wes Hooper Date: Thu, 7 May 2026 14:34:30 +0100 Subject: [PATCH 322/596] [13.x] Unused `$parameters` in `validate*case()` (#60024) Poking around in here for a new validation rule idea, spotted these aren't used. Confused me for a couple of seconds, since my first time in this area and started to wonder if some magic I needed to be aware of. --- src/Illuminate/Validation/Concerns/ValidatesAttributes.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index ebd0616ec90d..28e4d89431f7 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1452,10 +1452,9 @@ public function validateLte($attribute, $value, $parameters) * * @param string $attribute * @param mixed $value - * @param array $parameters * @return bool */ - public function validateLowercase($attribute, $value, $parameters) + public function validateLowercase($attribute, $value) { return is_string($value) && Str::lower($value) === $value; } @@ -1465,10 +1464,9 @@ public function validateLowercase($attribute, $value, $parameters) * * @param string $attribute * @param mixed $value - * @param array $parameters * @return bool */ - public function validateUppercase($attribute, $value, $parameters) + public function validateUppercase($attribute, $value) { return is_string($value) && Str::upper($value) === $value; } From 0ae7a9a5b93c128228decf9880197926e8d99e0a Mon Sep 17 00:00:00 2001 From: Ben Bjurstrom Date: Thu, 7 May 2026 07:49:11 -0700 Subject: [PATCH 323/596] narrow attachment url scheme (#60034) --- src/Illuminate/Mail/Attachment.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Illuminate/Mail/Attachment.php b/src/Illuminate/Mail/Attachment.php index 7b5f0b654c31..5f34bb98b326 100644 --- a/src/Illuminate/Mail/Attachment.php +++ b/src/Illuminate/Mail/Attachment.php @@ -7,7 +7,9 @@ use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use RuntimeException; class Attachment @@ -64,6 +66,10 @@ public static function fromPath($path) */ public static function fromUrl($url) { + if (! Str::isUrl($url, ['http', 'https'])) { + throw new InvalidArgumentException('Attachment URLs must use the http or https scheme.'); + } + return static::fromPath($url); } From 812bcd1fff6a88be98f0977bb0113febd7b290b1 Mon Sep 17 00:00:00 2001 From: Ben Bjurstrom Date: Thu, 7 May 2026 11:38:37 -0700 Subject: [PATCH 324/596] narrow attachment url scheme (#60035) --- src/Illuminate/Mail/Attachment.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Illuminate/Mail/Attachment.php b/src/Illuminate/Mail/Attachment.php index 609d116a0466..c0d6fa856e23 100644 --- a/src/Illuminate/Mail/Attachment.php +++ b/src/Illuminate/Mail/Attachment.php @@ -7,7 +7,9 @@ use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use RuntimeException; class Attachment @@ -64,6 +66,10 @@ public static function fromPath($path) */ public static function fromUrl($url) { + if (! Str::isUrl($url, ['http', 'https'])) { + throw new InvalidArgumentException('Attachment URLs must use the http or https scheme.'); + } + return static::fromPath($url); } From d2fab5218e59338bea3bf0910d2e146f78c0defb Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 7 May 2026 18:39:12 +0000 Subject: [PATCH 325/596] Update facade docblocks --- src/Illuminate/Support/Facades/Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 2865715dcb98..411ad9739f31 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -175,7 +175,7 @@ * @method static float|int clamp(string $key, int|float $min, int|float $max, int|float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, \UnitEnum|string|null $tz = null) * @method static \Carbon\CarbonInterval|null interval(string $key, \Carbon\Unit|string|null $unit = null) - * @method static \BackedEnum|(\BackedEnum|null enum(string $key, string $enumClass, \BackedEnum|null $default = null) + * @method static \BackedEnum|\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) From 5bb7394202b95b3a3c5d63cb79a616b1c2f20a5c Mon Sep 17 00:00:00 2001 From: olivier-zenchef <90190636+olivier-zenchef@users.noreply.github.com> Date: Thu, 7 May 2026 20:41:18 +0200 Subject: [PATCH 326/596] [13.x] Skip allocation in mergeFillable/Appends/Hidden/Visible when input is empty (#60008) Co-authored-by: olivier-zenchef --- .../Eloquent/Concerns/GuardsAttributes.php | 4 ++ .../Eloquent/Concerns/HasAttributes.php | 4 ++ .../Eloquent/Concerns/HidesAttributes.php | 8 ++++ .../DatabaseEloquentModelAttributesTest.php | 44 +++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index e99bd922f63a..865202f4725c 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -87,6 +87,10 @@ public function fillable(array $fillable) */ public function mergeFillable(array $fillable) { + if ($fillable === []) { + return $this; + } + $this->fillable = array_values(array_unique(array_merge($this->fillable, $fillable))); return $this; diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 8f719b767e70..6836ceb8b611 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2484,6 +2484,10 @@ public function setAppends(array $appends) */ public function mergeAppends(array $appends) { + if ($appends === []) { + return $this; + } + $this->appends = array_values(array_unique(array_merge($this->appends, $appends))); return $this; diff --git a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php index 1d3d7e591418..0e868b1fc312 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php @@ -65,6 +65,10 @@ public function setHidden(array $hidden) */ public function mergeHidden(array $hidden) { + if ($hidden === []) { + return $this; + } + $this->hidden = array_values(array_unique(array_merge($this->hidden, $hidden))); return $this; @@ -101,6 +105,10 @@ public function setVisible(array $visible) */ public function mergeVisible(array $visible) { + if ($visible === []) { + return $this; + } + $this->visible = array_values(array_unique(array_merge($this->visible, $visible))); return $this; diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index f3988e817b06..d672d05a1634 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -347,6 +347,50 @@ public function test_merge_hidden_works_with_attribute(): void $this->assertSame(['password', 'secret', 'api_key'], $model->getHidden()); } + public function test_merge_fillable_with_empty_array_is_noop(): void + { + $model = new ModelWithFillableAttribute; + $original = $model->getFillable(); + + $result = $model->mergeFillable([]); + + $this->assertSame($model, $result); + $this->assertSame($original, $model->getFillable()); + } + + public function test_merge_hidden_with_empty_array_is_noop(): void + { + $model = new ModelWithHiddenAttribute; + $original = $model->getHidden(); + + $result = $model->mergeHidden([]); + + $this->assertSame($model, $result); + $this->assertSame($original, $model->getHidden()); + } + + public function test_merge_visible_with_empty_array_is_noop(): void + { + $model = new ModelWithVisibleAttribute; + $original = $model->getVisible(); + + $result = $model->mergeVisible([]); + + $this->assertSame($model, $result); + $this->assertSame($original, $model->getVisible()); + } + + public function test_merge_appends_with_empty_array_is_noop(): void + { + $model = new ModelWithAppendsAttribute; + $original = $model->getAppends(); + + $result = $model->mergeAppends([]); + + $this->assertSame($model, $result); + $this->assertSame($original, $model->getAppends()); + } + public function test_set_fillable_overrides_attribute(): void { $model = new ModelWithFillableAttribute; From 8c47b464ab856dc298257947449e5f25932d71d2 Mon Sep 17 00:00:00 2001 From: Levi Klingler Date: Fri, 8 May 2026 09:45:19 -0400 Subject: [PATCH 327/596] add generic return types to `Builder` paginate methods (#60045) --- src/Illuminate/Database/Eloquent/Builder.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 8950c81b3816..841426aa4b68 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -1115,7 +1115,7 @@ public function pluck($column, $key = null) * @param string $pageName * @param int|null $page * @param \Closure|int|null $total - * @return \Illuminate\Pagination\LengthAwarePaginator + * @return \Illuminate\Pagination\LengthAwarePaginator * * @throws \InvalidArgumentException */ @@ -1144,7 +1144,7 @@ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', * @param array|string $columns * @param string $pageName * @param int|null $page - * @return \Illuminate\Contracts\Pagination\Paginator + * @return \Illuminate\Pagination\Paginator */ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { @@ -1170,7 +1170,7 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p * @param array|string $columns * @param string $cursorName * @param \Illuminate\Pagination\Cursor|string|null $cursor - * @return \Illuminate\Contracts\Pagination\CursorPaginator + * @return \Illuminate\Pagination\CursorPaginator */ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) { From c5bf38d7a2f95ffa6a7dea6079b80d4b58c7d5dd Mon Sep 17 00:00:00 2001 From: Kevin Bui Date: Fri, 8 May 2026 23:46:12 +1000 Subject: [PATCH 328/596] [13.x] Make PendingDispatch conditionable (#60047) * Make PendingDispatch conditionable. * Remove empty lines. * Updates according to StyleCI. * Remove empty spaces. --- .../Foundation/Bus/PendingDispatch.php | 4 +-- tests/Bus/BusPendingDispatchTest.php | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index d321c5be31c3..462565d5ecc4 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -12,12 +12,12 @@ use Illuminate\Foundation\Queue\InteractsWithUniqueJobs; use Illuminate\Queue\Attributes\DebounceFor; use Illuminate\Queue\Attributes\ReadsQueueAttributes; +use Illuminate\Support\Traits\Conditionable; use LogicException; class PendingDispatch { - use InteractsWithUniqueJobs; - use ReadsQueueAttributes; + use Conditionable, InteractsWithUniqueJobs, ReadsQueueAttributes; /** * The job. diff --git a/tests/Bus/BusPendingDispatchTest.php b/tests/Bus/BusPendingDispatchTest.php index bb612a35cbc4..c98c497cb397 100644 --- a/tests/Bus/BusPendingDispatchTest.php +++ b/tests/Bus/BusPendingDispatchTest.php @@ -107,4 +107,38 @@ public function testDynamicallyProxyMethods() $this->job->shouldReceive('appendToChain')->once()->with($newJob); $this->pendingDispatch->appendToChain($newJob); } + + public function testWhenMethodOfConditionableTraitWithTrue() + { + $this->job->shouldReceive('delay')->once()->with(300); + + $this->pendingDispatch->when(true, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + } + + public function testWhenMethodOfConditionableTraitWithFalse() + { + $this->job->shouldReceive('delay')->never(); + + $this->pendingDispatch->when(false, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + } + + public function testUnlessMethodOfConditionableTraitWithTrue() + { + $this->job->shouldReceive('delay')->never(); + + $this->pendingDispatch->unless(true, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + } + + public function testUnlessMethodOfConditionableTraitWithFalse() + { + $this->job->shouldReceive('delay')->once()->with(300); + + $this->pendingDispatch->unless(false, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + } + + protected function tearDown(): void + { + m::close(); + parent::tearDown(); + } } From 94f1ad15dc94a26ea0d6f61510bf05be5f2fcee1 Mon Sep 17 00:00:00 2001 From: Wes Hooper Date: Fri, 8 May 2026 14:46:50 +0100 Subject: [PATCH 329/596] [13.x] Display error in `queue:pause` when `Worker` isn't pausable (#60023) * [13.x] Display error in `queue:pause` with `withoutInterruptionPolling()` Hey Taylor, @jackbayliss is on vacation this week (allegedly) so you've got me for a change :bowtie: - We didn't used to pause our queues, so called `withoutInterruptionPolling()` in our `AppServiceProvider` - Now we've modernised, we're leaning on pause, but forgot we'd done this and `queue:pause` gives no clue - Queue(!) much swearing by another dev in our team We'll likely stop calling `withoutInterruptionPolling()` of course, so feel free to throw this PR in the sea, Just thought it may avoid other souls scratching their heads. * my exclamation bring all the ci to the style * push all teh changes, duh, people can see this shit * Update PauseCommand.php Co-authored-by: Jack Bayliss --------- Co-authored-by: Jack Bayliss --- src/Illuminate/Queue/Console/PauseCommand.php | 7 ++++ .../Scheduling/QueuePauseCommandTest.php | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/Integration/Console/Scheduling/QueuePauseCommandTest.php diff --git a/src/Illuminate/Queue/Console/PauseCommand.php b/src/Illuminate/Queue/Console/PauseCommand.php index 8b18c23ee621..66abe69f150f 100644 --- a/src/Illuminate/Queue/Console/PauseCommand.php +++ b/src/Illuminate/Queue/Console/PauseCommand.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Queue\Factory as QueueManager; use Illuminate\Queue\Console\Concerns\ParsesQueue; +use Illuminate\Queue\Worker; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'queue:pause')] @@ -35,6 +36,12 @@ public function handle(QueueManager $manager) { [$connection, $queue] = $this->parseQueue($this->argument('queue')); + if (! Worker::$pausable) { + $this->components->error('Queue pausing is currently disabled.'); + + return 1; + } + $manager->pause($connection, $queue); $this->components->info("Job processing on queue [{$connection}:{$queue}] has been paused."); diff --git a/tests/Integration/Console/Scheduling/QueuePauseCommandTest.php b/tests/Integration/Console/Scheduling/QueuePauseCommandTest.php new file mode 100644 index 000000000000..ca6828f817da --- /dev/null +++ b/tests/Integration/Console/Scheduling/QueuePauseCommandTest.php @@ -0,0 +1,33 @@ +artisan('queue:pause default'); + + Event::assertDispatched(QueuePaused::class); + } + + public function testDisabledError() + { + Event::fake(); + + Worker::$pausable = false; + + $this->artisan('queue:pause default'); + + Event::assertNotDispatched(QueuePaused::class); + + Worker::$pausable = true; + } +} From 01de678651200cd8f782b98d3368e22c53da6f8d Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Sat, 9 May 2026 04:26:25 +0600 Subject: [PATCH 330/596] [13.x] Add tests for Attachment::fromUrl() URL scheme validation (#60054) --- tests/Mail/AttachmentTest.php | 114 ++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/Mail/AttachmentTest.php diff --git a/tests/Mail/AttachmentTest.php b/tests/Mail/AttachmentTest.php new file mode 100644 index 000000000000..6b78913f982a --- /dev/null +++ b/tests/Mail/AttachmentTest.php @@ -0,0 +1,114 @@ +assertInstanceOf(Attachment::class, $attachment); + } + + public function testFromUrlWithHttpsScheme(): void + { + $attachment = Attachment::fromUrl('https://example.com/file.pdf'); + + $this->assertInstanceOf(Attachment::class, $attachment); + } + + public function testFromUrlThrowsForFtpScheme(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Attachment URLs must use the http or https scheme.'); + + Attachment::fromUrl('ftp://example.com/file.pdf'); + } + + public function testFromUrlThrowsForFileScheme(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Attachment URLs must use the http or https scheme.'); + + Attachment::fromUrl('file:///var/www/file.pdf'); + } + + public function testFromUrlThrowsForMailtoScheme(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Attachment URLs must use the http or https scheme.'); + + Attachment::fromUrl('mailto:user@example.com'); + } + + public function testFromUrlThrowsForInvalidUrl(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Attachment URLs must use the http or https scheme.'); + + Attachment::fromUrl('not-a-url'); + } + + public function testFromUrlThrowsForEmptyString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Attachment URLs must use the http or https scheme.'); + + Attachment::fromUrl(''); + } + + public function testAsSetFilename(): void + { + $attachment = Attachment::fromPath('/path/to/file.pdf') + ->as('renamed.pdf'); + + $this->assertSame('renamed.pdf', $attachment->as); + } + + public function testWithMimeSetsMimeType(): void + { + $attachment = Attachment::fromPath('/path/to/file.pdf') + ->withMime('application/pdf'); + + $this->assertSame('application/pdf', $attachment->mime); + } + + public function testFluentChaining(): void + { + $attachment = Attachment::fromPath('/path/to/file.jpg') + ->as('photo.jpg') + ->withMime('image/jpeg'); + + $this->assertSame('photo.jpg', $attachment->as); + $this->assertSame('image/jpeg', $attachment->mime); + } + + public function testIsEquivalentWithSamePath(): void + { + $a = Attachment::fromPath('/path/to/file.pdf')->as('file.pdf'); + $b = Attachment::fromPath('/path/to/file.pdf')->as('file.pdf'); + + $this->assertTrue($a->isEquivalent($b)); + } + + public function testIsEquivalentWithDifferentPaths(): void + { + $a = Attachment::fromPath('/path/to/a.pdf'); + $b = Attachment::fromPath('/path/to/b.pdf'); + + $this->assertFalse($a->isEquivalent($b)); + } + + public function testFromDataCreatesAttachment(): void + { + $attachment = Attachment::fromData(fn () => 'file content', 'report.txt'); + + $this->assertInstanceOf(Attachment::class, $attachment); + $this->assertSame('report.txt', $attachment->as); + } +} From 59547770404cf40153c73c2f340a337776a118f6 Mon Sep 17 00:00:00 2001 From: Levi Klingler Date: Fri, 8 May 2026 18:26:39 -0400 Subject: [PATCH 331/596] backport #60045 to 12.x (#60052) --- src/Illuminate/Database/Eloquent/Builder.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 41cfaa494753..24f2830a7fda 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -1109,7 +1109,7 @@ public function pluck($column, $key = null) * @param string $pageName * @param int|null $page * @param \Closure|int|null $total - * @return \Illuminate\Pagination\LengthAwarePaginator + * @return \Illuminate\Pagination\LengthAwarePaginator * * @throws \InvalidArgumentException */ @@ -1138,7 +1138,7 @@ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', * @param array|string $columns * @param string $pageName * @param int|null $page - * @return \Illuminate\Contracts\Pagination\Paginator + * @return \Illuminate\Pagination\Paginator */ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { @@ -1164,7 +1164,7 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p * @param array|string $columns * @param string $cursorName * @param \Illuminate\Pagination\Cursor|string|null $cursor - * @return \Illuminate\Contracts\Pagination\CursorPaginator + * @return \Illuminate\Pagination\CursorPaginator */ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null) { From 7d5d19f478f5285dfd80d8385141de9aedf95da4 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Sat, 9 May 2026 04:28:19 +0600 Subject: [PATCH 332/596] Fix @params typo in toPrettyJson docblocks (#60050) --- src/Illuminate/Pagination/CursorPaginator.php | 2 +- src/Illuminate/Pagination/LengthAwarePaginator.php | 2 +- src/Illuminate/Pagination/Paginator.php | 2 +- src/Illuminate/Support/Fluent.php | 2 +- src/Illuminate/Support/MessageBag.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Pagination/CursorPaginator.php b/src/Illuminate/Pagination/CursorPaginator.php index 314365ae6517..69f5445589d1 100644 --- a/src/Illuminate/Pagination/CursorPaginator.php +++ b/src/Illuminate/Pagination/CursorPaginator.php @@ -184,7 +184,7 @@ public function toJson($options = 0) /** * Convert the object to pretty print formatted JSON. * - * @params int $options + * @param int $options * * @return string */ diff --git a/src/Illuminate/Pagination/LengthAwarePaginator.php b/src/Illuminate/Pagination/LengthAwarePaginator.php index aafa1447feb4..07ef2199ff40 100644 --- a/src/Illuminate/Pagination/LengthAwarePaginator.php +++ b/src/Illuminate/Pagination/LengthAwarePaginator.php @@ -246,7 +246,7 @@ public function toJson($options = 0) /** * Convert the object to pretty print formatted JSON. * - * @params int $options + * @param int $options * * @return string */ diff --git a/src/Illuminate/Pagination/Paginator.php b/src/Illuminate/Pagination/Paginator.php index 32e5ca57ccbe..b99981d9af9f 100644 --- a/src/Illuminate/Pagination/Paginator.php +++ b/src/Illuminate/Pagination/Paginator.php @@ -189,7 +189,7 @@ public function toJson($options = 0) /** * Convert the object to pretty print formatted JSON. * - * @params int $options + * @param int $options * * @return string */ diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index 15ba52b236da..c865f004e932 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -206,7 +206,7 @@ public function toJson($options = 0) /** * Convert the fluent instance to pretty print formatted JSON. * - * @params int $options + * @param int $options * * @return string */ diff --git a/src/Illuminate/Support/MessageBag.php b/src/Illuminate/Support/MessageBag.php index 069534d4f602..03fc5b8c9984 100755 --- a/src/Illuminate/Support/MessageBag.php +++ b/src/Illuminate/Support/MessageBag.php @@ -434,7 +434,7 @@ public function toJson($options = 0) /** * Convert the object to pretty print formatted JSON. * - * @params int $options + * @param int $options * * @return string */ From da527a14252e4538db068a854be54368c697275d Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 8 May 2026 22:28:46 +0000 Subject: [PATCH 333/596] Apply fixes from StyleCI --- src/Illuminate/Pagination/CursorPaginator.php | 1 - src/Illuminate/Pagination/LengthAwarePaginator.php | 1 - src/Illuminate/Pagination/Paginator.php | 1 - src/Illuminate/Support/Fluent.php | 1 - src/Illuminate/Support/MessageBag.php | 1 - 5 files changed, 5 deletions(-) diff --git a/src/Illuminate/Pagination/CursorPaginator.php b/src/Illuminate/Pagination/CursorPaginator.php index 69f5445589d1..9c323562d98a 100644 --- a/src/Illuminate/Pagination/CursorPaginator.php +++ b/src/Illuminate/Pagination/CursorPaginator.php @@ -185,7 +185,6 @@ public function toJson($options = 0) * Convert the object to pretty print formatted JSON. * * @param int $options - * * @return string */ public function toPrettyJson(int $options = 0) diff --git a/src/Illuminate/Pagination/LengthAwarePaginator.php b/src/Illuminate/Pagination/LengthAwarePaginator.php index 07ef2199ff40..e09f2b923b05 100644 --- a/src/Illuminate/Pagination/LengthAwarePaginator.php +++ b/src/Illuminate/Pagination/LengthAwarePaginator.php @@ -247,7 +247,6 @@ public function toJson($options = 0) * Convert the object to pretty print formatted JSON. * * @param int $options - * * @return string */ public function toPrettyJson(int $options = 0) diff --git a/src/Illuminate/Pagination/Paginator.php b/src/Illuminate/Pagination/Paginator.php index b99981d9af9f..bf43969fd8cd 100644 --- a/src/Illuminate/Pagination/Paginator.php +++ b/src/Illuminate/Pagination/Paginator.php @@ -190,7 +190,6 @@ public function toJson($options = 0) * Convert the object to pretty print formatted JSON. * * @param int $options - * * @return string */ public function toPrettyJson(int $options = 0) diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index c865f004e932..cdec09f0e454 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -207,7 +207,6 @@ public function toJson($options = 0) * Convert the fluent instance to pretty print formatted JSON. * * @param int $options - * * @return string */ public function toPrettyJson(int $options = 0) diff --git a/src/Illuminate/Support/MessageBag.php b/src/Illuminate/Support/MessageBag.php index 03fc5b8c9984..1b5beadc6b8a 100755 --- a/src/Illuminate/Support/MessageBag.php +++ b/src/Illuminate/Support/MessageBag.php @@ -435,7 +435,6 @@ public function toJson($options = 0) * Convert the object to pretty print formatted JSON. * * @param int $options - * * @return string */ public function toPrettyJson(int $options = 0) From 12295fa1878c7f9fc1eb92a6290a7f1d5590fef5 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 8 May 2026 17:28:53 -0500 Subject: [PATCH 334/596] re-add docblock for `apply()` method (#60055) this docblock was removed in #59675 when trying to refactor some of the generics. we will add the docblock back, and define the `TModel` generic at the class level, so it can be passed to the interface, and also used in the `apply()` method. --- src/Illuminate/Database/Eloquent/SoftDeletingScope.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index 83b3cdbfd640..09d28a05b043 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -3,7 +3,8 @@ namespace Illuminate\Database\Eloquent; /** - * @implements \Illuminate\Database\Eloquent\Scope<\Illuminate\Database\Eloquent\Model> + * @template TModel of \Illuminate\Database\Eloquent\Model + * @implements \Illuminate\Database\Eloquent\Scope */ class SoftDeletingScope implements Scope { @@ -14,6 +15,13 @@ class SoftDeletingScope implements Scope */ protected $extensions = ['Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed']; + /** + * Apply the scope to a given Eloquent query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param TModel $model + * @return void + */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); From 295448c0076aa817f39a346f058645a7834a566a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 8 May 2026 22:29:09 +0000 Subject: [PATCH 335/596] Apply fixes from StyleCI --- src/Illuminate/Database/Eloquent/SoftDeletingScope.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index 09d28a05b043..e03c8f76e78c 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -4,6 +4,7 @@ /** * @template TModel of \Illuminate\Database\Eloquent\Model + * * @implements \Illuminate\Database\Eloquent\Scope */ class SoftDeletingScope implements Scope From e7ab92eb36a92363c0395d8bc27f7d9c59b16c73 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sun, 10 May 2026 12:47:41 -0300 Subject: [PATCH 336/596] add unicode modifier to preg_split (#60056) --- src/Illuminate/Support/Str.php | 8 ++++---- tests/Support/SupportStrTest.php | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 229574aec74d..9f31b9b135c8 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1448,7 +1448,7 @@ public static function title($value) */ public static function headline($value) { - $parts = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); + $parts = preg_split('/\s+/u', $value, -1, PREG_SPLIT_NO_EMPTY); $parts = count($parts) > 1 ? array_map(static::title(...), $parts) @@ -1468,7 +1468,7 @@ public static function headline($value) */ public static function initials($value, $capitalize = false) { - $parts = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); + $parts = preg_split('/\s+/u', $value, -1, PREG_SPLIT_NO_EMPTY); $parts = array_map(fn ($part) => mb_substr($part, 0, 1), $parts); @@ -1499,7 +1499,7 @@ public static function apa($value) $endPunctuation = ['.', '!', '?', ':', '—', ',']; - $words = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); + $words = preg_split('/\s+/u', $value, -1, PREG_SPLIT_NO_EMPTY); $wordCount = count($words); for ($i = 0; $i < $wordCount; $i++) { @@ -1719,7 +1719,7 @@ public static function studly($value) return static::$studlyCache[$key]; } - $words = preg_split('/\s+/', static::replace(['-', '_'], ' ', $value), -1, PREG_SPLIT_NO_EMPTY); + $words = preg_split('/\s+/u', static::replace(['-', '_'], ' ', $value), -1, PREG_SPLIT_NO_EMPTY); $studlyWords = array_map(fn ($word) => static::ucfirst($word), $words); diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 9afdec875516..f71919ae56ef 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -90,6 +90,8 @@ public function testStringHeadline() $this->assertSame('Orwell 1984', Str::headline('orwell 1984')); $this->assertSame('Orwell 1984', Str::headline('-orwell-1984 -')); $this->assertSame('Orwell 1984', Str::headline(' orwell_- 1984 ')); + + $this->assertSame('Laravel Rocks!', Str::headline('laravel rocks!')); } public function testStringInitials() @@ -105,6 +107,9 @@ public function testStringInitials() $this->assertSame('JBLL', Str::initials('james bond loves laravel', true)); $this->assertSame('❤M☆', Str::initials('❤ MULTIByte ☆')); + + $this->assertSame('lr', Str::initials('laravel rocks!')); + $this->assertSame('LR', Str::initials('laravel rocks!', true)); } public function testStringApa() @@ -151,6 +156,10 @@ public function testStringApa() $this->assertSame('❤ Multibyte ☆', Str::apa('❤ MULTIByte ☆')); + $this->assertSame('Laravel Rocks!', Str::apa('Laravel Rocks!')); + $this->assertSame('Laravel Rocks!', Str::apa('Laravel rocks!')); + $this->assertSame('Laravel Rocks!', Str::apa('LARAVEL ROCKS!')); + $this->assertSame('', Str::apa('')); $this->assertSame(' ', Str::apa(' ')); } @@ -1161,6 +1170,8 @@ public function testStudly() $this->assertSame('ÖffentlicheÜberraschungen', Str::studly('öffentliche-überraschungen')); $this->assertSame('❤MultiByte☆', Str::studly('❤ multi-byte☆')); + + $this->assertSame('LaravelRocks!', Str::studly('laravel rocks!')); } public function testPascal() From fefc53a93fc0e5b0abcf082bd6fc868e6217ead9 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 10 May 2026 16:49:54 +0100 Subject: [PATCH 337/596] [13.x] Add name to MigrationStarted/MigrationEnded events (#60059) * default null * pass it thru * test --- .../Database/Events/MigrationEvent.php | 11 ++++++++++- src/Illuminate/Database/Migrations/Migrator.php | 12 ++++++------ .../Integration/Database/MigratorEventsTest.php | 16 ++++++++++++---- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Database/Events/MigrationEvent.php b/src/Illuminate/Database/Events/MigrationEvent.php index 83f10871a1d2..2481ebd18553 100644 --- a/src/Illuminate/Database/Events/MigrationEvent.php +++ b/src/Illuminate/Database/Events/MigrationEvent.php @@ -21,15 +21,24 @@ abstract class MigrationEvent implements MigrationEventContract */ public $method; + /** + * The migration name. + * + * @var string|null + */ + public $name; + /** * Create a new event instance. * * @param \Illuminate\Database\Migrations\Migration $migration * @param string $method + * @param string|null $name */ - public function __construct(Migration $migration, $method) + public function __construct(Migration $migration, $method, $name = null) { $this->method = $method; $this->migration = $migration; + $this->name = $name; } } diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 197799390c49..0a110d685471 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -250,7 +250,7 @@ protected function runUp($file, $batch, $pretend) $this->write(Task::class, $name, fn () => MigrationResult::Skipped->value); } else { - $this->write(Task::class, $name, fn () => $this->runMigration($migration, 'up')); + $this->write(Task::class, $name, fn () => $this->runMigration($migration, 'up', $name)); // Once we have run a migrations class, we will log that it was run in this // repository so that we don't try to run it next time we do a migration @@ -414,7 +414,7 @@ protected function runDown($file, $migration, $pretend) return $this->pretendToRun($instance, 'down'); } - $this->write(Task::class, $name, fn () => $this->runMigration($instance, 'down')); + $this->write(Task::class, $name, fn () => $this->runMigration($instance, 'down', $name)); // Once we have successfully run the migration "down" we will remove it from // the migration repository so it will be considered to have not been run @@ -429,19 +429,19 @@ protected function runDown($file, $migration, $pretend) * @param string $method * @return void */ - protected function runMigration($migration, $method) + protected function runMigration($migration, $method, $name = null) { $connection = $this->resolveConnection( $migration->getConnection() ); - $callback = function () use ($connection, $migration, $method) { + $callback = function () use ($connection, $migration, $method, $name) { if (method_exists($migration, $method)) { - $this->fireMigrationEvent(new MigrationStarted($migration, $method)); + $this->fireMigrationEvent(new MigrationStarted($migration, $method, $name)); $this->runMethod($connection, $migration, $method); - $this->fireMigrationEvent(new MigrationEnded($migration, $method)); + $this->fireMigrationEvent(new MigrationEnded($migration, $method, $name)); } }; diff --git a/tests/Integration/Database/MigratorEventsTest.php b/tests/Integration/Database/MigratorEventsTest.php index e4b1db0b34fe..cbfb45e405a0 100644 --- a/tests/Integration/Database/MigratorEventsTest.php +++ b/tests/Integration/Database/MigratorEventsTest.php @@ -112,16 +112,24 @@ public function testMigrationEventsContainTheMigrationAndMethod() }); Event::assertDispatched(MigrationStarted::class, function ($event) { - return $event->method === 'up' && $event->migration instanceof Migration; + return $event->method === 'up' + && $event->migration instanceof Migration + && $event->name === '2014_10_12_000000_create_members_table'; }); Event::assertDispatched(MigrationStarted::class, function ($event) { - return $event->method === 'down' && $event->migration instanceof Migration; + return $event->method === 'down' + && $event->migration instanceof Migration + && $event->name === '2014_10_12_000000_create_members_table'; }); Event::assertDispatched(MigrationEnded::class, function ($event) { - return $event->method === 'up' && $event->migration instanceof Migration; + return $event->method === 'up' + && $event->migration instanceof Migration + && $event->name === '2014_10_12_000000_create_members_table'; }); Event::assertDispatched(MigrationEnded::class, function ($event) { - return $event->method === 'down' && $event->migration instanceof Migration; + return $event->method === 'down' + && $event->migration instanceof Migration + && $event->name === '2014_10_12_000000_create_members_table'; }); } From db0c3ca727b5934789244ccc674dd7ae7528e678 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 11 May 2026 15:16:22 +0100 Subject: [PATCH 338/596] 13.x-custom-timedout-exit (#60072) --- src/Illuminate/Queue/Worker.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 1334ff637a66..74d3127bb35b 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -124,6 +124,13 @@ class Worker */ public static $memoryExceededExitCode; + /** + * The custom exit code to be used when a job times out. + * + * @var int|null + */ + public static $timedOutExitCode; + /** * Indicates if the worker should report job exceptions. * @@ -280,7 +287,7 @@ protected function registerTimeoutHandler($job, WorkerOptions $options) )); } - $this->kill(static::EXIT_ERROR, $options, WorkerStopReason::TimedOut); + $this->kill(static::$timedOutExitCode ?? static::EXIT_ERROR, $options, WorkerStopReason::TimedOut); }, true); pcntl_alarm( From 9a96d037a4fc32cfa4379329be984f1bc7a3d8ac Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 11 May 2026 15:20:34 +0100 Subject: [PATCH 339/596] Add method to convert a Password instance to a passwordrules string (#60070) --- src/Illuminate/Validation/Rules/Password.php | 33 +++++++++++++++++++ .../Validation/ValidationPasswordRuleTest.php | 20 +++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index e4c5eadbb3b2..4b4cfb83b168 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -431,6 +431,39 @@ public function appliedRules() ]; } + /** + * Convert the password rule to a passwordrules HTML attribute string. + * + * @return string + * + * @see https://developer.apple.com/password-rules/ + */ + public function toPasswordRulesString() + { + $rules = ['minlength: '.$this->min]; + + if ($this->max) { + $rules[] = 'maxlength: '.$this->max; + } + + if ($this->mixedCase) { + $rules[] = 'required: lower'; + $rules[] = 'required: upper'; + } elseif ($this->letters) { + $rules[] = 'required: lower'; + } + + if ($this->numbers) { + $rules[] = 'required: digit'; + } + + if ($this->symbols) { + $rules[] = 'required: special'; + } + + return implode('; ', $rules).';'; + } + /** * Get an iterator for the password validation rules. * diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 8a4486b1365e..00660ffb7a81 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -498,6 +498,26 @@ public function testItCanReturnsAsUnpackedArray() $this->assertSame(['sometimes', 'string', 'min:8'], [...Password::sometimes()]); } + public function testToPasswordRulesString() + { + $this->assertSame('minlength: 8;', Password::min(8)->toPasswordRulesString()); + + $this->assertSame('minlength: 8; maxlength: 64;', Password::min(8)->max(64)->toPasswordRulesString()); + + $this->assertSame('minlength: 8; required: lower; required: upper;', Password::min(8)->mixedCase()->toPasswordRulesString()); + + $this->assertSame('minlength: 8; required: lower;', Password::min(8)->letters()->toPasswordRulesString()); + + $this->assertSame('minlength: 8; required: digit;', Password::min(8)->numbers()->toPasswordRulesString()); + + $this->assertSame('minlength: 8; required: special;', Password::min(8)->symbols()->toPasswordRulesString()); + + $this->assertSame( + 'minlength: 12; maxlength: 64; required: lower; required: upper; required: digit; required: special;', + Password::min(12)->max(64)->mixedCase()->numbers()->symbols()->toPasswordRulesString() + ); + } + protected function passes($rule, $values) { $this->assertValidationRules($rule, $values, true, []); From 5e6264010f729d8a303692b81014c878270c2886 Mon Sep 17 00:00:00 2001 From: Devon Garbalosa <58236685+DGarbs51@users.noreply.github.com> Date: Mon, 11 May 2026 15:03:07 -0400 Subject: [PATCH 340/596] add index for database performance (#60073) --- src/Illuminate/Queue/Console/stubs/failed_jobs.stub | 6 ++++-- .../2022_02_21_000000_create_failed_jobs_table.php | 6 ++++-- .../Integration/Generators/QueueFailedTableCommandTest.php | 3 +++ tests/Queue/DatabaseFailedJobProviderTest.php | 6 ++++-- tests/Queue/DatabaseUuidFailedJobProviderTest.php | 6 ++++-- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Queue/Console/stubs/failed_jobs.stub b/src/Illuminate/Queue/Console/stubs/failed_jobs.stub index d81d903db991..f461c03a3a80 100644 --- a/src/Illuminate/Queue/Console/stubs/failed_jobs.stub +++ b/src/Illuminate/Queue/Console/stubs/failed_jobs.stub @@ -14,11 +14,13 @@ return new class extends Migration Schema::create('{{table}}', function (Blueprint $table) { $table->id(); $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); + $table->string('connection'); + $table->string('queue'); $table->longText('payload'); $table->longText('exception'); $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); }); } diff --git a/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php b/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php index c95b6f0e527d..90690083a507 100644 --- a/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php +++ b/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php @@ -22,11 +22,13 @@ public function up() { Schema::create('failed_jobs', function (Blueprint $table) { $table->id(); - $table->text('connection'); - $table->text('queue'); + $table->string('connection'); + $table->string('queue'); $table->longText('payload'); $table->longText('exception'); $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); }); } diff --git a/tests/Integration/Generators/QueueFailedTableCommandTest.php b/tests/Integration/Generators/QueueFailedTableCommandTest.php index c996da1d834a..b99382ebedd6 100644 --- a/tests/Integration/Generators/QueueFailedTableCommandTest.php +++ b/tests/Integration/Generators/QueueFailedTableCommandTest.php @@ -14,6 +14,9 @@ public function testCreateMakesMigration() 'use Illuminate\Database\Migrations\Migration;', 'return new class extends Migration', 'Schema::create(\'failed_jobs\', function (Blueprint $table) {', + '$table->string(\'connection\');', + '$table->string(\'queue\');', + '$table->index([\'connection\', \'queue\', \'failed_at\']);', 'Schema::dropIfExists(\'failed_jobs\');', ], 'create_failed_jobs_table.php'); } diff --git a/tests/Queue/DatabaseFailedJobProviderTest.php b/tests/Queue/DatabaseFailedJobProviderTest.php index e8a3afe65900..94e0c4c13ed5 100644 --- a/tests/Queue/DatabaseFailedJobProviderTest.php +++ b/tests/Queue/DatabaseFailedJobProviderTest.php @@ -203,11 +203,13 @@ protected function createDatabaseWithFailedJobTable() $this->db->getConnection()->getSchemaBuilder()->create('failed_jobs', function (Blueprint $table) { $table->id(); - $table->text('connection'); - $table->text('queue'); + $table->string('connection'); + $table->string('queue'); $table->longText('payload'); $table->longText('exception'); $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); }); return $this; diff --git a/tests/Queue/DatabaseUuidFailedJobProviderTest.php b/tests/Queue/DatabaseUuidFailedJobProviderTest.php index 51820fb5ae36..d07380953695 100644 --- a/tests/Queue/DatabaseUuidFailedJobProviderTest.php +++ b/tests/Queue/DatabaseUuidFailedJobProviderTest.php @@ -187,11 +187,13 @@ protected function getFailedJobProvider(string $database = 'default', string $ta ]); $db->getConnection()->getSchemaBuilder()->create('failed_jobs', function (Blueprint $table) { $table->uuid(); - $table->text('connection'); - $table->text('queue'); + $table->string('connection'); + $table->string('queue'); $table->longText('payload'); $table->longText('exception'); $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); }); return new DatabaseUuidFailedJobProvider($db->getDatabaseManager(), $database, $table); From 7fcb903b148a1008c37309a2276a4bd3cda96f92 Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Mon, 11 May 2026 18:06:33 -0400 Subject: [PATCH 341/596] [13.x] Add optional disk storage for large SQS queue payloads (#59734) * Set up SQS disk extended addition Signed-off-by: Kevin Ullyott * Add tests for the job changes Signed-off-by: Kevin Ullyott * Add tests for SqsJob raw body handling and cleanup behavior Signed-off-by: Kevin Ullyott * Remove unused import Signed-off-by: Kevin Ullyott * formatting * formatting --------- Signed-off-by: Kevin Ullyott Co-authored-by: Taylor Otwell --- config/queue.php | 6 + .../Queue/Connectors/SqsConnector.php | 5 +- src/Illuminate/Queue/Jobs/SqsJob.php | 70 ++++++++- src/Illuminate/Queue/SqsQueue.php | 70 ++++++++- tests/Queue/QueueSqsJobTest.php | 139 +++++++++++++++++ tests/Queue/QueueSqsQueueTest.php | 140 ++++++++++++++++++ 6 files changed, 426 insertions(+), 4 deletions(-) diff --git a/config/queue.php b/config/queue.php index 9d5e5895999a..737a2464cb7a 100644 --- a/config/queue.php +++ b/config/queue.php @@ -62,6 +62,12 @@ 'suffix' => env('SQS_SUFFIX'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'after_commit' => false, + 'overflow' => [ + 'enabled' => env('SQS_OVERFLOW_ENABLED', false), + 'store' => env('SQS_OVERFLOW_STORE'), + 'always' => false, + 'delete_after_processing' => true, + ], ], 'redis' => [ diff --git a/src/Illuminate/Queue/Connectors/SqsConnector.php b/src/Illuminate/Queue/Connectors/SqsConnector.php index 55f23add64ed..56d1cbc28a6d 100755 --- a/src/Illuminate/Queue/Connectors/SqsConnector.php +++ b/src/Illuminate/Queue/Connectors/SqsConnector.php @@ -32,12 +32,13 @@ public function connect(array $config) return new SqsQueue( new SqsClient( - Arr::except($config, ['token']) + Arr::except($config, ['token', 'overflow']) ), $config['queue'], $config['prefix'] ?? '', $config['suffix'] ?? '', - $config['after_commit'] ?? null + $config['after_commit'] ?? null, + $config['overflow'] ?? [], ); } diff --git a/src/Illuminate/Queue/Jobs/SqsJob.php b/src/Illuminate/Queue/Jobs/SqsJob.php index 227c1b7b0ac1..12493f5f61c3 100755 --- a/src/Illuminate/Queue/Jobs/SqsJob.php +++ b/src/Illuminate/Queue/Jobs/SqsJob.php @@ -5,6 +5,7 @@ use Aws\Sqs\SqsClient; use Illuminate\Container\Container; use Illuminate\Contracts\Queue\Job as JobContract; +use Illuminate\Support\Arr; class SqsJob extends Job implements JobContract { @@ -22,6 +23,20 @@ class SqsJob extends Job implements JobContract */ protected $job; + /** + * The overflow storage options for large payload offloading. + * + * @var array + */ + protected $overflowStorage = []; + + /** + * The cached raw body of the job. + * + * @var string|null + */ + protected $cachedRawBody = null; + /** * Create a new job instance. * @@ -30,14 +45,16 @@ class SqsJob extends Job implements JobContract * @param array $job * @param string $connectionName * @param string $queue + * @param array $overflowStorage */ - public function __construct(Container $container, SqsClient $sqs, array $job, $connectionName, $queue) + public function __construct(Container $container, SqsClient $sqs, array $job, $connectionName, $queue, array $overflowStorage = []) { $this->sqs = $sqs; $this->job = $job; $this->queue = $queue; $this->container = $container; $this->connectionName = $connectionName; + $this->overflowStorage = $overflowStorage; } /** @@ -69,6 +86,11 @@ public function delete() $this->sqs->deleteMessage([ 'QueueUrl' => $this->queue, 'ReceiptHandle' => $this->job['ReceiptHandle'], ]); + + if (Arr::get($this->overflowStorage, 'delete_after_processing') && + $pointer = $this->overflowPointer()) { + $this->overflowStore()->forget($pointer); + } } /** @@ -98,9 +120,55 @@ public function getJobId() */ public function getRawBody() { + if ($this->cachedRawBody !== null) { + return $this->cachedRawBody; + } + + if ($pointer = $this->overflowPointer()) { + return $this->cachedRawBody = $this->overflowStore()->get($pointer); + } + return $this->job['Body']; } + /** + * Resolve the pointer path from the job body, if present. + * + * @return string|null + */ + protected function overflowPointer() + { + if (! Arr::get($this->overflowStorage, 'enabled', false)) { + return null; + } + + $body = $this->job['Body'] ?? null; + + if (! is_string($body) || $body === '') { + return null; + } + + $decoded = json_decode($body, true); + + if (! is_array($decoded) || ! isset($decoded['@pointer'])) { + return null; + } + + return is_string($decoded['@pointer']) ? $decoded['@pointer'] : null; + } + + /** + * Resolve the configured cache store for extended storage. + * + * @return \Illuminate\Contracts\Cache\Repository + */ + protected function overflowStore() + { + return $this->container->make('cache')->store( + Arr::get($this->overflowStorage, 'store') + ); + } + /** * Get the underlying SQS client instance. * diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index 7c7619c4b055..193235a75bd9 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -6,11 +6,26 @@ use Illuminate\Contracts\Queue\ClearableQueue; use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Jobs\SqsJob; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; class SqsQueue extends Queue implements QueueContract, ClearableQueue { + /** + * The maximum SQS payload size in bytes (1 MB). + * + * @var int + */ + const MAX_SQS_PAYLOAD_SIZE = 1048576; + + /** + * The cache key prefix for extended SQS payloads. + * + * @var string + */ + const EXTENDED_PAYLOAD_CACHE_PREFIX = 'laravel:sqs-payloads:'; + /** * The Amazon SQS instance. * @@ -39,6 +54,13 @@ class SqsQueue extends Queue implements QueueContract, ClearableQueue */ protected $suffix; + /** + * The overflow storage options for large payload offloading. + * + * @var array + */ + protected $overflowStorage = []; + /** * Create a new Amazon SQS queue instance. * @@ -47,6 +69,7 @@ class SqsQueue extends Queue implements QueueContract, ClearableQueue * @param string $prefix * @param string $suffix * @param bool $dispatchAfterCommit + * @param array $overflowStorage */ public function __construct( SqsClient $sqs, @@ -54,12 +77,14 @@ public function __construct( $prefix = '', $suffix = '', $dispatchAfterCommit = false, + array $overflowStorage = [], ) { $this->sqs = $sqs; $this->prefix = $prefix; $this->default = $default; $this->suffix = $suffix; $this->dispatchAfterCommit = $dispatchAfterCommit; + $this->overflowStorage = $overflowStorage; } /** @@ -242,6 +267,10 @@ function ($payload, $queue) use ($job) { */ public function pushRaw($payload, $queue = null, array $options = []) { + if ($this->willOverflow($payload)) { + $payload = $this->overflow($payload); + } + return $this->sqs->sendMessage([ 'QueueUrl' => $this->getQueue($queue), 'MessageBody' => $payload, ...$options, ])->get('MessageId'); @@ -350,6 +379,45 @@ public function bulk($jobs, $data = '', $queue = null) } } + /** + * Determine if the payload should be stored in cache. + * + * @param string $payload + * @return bool + */ + protected function willOverflow($payload) + { + if (! Arr::get($this->overflowStorage, 'enabled', false)) { + return false; + } + + return Arr::get($this->overflowStorage, 'always', false) + || strlen($payload) >= static::MAX_SQS_PAYLOAD_SIZE; + } + + /** + * Store the payload in cache and return a pointer payload. + * + * @param string $payload + * @return string + */ + protected function overflow($payload) + { + $decoded = json_decode($payload); + + $uuid = is_object($decoded) && isset($decoded->uuid) + ? $decoded->uuid + : (string) Str::uuid(); + + $this->container->make('cache')->store( + Arr::get($this->overflowStorage, 'store') + )->put( + $path = static::EXTENDED_PAYLOAD_CACHE_PREFIX.$uuid, $payload + ); + + return json_encode(['@pointer' => $path]); + } + /** * Pop the next job off of the queue. * @@ -366,7 +434,7 @@ public function pop($queue = null) if (! is_null($response['Messages']) && count($response['Messages']) > 0) { return new SqsJob( $this->container, $this->sqs, $response['Messages'][0], - $this->connectionName, $queue + $this->connectionName, $queue, $this->overflowStorage ); } } diff --git a/tests/Queue/QueueSqsJobTest.php b/tests/Queue/QueueSqsJobTest.php index b6e4e845f17e..0f0f2bd8c7a2 100644 --- a/tests/Queue/QueueSqsJobTest.php +++ b/tests/Queue/QueueSqsJobTest.php @@ -4,6 +4,8 @@ use Aws\Sqs\SqsClient; use Illuminate\Container\Container; +use Illuminate\Contracts\Cache\Factory as CacheFactory; +use Illuminate\Contracts\Cache\Repository as CacheRepository; use Illuminate\Queue\Jobs\SqsJob; use Illuminate\Queue\SqsQueue; use Mockery as m; @@ -94,6 +96,143 @@ public function testReleaseProperlyReleasesTheJobOntoSqs() $this->assertTrue($job->isReleased()); } + public function testGetRawBodyResolvesPointerFromCache() + { + $fullPayload = json_encode(['job' => 'foo', 'data' => ['key' => 'value']]); + $pointerPath = 'laravel:sqs-payloads:some-uuid'; + $pointerBody = json_encode(['@pointer' => $pointerPath]); + + $store = m::mock(CacheRepository::class); + $store->shouldReceive('get')->once()->with($pointerPath)->andReturn($fullPayload); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->with('database')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('cache')->andReturn($cache); + + $jobData = $this->mockedJobData; + $jobData['Body'] = $pointerBody; + + $job = new SqsJob($container, $this->mockedSqsClient, $jobData, 'connection-name', $this->queueUrl, [ + 'enabled' => true, + 'store' => 'database', + 'delete_after_processing' => true, + ]); + + $this->assertEquals($fullPayload, $job->getRawBody()); + } + + public function testGetRawBodyReturnsNormalBodyWithoutPointer() + { + $job = $this->getJob(); + $this->assertEquals($this->mockedPayload, $job->getRawBody()); + } + + public function testGetRawBodyReturnsPointerBodyWhenExtendedStoreIsDisabled() + { + $pointerBody = json_encode(['@pointer' => 'laravel:sqs-payloads:some-uuid']); + + $jobData = $this->mockedJobData; + $jobData['Body'] = $pointerBody; + + $job = new SqsJob($this->mockedContainer, $this->mockedSqsClient, $jobData, 'connection-name', $this->queueUrl); + + $this->assertEquals($pointerBody, $job->getRawBody()); + } + + public function testGetRawBodyCachesResult() + { + $fullPayload = json_encode(['job' => 'foo', 'data' => ['key' => 'value']]); + $pointerPath = 'laravel:sqs-payloads:some-uuid'; + $pointerBody = json_encode(['@pointer' => $pointerPath]); + + $store = m::mock(CacheRepository::class); + $store->shouldReceive('get')->once()->with($pointerPath)->andReturn($fullPayload); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->with('database')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('cache')->andReturn($cache); + + $jobData = $this->mockedJobData; + $jobData['Body'] = $pointerBody; + + $job = new SqsJob($container, $this->mockedSqsClient, $jobData, 'connection-name', $this->queueUrl, [ + 'enabled' => true, + 'store' => 'database', + 'delete_after_processing' => true, + ]); + + // Call twice; cache should only be hit once. + $job->getRawBody(); + $this->assertEquals($fullPayload, $job->getRawBody()); + } + + public function testDeleteCleansUpCacheKeyWhenCleanupEnabled() + { + $pointerPath = 'laravel:sqs-payloads:some-uuid'; + $pointerBody = json_encode(['@pointer' => $pointerPath]); + + $store = m::mock(CacheRepository::class); + $store->shouldReceive('forget')->once()->with($pointerPath); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->with('database')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('cache')->andReturn($cache); + + $jobData = $this->mockedJobData; + $jobData['Body'] = $pointerBody; + + $sqsClient = m::mock(SqsClient::class)->makePartial(); + $sqsClient->shouldReceive('deleteMessage')->once(); + + $job = new SqsJob($container, $sqsClient, $jobData, 'connection-name', $this->queueUrl, [ + 'enabled' => true, + 'store' => 'database', + 'delete_after_processing' => true, + ]); + + $job->delete(); + } + + public function testDeleteDoesNotCleanUpWhenCleanupDisabled() + { + $pointerPath = 'laravel:sqs-payloads:some-uuid'; + $pointerBody = json_encode(['@pointer' => $pointerPath]); + + $jobData = $this->mockedJobData; + $jobData['Body'] = $pointerBody; + + $sqsClient = m::mock(SqsClient::class)->makePartial(); + $sqsClient->shouldReceive('deleteMessage')->once(); + + $job = new SqsJob($this->mockedContainer, $sqsClient, $jobData, 'connection-name', $this->queueUrl, [ + 'enabled' => true, + 'store' => 'database', + 'delete_after_processing' => false, + ]); + + $job->delete(); + } + + public function testDeleteDoesNotCleanUpWhenNoPointer() + { + $sqsClient = m::mock(SqsClient::class)->makePartial(); + $sqsClient->shouldReceive('deleteMessage')->once(); + + $job = new SqsJob($this->mockedContainer, $sqsClient, $this->mockedJobData, 'connection-name', $this->queueUrl, [ + 'enabled' => true, + 'store' => 'database', + 'delete_after_processing' => true, + ]); + + $job->delete(); + } + protected function getJob() { return new SqsJob( diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 842537eb8f12..8917b3a5aac8 100755 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -7,6 +7,8 @@ use Illuminate\Bus\Dispatcher; use Illuminate\Container\Container; use Illuminate\Contracts\Bus\Dispatcher as DispatcherContract; +use Illuminate\Contracts\Cache\Factory as CacheFactory; +use Illuminate\Contracts\Cache\Repository as CacheRepository; use Illuminate\Queue\Jobs\SqsJob; use Illuminate\Queue\QueueRoutes; use Illuminate\Queue\SqsQueue; @@ -676,4 +678,142 @@ public function testDelayedPendingDispatchProperlyPushesJobObjectOntoSqsFifoQueu Str::createUuidsNormally(); } + + public function testPushRawStoresPayloadToCacheWhenExceedingThreshold() + { + $uuid = 'test-uuid-1234'; + $largePayload = json_encode(['uuid' => $uuid, 'job' => 'App\\Jobs\\TestJob', 'data' => str_repeat('x', SqsQueue::MAX_SQS_PAYLOAD_SIZE)]); + $expectedPath = 'laravel:sqs-payloads:'.$uuid; + $expectedPointer = json_encode(['@pointer' => $expectedPath]); + + $store = m::mock(CacheRepository::class); + $store->shouldReceive('put')->once()->with($expectedPath, $largePayload); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->with('database')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('cache')->andReturn($cache); + + $queue = new SqsQueue($this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => true, + 'store' => 'database', + 'always' => false, + 'delete_after_processing' => true, + ]); + $queue->setContainer($container); + + $this->sqs->shouldReceive('sendMessage')->once()->withArgs(function ($args) use ($expectedPointer) { + return $args['MessageBody'] === $expectedPointer; + })->andReturn($this->mockedSendMessageResponseModel); + + $queue->pushRaw($largePayload, $this->queueName); + } + + public function testPushRawDoesNotStoreToCacheWhenBelowThreshold() + { + $smallPayload = json_encode(['uuid' => 'test-uuid', 'job' => 'App\\Jobs\\TestJob', 'data' => 'small']); + + $queue = new SqsQueue($this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => true, + 'store' => 'database', + 'always' => false, + 'delete_after_processing' => true, + ]); + $queue->setContainer(m::mock(Container::class)); + + $this->sqs->shouldReceive('sendMessage')->once()->withArgs(function ($args) use ($smallPayload) { + return $args['MessageBody'] === $smallPayload; + })->andReturn($this->mockedSendMessageResponseModel); + + $queue->pushRaw($smallPayload, $this->queueName); + } + + public function testPushRawAlwaysStoresToCacheWhenAlwaysIsTrue() + { + $uuid = 'test-uuid-always'; + $smallPayload = json_encode(['uuid' => $uuid, 'job' => 'App\\Jobs\\TestJob', 'data' => 'small']); + $expectedPath = 'laravel:sqs-payloads:'.$uuid; + $expectedPointer = json_encode(['@pointer' => $expectedPath]); + + $store = m::mock(CacheRepository::class); + $store->shouldReceive('put')->once()->with($expectedPath, $smallPayload); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->with('database')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('cache')->andReturn($cache); + + $queue = new SqsQueue($this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => true, + 'store' => 'database', + 'always' => true, + 'delete_after_processing' => true, + ]); + $queue->setContainer($container); + + $this->sqs->shouldReceive('sendMessage')->once()->withArgs(function ($args) use ($expectedPointer) { + return $args['MessageBody'] === $expectedPointer; + })->andReturn($this->mockedSendMessageResponseModel); + + $queue->pushRaw($smallPayload, $this->queueName); + } + + public function testPushRawDoesNotStoreToCacheWhenNotEnabled() + { + $largePayload = json_encode(['uuid' => 'test-uuid', 'job' => 'App\\Jobs\\TestJob', 'data' => str_repeat('x', SqsQueue::MAX_SQS_PAYLOAD_SIZE)]); + + $queue = new SqsQueue($this->sqs, $this->queueName, $this->prefix); + $queue->setContainer(m::mock(Container::class)); + + $this->sqs->shouldReceive('sendMessage')->once()->withArgs(function ($args) use ($largePayload) { + return $args['MessageBody'] === $largePayload; + })->andReturn($this->mockedSendMessageResponseModel); + + $queue->pushRaw($largePayload, $this->queueName); + } + + public function testClearDoesNotFlushCacheStore() + { + $queue = $this->getMockBuilder(SqsQueue::class) + ->onlyMethods(['getQueue', 'size']) + ->setConstructorArgs([$this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => true, + 'store' => 'database', + 'always' => false, + 'delete_after_processing' => true, + ]]) + ->getMock(); + $queue->setContainer(m::mock(Container::class)); + $queue->expects($this->once())->method('getQueue')->willReturn($this->queueUrl); + $queue->expects($this->once())->method('size')->willReturn(5); + + $this->sqs->shouldReceive('purgeQueue')->once(); + + $queue->clear($this->queueName); + } + + public function testPopPassesOverflowStorageOptionsToJob() + { + $overflowStorage = [ + 'enabled' => true, + 'store' => 'database', + 'always' => false, + 'delete_after_processing' => true, + ]; + + $queue = $this->getMockBuilder(SqsQueue::class) + ->onlyMethods(['getQueue']) + ->setConstructorArgs([$this->sqs, $this->queueName, $this->account, '', false, $overflowStorage]) + ->getMock(); + $queue->setContainer(m::mock(Container::class)); + $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); + + $this->sqs->shouldReceive('receiveMessage')->once()->andReturn($this->mockedReceiveMessageResponseModel); + + $job = $queue->pop($this->queueName); + + $this->assertInstanceOf(SqsJob::class, $job); + } } From 2635a91837cb86513ba6bf3736662d21531a6381 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 12 May 2026 14:10:11 +1000 Subject: [PATCH 342/596] [13.x] Cloud queue metrics (#60074) * Cloud queue metrics * Reset static state before running tests * Add binding test * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Cloud.php | 72 +- src/Illuminate/Foundation/Cloud/Events.php | 217 ++++++ .../Foundation/Cloud/FailedJobProvider.php | 202 ++++++ src/Illuminate/Foundation/Cloud/Queue.php | 441 ++++++++++++ .../Foundation/Cloud/QueueConnector.php | 76 ++ tests/Foundation/Cloud/QueueTest.php | 662 ++++++++++++++++++ .../Foundation/FoundationAliasLoaderTest.php | 8 + tests/Integration/Foundation/CloudTest.php | 68 -- 8 files changed, 1661 insertions(+), 85 deletions(-) create mode 100644 src/Illuminate/Foundation/Cloud/Events.php create mode 100644 src/Illuminate/Foundation/Cloud/FailedJobProvider.php create mode 100644 src/Illuminate/Foundation/Cloud/Queue.php create mode 100644 src/Illuminate/Foundation/Cloud/QueueConnector.php create mode 100644 tests/Foundation/Cloud/QueueTest.php diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 95db25d590ba..1067de3757d2 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -3,9 +3,13 @@ namespace Illuminate\Foundation; use Illuminate\Database\Migrations\Migrator; +use Illuminate\Foundation\Bootstrap\BootProviders; use Illuminate\Foundation\Bootstrap\HandleExceptions; use Illuminate\Foundation\Bootstrap\LoadConfiguration; -use Illuminate\Queue\Worker; +use Illuminate\Foundation\Cloud\Events; +use Illuminate\Foundation\Cloud\FailedJobProvider; +use Illuminate\Foundation\Cloud\QueueConnector; +use Illuminate\Queue\Connectors\SqsConnector; use Monolog\Formatter\JsonFormatter; use Monolog\Handler\SocketHandler; use PDO; @@ -35,6 +39,9 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); }, + BootProviders::class => function () use ($app) { + static::bootManagedQueues($app); + }, default => fn () => true, })(); } @@ -127,24 +134,39 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): */ public static function configureManagedQueues(Application $app): void { - if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { - Worker::$restartable = false; - Worker::$pausable = false; + if (! Cloud::managedQueuesAreActive()) { + return; + } - $app['config']->set( - 'queue.connections.sqs.credentials', - 'ecs' - ); + $app['config']->set('queue.connections.sqs.credentials', 'ecs'); - if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { - $app['config']->set( - 'queue.connections.sqs.region', - $_SERVER['LARAVEL_CLOUD_REGION'] - ); - } + if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { + $app['config']->set('queue.connections.sqs.region', $_SERVER['LARAVEL_CLOUD_REGION']); } } + /** + * Boot managed queues if applicable. + */ + public static function bootManagedQueues(Application $app): void + { + if (! Cloud::managedQueuesAreActive()) { + return; + } + + $app->singleton(Events::class, fn () => new Events(Cloud::socket())); + $app->bind(QueueConnector::class, fn ($app) => new QueueConnector(new SqsConnector, $app)); + + $app['queue']->addConnector('sqs', $app->factory(QueueConnector::class)); + + $failer = $app['queue.failer']; + unset($app['queue.failer']); + + $app->singleton('queue.failer', fn ($app) => new FailedJobProvider( + $failer, $app[Events::class], $app['encrypter'], + )); + } + /** * Configure the Laravel Cloud log channels. */ @@ -163,11 +185,27 @@ public static function configureCloudLogging(Application $app): void 'includeStacktraces' => true, ], 'with' => [ - 'connectionString' => $_ENV['LARAVEL_CLOUD_LOG_SOCKET'] ?? - $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? - 'unix:///tmp/cloud-init.sock', + 'connectionString' => Cloud::socket(), 'persistent' => true, ], ]); } + + /** + * The cloud socket address. + */ + protected static function socket(): string + { + return $_ENV['LARAVEL_CLOUD_LOG_SOCKET'] ?? + $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? + 'unix:///tmp/cloud-init.sock'; + } + + /** + * Determine if managed queues are active. + */ + protected static function managedQueuesAreActive(): bool + { + return ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? null) === '1'; + } } diff --git a/src/Illuminate/Foundation/Cloud/Events.php b/src/Illuminate/Foundation/Cloud/Events.php new file mode 100644 index 000000000000..2d90ccad5b11 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/Events.php @@ -0,0 +1,217 @@ + $payload + */ + public function emit(array $payload): void + { + $this->emitMany([$payload]); + } + + /** + * Emit many events. + * + * @param list> $payloads + */ + public function emitMany(array $payloads): void + { + if ($payloads === []) { + return; + } + + try { + $this->ensureConnected(); + + $this->write($this->format($payloads)); + } catch (Throwable) { + // + } + } + + /** + * Write the payload to the socket. + * + * @param list> $payloads + */ + protected function write(string $payload): void + { + $originalPayloadLength = strlen($payload); + $written = 0; + $zeroLengthWrites = 0; + + while (true) { + $thisWrite = @fwrite($this->socket, $payload); + + if ($thisWrite === false) { + $e = new RuntimeException($this->withSocketMetaData('Unable to write to socket')); + + $this->disconnect(); + + throw $e; + } + + $written += $thisWrite; + + if ($written >= $originalPayloadLength) { + return; + } + + if ($thisWrite === 0) { + $zeroLengthWrites++; + } + + if ($zeroLengthWrites >= 5) { + $e = new RuntimeException($this->withSocketMetaData('Unable to write bytes to socket')); + + $this->disconnect(); + + throw $e; + } + + $payload = substr($payload, $thisWrite); + } + } + + /** + * Format the payload. + * + * @param list> $payloads + */ + protected function format(array $payloads): string + { + return array_reduce($payloads, function (string $carry, array $line) { + if ($carry !== '') { + $carry .= "\n"; + } + + return $carry .= json_encode($line, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE); + }, '')."\n"; + } + + /** + * Ensure the socket is connected. + */ + protected function ensureConnected(): void + { + if (! $this->connected()) { + $this->connect(); + } + } + + /** + * Connect the socket. + */ + protected function connect(): void + { + $socket = stream_socket_client( + address: $this->address, + error_code: $errorCode, + error_message: $errorMessage, + timeout: 2, + flags: STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, + ); + + if ($socket === false) { + throw new RuntimeException("Failed connecting to the socket: {$errorMessage} [{$errorCode}]"); + } + + if (! stream_set_timeout($socket, 2)) { + $e = new RuntimeException($this->withSocketMetaData('Failed configuring socket timeout')); + + $this->disconnect(); + + throw $e; + } + + $this->socket = $socket; + } + + /** + * Determine if the socket is connected. + */ + protected function connected(): bool + { + if (gettype($this->socket) !== 'resource') { + return false; + } + + if (feof($this->socket)) { + $this->disconnect(); + + return false; + } + + return true; + } + + /** + * Disconnect the socket. + */ + protected function disconnect(): void + { + if (gettype($this->socket) !== 'resource') { + $this->socket = null; + + return; + } + + try { + fclose($this->socket); + } catch (Throwable) { + // + } + + $this->socket = null; + } + + /** + * Decorate the message with the socket's meta data. + */ + protected function withSocketMetaData(string $message): string + { + $prefix = "{$message}\n---\n"; + + if (! $this->connected()) { + return "{$prefix}closed: true"; + } + + $meta = stream_get_meta_data($this->socket); + + return $prefix.array_reduce(array_keys($meta), function ($carry, $key) use ($meta) { + try { + return $carry.$key.': '.match ($meta[$key]) { + true => 'true', + false => 'false', + default => $meta[$key], + }."\n"; + } catch (Throwable) { + return $carry; + } + }, ''); + } +} diff --git a/src/Illuminate/Foundation/Cloud/FailedJobProvider.php b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php new file mode 100644 index 000000000000..9c0dd8dad795 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php @@ -0,0 +1,202 @@ + + */ + protected $loadedFailedJobs = []; + + /** + * Create a new instance. + */ + public function __construct( + protected FailedJobProviderInterface $failer, + protected Events $events, + protected StringEncrypter $encrypter, + ) { + // + } + + /** + * Log a failed job into storage. + * + * @param string $connection + * @param string $queue + * @param string $payload + * @param \Throwable $exception + * @return string|null + */ + public function log($connection, $queue, $payload, $exception) + { + if ($connection !== 'sqs') { + return $this->failer->log(...func_get_args()); + } + + if ($this->queue === null) { + throw new RuntimeException('The failed job provider does not have a configured queue'); + } + + $timestamp = CarbonImmutable::now('UTC'); + $processingJobDetails = $this->queue->processingJobDetails(); + + $this->events->emit([ + '_cloud_event' => 'failed_job', + 'id' => $id = Str::uuid7($timestamp)->toString(), + 'queue' => $processingJobDetails['queue'], + 'started_at' => $processingJobDetails['started_at']->toDateTimeString('microsecond'), + 'attempts' => $processingJobDetails['attempts'], + 'payload' => $payload, + 'exception' => (string) mb_convert_encoding($exception, 'UTF-8'), + ]); + + $this->queue->finishProcessingJob(timestamp: $timestamp); + + return $id; + } + + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return $this->failer->ids(...func_get_args()); + } + + /** + * Get a list of all of the failed jobs. + * + * @return array + */ + public function all() + { + return $this->failer->all(...func_get_args()); + } + + /** + * Get a single failed job. + * + * @param mixed $id + * @return object|null + */ + public function find($id) + { + if (! str_starts_with($id, 'https://')) { + return $this->failer->find($id); + } + + $response = Http::connectTimeout(10) + ->timeout(10) + ->retry(3, 1000, fn ($exception) => $exception instanceof ConnectionException) + ->throw() + ->get($id); + + return $this->loadedFailedJobs[$id] = json_decode($this->encrypter->decryptString($response->body()), flags: JSON_THROW_ON_ERROR); + } + + /** + * Delete a single failed job from storage. + * + * @param mixed $id + * @return bool + */ + public function forget($id) + { + if (! str_starts_with($id, 'https://')) { + return $this->failer->forget($id); + } + + if (is_null($job = $this->loadedFailedJobs[$id] ?? null)) { + return false; + } + + $this->events->emit([ + '_cloud_event' => 'failed_job', + 'id' => $job->id, + 'queue' => $job->queue, + 'retried_at' => CarbonImmutable::now('UTC')->toDateTimeString('microsecond'), + ]); + + return true; + } + + /** + * Flush all of the failed jobs from storage. + * + * @param int|null $hours + * @return void + */ + public function flush($hours = null) + { + $this->failer->flush(...func_get_args()); + } + + /** + * Count the failed jobs. + * + * @param string|null $connection + * @param string|null $queue + * @return int + */ + public function count($connection = null, $queue = null) + { + if (! $this->failer instanceof CountableFailedJobProvider) { + return 0; + } + + return $this->failer->count(...func_get_args()); + } + + /** + * Prune all of the entries older than the given date. + * + * @param \DateTimeInterface $before + * @return int + */ + public function prune(DateTimeInterface $before) + { + if (! $this->failer instanceof PrunableFailedJobProvider) { + return 0; + } + + return $this->failer->prune(...func_get_args()); + } + + /** + * Set the connected queue instance. + * + * @param \Illuminate\Foundation\Cloud\Queue $queue + * @return $this + */ + public function setQueue($queue) + { + $this->queue = $queue; + + return $this; + } +} diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php new file mode 100644 index 000000000000..b6a723be3877 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -0,0 +1,441 @@ +queue->size(...func_get_args()); + } + + /** + * Get the number of pending jobs. + * + * @param string|null $queue + * @return int + */ + public function pendingSize($queue = null) + { + return $this->queue->pendingSize(...func_get_args()); + } + + /** + * Get the number of delayed jobs. + * + * @param string|null $queue + * @return int + */ + public function delayedSize($queue = null) + { + return $this->queue->delayedSize(...func_get_args()); + } + + /** + * Get the number of reserved jobs. + * + * @param string|null $queue + * @return int + */ + public function reservedSize($queue = null) + { + return $this->queue->reservedSize(...func_get_args()); + } + + /** + * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * + * @param string|null $queue + * @return int|null + */ + public function creationTimeOfOldestPendingJob($queue = null) + { + return $this->queue->creationTimeOfOldestPendingJob(...func_get_args()); + } + + /** + * Push a new job onto the queue. + * + * @param string|object $job + * @param mixed $data + * @param string|null $queue + * @return mixed + */ + public function push($job, $data = '', $queue = null) + { + $this->beforeJobPushed(); + + $result = $this->queue->push(...func_get_args()); + + $this->afterJobPushed($queue); + + return $result; + } + + /** + * Push a new job onto the queue. + * + * @param string $queue + * @param string|object $job + * @param mixed $data + * @return mixed + */ + public function pushOn($queue, $job, $data = '') + { + $this->beforeJobPushed(); + + $result = $this->queue->pushOn(...func_get_args()); + + $this->afterJobPushed($queue); + + return $result; + } + + /** + * Push a raw payload onto the queue. + * + * @param string $payload + * @param string|null $queue + * @return mixed + */ + public function pushRaw($payload, $queue = null, array $options = []) + { + $this->beforeJobPushed(); + + $result = $this->queue->pushRaw(...func_get_args()); + + $this->afterJobPushed($queue); + + return $result; + } + + /** + * Push a new job onto the queue after (n) seconds. + * + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string|null $queue + * @return mixed + */ + public function later($delay, $job, $data = '', $queue = null) + { + $this->beforeJobPushed(); + + $result = $this->queue->later(...func_get_args()); + + $this->afterJobPushed($queue); + + return $result; + } + + /** + * Push a new job onto a specific queue after (n) seconds. + * + * @param string $queue + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @return mixed + */ + public function laterOn($queue, $delay, $job, $data = '') + { + $this->beforeJobPushed(); + + $result = $this->queue->laterOn(...func_get_args()); + + $this->afterJobPushed($queue); + + return $result; + } + + /** + * Push an array of jobs onto the queue. + * + * @param array $jobs + * @param mixed $data + * @param string|null $queue + * @return mixed + */ + public function bulk($jobs, $data = '', $queue = null) + { + $this->beforeJobPushed(); + + $result = $this->queue->bulk(...func_get_args()); + + $this->afterJobsPushed(count($jobs), $queue); + + return $result; + } + + /** + * Pop the next job off of the queue. + * + * @param string|null $queue + * @return \Illuminate\Contracts\Queue\Job|null + */ + public function pop($queue = null) + { + $this->finishProcessingJob(); + + $job = $this->queue->pop(...func_get_args()); + + $this->startProcessingJob($queue, $job); + + return $job; + } + + /** + * Delete all of the jobs from the queue. + * + * @param string $queue + * @return int + */ + public function clear($queue) + { + return $this->queue->clear(...func_get_args()); + } + + /** + * Get the connection name for the queue. + * + * @return string + */ + public function getConnectionName() + { + return $this->queue->getConnectionName(); + } + + /** + * Set the connection name for the queue. + * + * @param string $name + * @return $this + */ + public function setConnectionName($name) + { + $this->queue->setConnectionName(...func_get_args()); + + return $this; + } + + /** + * Set the queue configuration array. + * + * @param array $config + * @return $this + */ + public function setConfig($config) + { + $this->queue->setConfig(...func_get_args()); + + return $this; + } + + /** + * Get the queueable options from the job. + * + * @param mixed $job + * @param string|null $queue + * @param string $payload + * @param \DateTimeInterface|\DateInterval|int|null $delay + * @return array{DelaySeconds?: int, MessageGroupId?: string, MessageDeduplicationId?: string} + */ + public function getQueueableOptions($job, $queue, $payload, $delay = null): array + { + if (! method_exists($this->queue, 'getQueueableOptions')) { + return []; + } + + return $this->queue->getQueueableOptions(...func_get_args()); + } + + /** + * Finish processing the current job and emit a queue event. + * + * @param string $default + * @param \Carbon\CarbonImmutable|null $timestamp + * @return void + */ + public function finishProcessingJob($default = 'processed', $timestamp = null) + { + if (! $this->processingJob) { + return; + } + + $timestamp ??= CarbonImmutable::now('UTC'); + + $this->events->emit([ + '_cloud_event' => 'queue', + 'timestamp' => $timestamp->toDateTimeString('microsecond'), + 'type' => match (true) { + $this->processingJob->hasFailed() => 'failed', + $this->processingJob->isReleased() => 'released', + default => $default, + }, + 'queue' => $this->processingQueue, + 'duration_ms' => (int) $this->processingJobStartedAt->diffInMilliseconds($timestamp), + ]); + + $this->processingQueue + = $this->processingJob + = $this->processingJobStartedAt + = null; + } + + /** + * Last job details resolver. + * + * @return array{queue: string, attempts: int, started_at: CarbonImmutable} + */ + public function processingJobDetails() + { + return [ + 'queue' => $this->processingQueue, + 'attempts' => $this->processingJob->attempts(), + 'started_at' => $this->processingJobStartedAt, + ]; + } + + /** + * Handle before a job is pushed. + * + * @return void + */ + protected function beforeJobPushed() + { + $this->lastJobPushedAt = CarbonImmutable::now('UTC'); + } + + /** + * Handle after a job is pushed. + * + * @param string|null $queue + * @return void + */ + protected function afterJobPushed($queue) + { + $this->afterJobsPushed(1, $queue); + } + + /** + * Handle jobs being pushed. + * + * @param int $count + * @param string|null $queue + */ + protected function afterJobsPushed($count, $queue) + { + $this->events->emitMany(array_fill(0, $count, [ + '_cloud_event' => 'queue', + 'timestamp' => $this->lastJobPushedAt->toDateTimeString('microsecond'), + 'type' => 'queued', + 'queue' => $this->normalizeQueue($queue), + ])); + + $this->lastJobPushedAt = null; + } + + /** + * Handle a job being popped. + * + * @param string|null $queue + * @param \Illuminate\Contracts\Queue\Job|null $job + * @return void + */ + protected function startProcessingJob($queue, $job) + { + if (! $job) { + return; + } + + $this->processingJob = $job; + $this->processingQueue = $this->normalizeQueue($queue); + $this->processingJobStartedAt = CarbonImmutable::now('UTC'); + + $this->events->emit([ + '_cloud_event' => 'queue', + 'timestamp' => $this->processingJobStartedAt->toDateTimeString('microsecond'), + 'type' => 'started', + 'queue' => $this->processingQueue, + ]); + } + + /** + * Normalize the queue name. + * + * @param string|null $queue + * @return string + */ + protected function normalizeQueue($queue) + { + return Str::of($this->queue->getQueue($queue)) + ->chopStart($_SERVER['SQS_PREFIX'].'/') + ->chopEnd($_SERVER['SQS_SUFFIX']) + ->toString(); + } + + /** + * Dynamically pass method calls to the underlying queue. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->forwardDecoratedCallTo($this->queue, $method, $parameters); + } +} diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php new file mode 100644 index 000000000000..99386d202e67 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -0,0 +1,76 @@ +connector->connect($config), $this->app[Events::class]); + + if (! $this->app->runningConsoleCommand('queue:work')) { + return $queue; + } + + $this->configureWorker($queue); + $this->configureFailedJobProvider($queue); + + return $queue; + } + + /** + * Configure the queue worker. + */ + protected function configureWorker(Queue $queue): void + { + Worker::$restartable = false; + Worker::$pausable = false; + + $this->app['events']->listen(fn (WorkerStopping $event) => match ($event->reason) { + WorkerStopReason::TimedOut => $queue->finishProcessingJob(default: 'released'), + default => $queue->finishProcessingJob(), + }); + + static::$reservedMemory = str_repeat('x', 32768); + + register_shutdown_function(function () use ($queue) { + static::$reservedMemory = null; + + if (! is_null($error = error_get_last()) && in_array($error['type'], [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE])) { + $queue->finishProcessingJob(default: 'released'); + } + }); + } + + /** + * Configure the failed job provider. + */ + protected function configureFailedJobProvider(Queue $queue): void + { + $this->app['queue.failer']->setQueue($queue); + } +} diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php new file mode 100644 index 000000000000..1788780b65cc --- /dev/null +++ b/tests/Foundation/Cloud/QueueTest.php @@ -0,0 +1,662 @@ +set('app.key', Str::random(32)); + } + + protected function setUp(): void + { + Worker::$restartable = true; + Worker::$pausable = true; + $_SERVER['LARAVEL_CLOUD'] = $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['SQS_PREFIX'] = 'https://sqs.us-east-2.amazonaws.com/1234567'; + $_SERVER['SQS_SUFFIX'] = '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'; + + parent::setUp(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX'], $_SERVER['LARAVEL_CLOUD_REGION']); + Worker::$restartable = true; + Worker::$pausable = true; + } + + public function testItDisablesQueueRestartPollingForManagedQueues() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + Cloud::bootManagedQueues($this->app); + $this->assertTrue(Worker::$restartable); + + $this->app['queue']->connection('sqs'); + $this->assertFalse(Worker::$restartable); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItDisablesQueuePausePollingForManagedQueues() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + Cloud::bootManagedQueues($this->app); + $this->assertTrue(Worker::$pausable); + + $this->app['queue']->connection('sqs'); + $this->assertFalse(Worker::$pausable); + } finally { + $_SERVER['argv'] = $argv; + } + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function testItConfiguresManagedQueueCredentials() + { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function testItDoesNotConfigureManagedQueuesWhenNotEnabled() + { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + Cloud::configureManagedQueues($this->app); + + $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function testItConfiguresManagedQueueRegion() + { + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + } + } + + public function testItSetSqsCredentialsToEcs() + { + $this->assertSame(null, Config::get('queue.connections.sqs.credentials')); + + Cloud::configureManagedQueues($this->app); + + $this->assertSame('ecs', Config::get('queue.connections.sqs.credentials')); + } + + public function testItSetsTheSqsRegion() + { + $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); + + Cloud::configureManagedQueues($this->app); + $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); + + $_SERVER['LARAVEL_CLOUD_REGION'] = 'eu-central-1'; + Cloud::configureManagedQueues($this->app); + + $this->assertSame('eu-central-1', Config::get('queue.connections.sqs.region')); + } + + public function testItBindsQueueConnectorAndNewsUpSqsConnector() + { + $this->app->bind(SqsConnector::class, fn () => throw new RuntimeException('Should not be resolved')); + Cloud::bootManagedQueues($this->app); + + $this->app[QueueConnector::class]; + } + + public function testItBindsCloudQueue() + { + Cloud::bootManagedQueues($this->app); + + $this->assertInstanceOf(Queue::class, $this->app['queue']->connection('sqs')); + } + + public function testItBindsCloudEventsAsSingleton() + { + Cloud::bootManagedQueues($this->app); + + $this->assertFalse($this->app->resolved(Events::class)); + $this->assertSame($this->app[Events::class], $this->app[Events::class]); + } + + public function testItBindsTheQueueFailer() + { + Cloud::bootManagedQueues($this->app); + + $this->assertInstanceOf(FailedJobProvider::class, $this->app['queue.failer']); + } + + public function testItDoesNotBindCloudQueueWhenManagedQueuesIsInactive() + { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + + Cloud::bootManagedQueues($this->app); + + $this->assertInstanceOf(SqsQueue::class, $this->app['queue']->connection('sqs')); + } + + public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() + { + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queue->pop(); + + $this->assertSame([], $eventsFake->emitted); + } + + public function testItEmitsStartedEventWhenJobIsSuccessfullyPopped() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->assertSame([[ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ]], $eventsFake->emitted); + } + + public function testItEmitsProcessedEventWhenNextJobIsAboutToPop() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $this->travel(1)->second(); + $queue->pop(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:06.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 1000, + ], + ], $eventsFake->emitted); + } + + public function testItDoesNotEmitEventsForTheSameJobAfterItHasBeenProcessed() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $queue->pop(); + $queue->pop(); + $queue->pop(); + + $this->assertCount(2, $eventsFake->emitted); + } + + public function testItRemembersTheQueueForTheProcessedEvent() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop = [new FakeJob, new FakeJob]; + $queue->pop('first'); + $queue->pop('second'); + $queue->pop('third'); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'first', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'first', + 'duration_ms' => 0, + ], [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'second', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'second', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } + + public function testItEmitsFailedJobEvents() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + $failerFake = $this->fakeFailer(); + $failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']); + $failedJobProvider->setQueue($queue); + $this->app[FailedJobProvider::class] = $failedJobProvider; + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->fail(); + Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]); + $failedJobProvider->log('sqs', 'default', ['payload' => 'here'], new RuntimeException('Whoops!')); + Str::createUuidsNormally(); + $queue->pop(); + + unset($eventsFake->emitted[1]['exception']); + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'failed_job', + 'id' => '00dc709e-90c4-70c2-87c8-9b7127d20e8f', + 'queue' => 'default', + 'started_at' => '2000-01-02 03:04:05.060708', + 'attempts' => 1, + 'payload' => [ + 'payload' => 'here', + ], + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'failed', + 'queue' => 'default', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } + + public function testItEmitsReleasedJobEvents() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->release(); + $queue->pop(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'released', + 'queue' => 'default', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } + + public function testItEmitsJobQueuedEvent() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queue->push(new FakeJob, queue: '1'); + $queue->pushOn('2', new FakeJob); + $queue->pushRaw('', queue: '3'); + $queue->later(1, new FakeJob, queue: '4'); + $queue->laterOn('5', 1, new FakeJob); + $queue->bulk([new FakeJob, new FakeJob], queue: '6'); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '1', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '2', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '3', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '4', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '5', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + ], $eventsFake->emitted); + } + + public function testTimestampsAreTheSameForBulkPush() + { + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queue->bulk([new FakeJob, new FakeJob]); + + $this->assertCount(2, $eventsFake->emitted); + // IMPORTANT: Do not freeze time to fix this test. + $this->assertSame($eventsFake->emitted[0]['timestamp'], $eventsFake->emitted[1]['timestamp']); + } + + public function testItCapturesDurationForMultipleJobs() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop = [new FakeJob, new FakeJob]; + $queue->pop(); + $this->travel(1)->second(); + $queue->pop(); + $this->travel(0.5)->second(); + $queue->pop(); + + $this->assertSame(1000, $eventsFake->emitted[1]['duration_ms']); + $this->assertSame(500, $eventsFake->emitted[3]['duration_ms']); + } + + public function testItCapturesUtcTime() + { + date_default_timezone_set('Australia/Melbourne'); + $this->travelTo(Carbon::parse('2000-01-02 03:04:05.060708', 'Australia/Melbourne')); + $eventsFake = $this->fakeEvents(); + $queueFake = $this->fakeQueue(); + $queue = new Queue($queueFake, $eventsFake); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $this->travel(1)->second(); + $queue->pop(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-01 16:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-01 16:04:06.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 1000, + ], + ], $eventsFake->emitted); + } + + public function testFindProxiesToFailerForNonUrls() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $job = $provider->find('not-a-url'); + + $this->assertNull($job); + } + + public function testFindGetsUrlAndDecryptsResponse() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $payload = ['id' => 'test-job-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{"job":"App\\\\Jobs\\\\TestJob"}']; + $encrypted = Crypt::encryptString(json_encode($payload)); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response($encrypted), + ]); + + $result = $provider->find('https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + + $this->assertIsObject($result); + $this->assertSame('test-job-id', $result->id); + $this->assertSame('sqs', $result->connection); + $this->assertSame('default', $result->queue); + $this->assertSame('{"job":"App\\\\Jobs\\\\TestJob"}', $result->payload); + Http::assertSent(fn ($request) => $request->url() === 'https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + } + + public function testFindReturnsNullWhenDecryptionFails() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response('not-valid-encrypted-data'), + ]); + + try { + $provider->find('https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + $this->fail(); + } catch (Throwable $e) { + $this->assertInstanceOf(DecryptException::class, $e); + } + } + + public function testFindReturnsNullWhenHttpRequestFails() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response('Server Error', 500), + ]); + + try { + $provider->find('https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + $this->fail(); + } catch (Throwable $e) { + $this->assertInstanceOf(RequestException::class, $e); + } + } + + public function testForgetProxiesToFailerForNonUrls() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + // First log a job to the failer with a UUID + $uuid = (string) Str::uuid(); + $failer->log('database', 'default', json_encode(['uuid' => $uuid]), new \Exception('test')); + $jobId = $failer->ids()[0]; + + // Forget should delegate to the underlying failer + $result = $provider->forget($jobId); + + $this->assertTrue($result); + $this->assertEmpty($failer->ids()); + } + + public function testForgetEmitsEventAfterFind() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $payload = ['id' => 'forget-test-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{}']; + $encrypted = Crypt::encryptString(json_encode($payload)); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response($encrypted), + ]); + + $url = 'https://cloud.laravel.com/api/jobs/forget-test-id?signature=abc'; + $provider->find($url); + $result = $provider->forget($url); + + $this->assertTrue($result); + $this->assertSame([ + [ + '_cloud_event' => 'failed_job', + 'id' => 'forget-test-id', + 'queue' => 'default', + 'retried_at' => '2000-01-02 03:04:05.060708', + ], + ], $eventsFake->emitted); + } + + public function testForgetReturnsFalseWithoutPriorFind() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $result = $provider->forget('https://cloud.laravel.com/api/jobs/some-id?signature=abc'); + + $this->assertFalse($result); + $this->assertEmpty($eventsFake->emitted); + } + + private function fakeEvents() + { + return new class('test-socket') extends Events + { + public array $emitted = []; + + public function emitMany(array $payloads): void + { + $this->emitted = [ + ...$this->emitted, + ...$payloads, + ]; + } + }; + } + + private function fakeQueue() + { + return new class($this->app, [], null) extends QueueFake + { + public array $jobsToPop = []; + + public function pop($queue = null) + { + return array_shift($this->jobsToPop); + } + + public function getQueue($queue) + { + $queue ??= 'default'; + + return $_SERVER['SQS_PREFIX'].'/'.$queue.$_SERVER['SQS_SUFFIX']; + } + }; + } + + private function fakeFailer() + { + return new FileFailedJobProvider(tempnam(sys_get_temp_dir(), 'cloud_failed_job_test_')); + } +} diff --git a/tests/Foundation/FoundationAliasLoaderTest.php b/tests/Foundation/FoundationAliasLoaderTest.php index 7889727027a6..08c64039dc8a 100755 --- a/tests/Foundation/FoundationAliasLoaderTest.php +++ b/tests/Foundation/FoundationAliasLoaderTest.php @@ -7,6 +7,14 @@ class FoundationAliasLoaderTest extends TestCase { + public function setUp(): void + { + parent::setUp(); + + AliasLoader::setInstance(null); + AliasLoader::setFacadeNamespace('Facades\\'); + } + public function testLoaderCanBeCreatedAndRegisteredOnce() { $loader = AliasLoader::getInstance(['foo' => 'bar']); diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index c16f074122fd..19644b64911b 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Integration\Foundation; use Illuminate\Foundation\Cloud; -use Illuminate\Queue\Worker; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; @@ -83,73 +82,6 @@ public function test_it_can_configure_scoped_disks() unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); } - public function test_it_disables_queue_restart_polling_for_managed_queues() - { - Worker::$restartable = true; - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertFalse(Worker::$restartable); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); - Worker::$restartable = true; - } - } - - public function test_it_disables_queue_pause_polling_for_managed_queues() - { - Worker::$pausable = true; - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertFalse(Worker::$pausable); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); - Worker::$pausable = true; - } - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function test_it_configures_managed_queue_credentials() - { - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); - } - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function test_it_does_not_configure_managed_queues_when_not_enabled() - { - Cloud::configureManagedQueues($this->app); - - $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function test_it_configures_managed_queue_region() - { - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); - } - } - public function test_it_respects_log_levels() { if (isset($_SERVER['LOG_LEVEL'])) { From 94dccadc7f01936b7561cc0b70b3667be3450b4e Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Tue, 12 May 2026 10:07:11 -0400 Subject: [PATCH 343/596] [13.x] reset Lottery on test case teardown (#60083) * reset Lottery on teardown * whoops --- .../Testing/Concerns/InteractsWithTestCaseLifecycle.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 54e264a27964..8a1145c0c4ba 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -37,6 +37,7 @@ use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\ParallelTesting; +use Illuminate\Support\Lottery; use Illuminate\Support\Once; use Illuminate\Support\Sleep; use Illuminate\Support\Str; @@ -191,6 +192,7 @@ protected function tearDownTheTestEnvironment(): void HandleExceptions::flushState($this); JsonApiResource::flushState(); JsonResource::flushState(); + Lottery::determineResultsNormally(); Markdown::flushState(); Migrator::withoutMigrations([]); Once::flush(); From 7bc59aebf9f4aeea5cf5856016a9d15751c64f3a Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 May 2026 00:08:49 +1000 Subject: [PATCH 344/596] Support `after_commit` for queue metrics (#60078) --- src/Illuminate/Foundation/Cloud/Queue.php | 87 ++--------- .../Foundation/Cloud/QueueConnector.php | 13 ++ tests/Foundation/Cloud/QueueTest.php | 144 +++++++++++++++--- 3 files changed, 151 insertions(+), 93 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php index b6a723be3877..e5dbfdad11bb 100644 --- a/src/Illuminate/Foundation/Cloud/Queue.php +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -26,13 +26,6 @@ class Queue implements QueueContract, ClearableQueue */ protected $processingQueue = null; - /** - * The date the last job was pushed. - * - * @var \Carbon\CarbonImmutable|null - */ - protected $lastJobPushedAt = null; - /** * The date the last job started processing. * @@ -115,13 +108,7 @@ public function creationTimeOfOldestPendingJob($queue = null) */ public function push($job, $data = '', $queue = null) { - $this->beforeJobPushed(); - - $result = $this->queue->push(...func_get_args()); - - $this->afterJobPushed($queue); - - return $result; + return $this->queue->push(...func_get_args()); } /** @@ -134,13 +121,7 @@ public function push($job, $data = '', $queue = null) */ public function pushOn($queue, $job, $data = '') { - $this->beforeJobPushed(); - - $result = $this->queue->pushOn(...func_get_args()); - - $this->afterJobPushed($queue); - - return $result; + return $this->queue->pushOn(...func_get_args()); } /** @@ -152,11 +133,9 @@ public function pushOn($queue, $job, $data = '') */ public function pushRaw($payload, $queue = null, array $options = []) { - $this->beforeJobPushed(); - $result = $this->queue->pushRaw(...func_get_args()); - $this->afterJobPushed($queue); + $this->finishQueueingJob($queue); return $result; } @@ -172,13 +151,7 @@ public function pushRaw($payload, $queue = null, array $options = []) */ public function later($delay, $job, $data = '', $queue = null) { - $this->beforeJobPushed(); - - $result = $this->queue->later(...func_get_args()); - - $this->afterJobPushed($queue); - - return $result; + return $this->queue->later(...func_get_args()); } /** @@ -192,13 +165,7 @@ public function later($delay, $job, $data = '', $queue = null) */ public function laterOn($queue, $delay, $job, $data = '') { - $this->beforeJobPushed(); - - $result = $this->queue->laterOn(...func_get_args()); - - $this->afterJobPushed($queue); - - return $result; + return $this->queue->laterOn(...func_get_args()); } /** @@ -211,13 +178,7 @@ public function laterOn($queue, $delay, $job, $data = '') */ public function bulk($jobs, $data = '', $queue = null) { - $this->beforeJobPushed(); - - $result = $this->queue->bulk(...func_get_args()); - - $this->afterJobsPushed(count($jobs), $queue); - - return $result; + return $this->queue->bulk(...func_get_args()); } /** @@ -350,42 +311,18 @@ public function processingJobDetails() } /** - * Handle before a job is pushed. - * - * @return void - */ - protected function beforeJobPushed() - { - $this->lastJobPushedAt = CarbonImmutable::now('UTC'); - } - - /** - * Handle after a job is pushed. - * - * @param string|null $queue - * @return void - */ - protected function afterJobPushed($queue) - { - $this->afterJobsPushed(1, $queue); - } - - /** - * Handle jobs being pushed. + * Handle jobs finishing being queued. * - * @param int $count - * @param string|null $queue + * @param string $queue */ - protected function afterJobsPushed($count, $queue) + public function finishQueueingJob($queue) { - $this->events->emitMany(array_fill(0, $count, [ + $this->events->emit([ '_cloud_event' => 'queue', - 'timestamp' => $this->lastJobPushedAt->toDateTimeString('microsecond'), + 'timestamp' => CarbonImmutable::now('UTC')->toDateTimeString('microsecond'), 'type' => 'queued', 'queue' => $this->normalizeQueue($queue), - ])); - - $this->lastJobPushedAt = null; + ]); } /** diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index 99386d202e67..da42cbe09a16 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Application; use Illuminate\Queue\Connectors\ConnectorInterface; +use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Queue\Worker; use Illuminate\Queue\WorkerStopReason; @@ -32,6 +33,8 @@ public function connect(array $config): Queue { $queue = new Queue($this->connector->connect($config), $this->app[Events::class]); + $this->configureQueue($queue); + if (! $this->app->runningConsoleCommand('queue:work')) { return $queue; } @@ -42,6 +45,16 @@ public function connect(array $config): Queue return $queue; } + /** + * Configure the queue. + */ + protected function configureQueue(Queue $queue): void + { + $this->app['events']->listen(fn (JobQueued $event) => $event->connectionName === $queue->getConnectionName() + ? $queue->finishQueueingJob($event->queue) + : null); + } + /** * Configure the queue worker. */ diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 1788780b65cc..064a961dd8d0 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -2,13 +2,17 @@ namespace Tests\Tests\Foundation; +use Aws\Result; +use Aws\Sqs\SqsClient; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Foundation\Cloud; use Illuminate\Foundation\Cloud\Events; use Illuminate\Foundation\Cloud\FailedJobProvider; use Illuminate\Foundation\Cloud\Queue; use Illuminate\Foundation\Cloud\QueueConnector; +use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Http\Client\RequestException; +use Illuminate\Queue\Connectors\ConnectorInterface; use Illuminate\Queue\Connectors\SqsConnector; use Illuminate\Queue\Failed\FileFailedJobProvider; use Illuminate\Queue\Jobs\FakeJob; @@ -17,17 +21,24 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Illuminate\Support\Testing\Fakes\QueueFake; +use Mockery\MockInterface; use Orchestra\Testbench\Attributes\WithConfig; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; use Ramsey\Uuid\Uuid; use RuntimeException; use Throwable; +#[WithMigration] +#[WithMigration('laravel', 'queue')] class QueueTest extends TestCase { + use DatabaseMigrations; + protected function defineEnvironment($app) { $app['config']->set('app.key', Str::random(32)); @@ -372,9 +383,12 @@ public function testItEmitsReleasedJobEvents() public function testItEmitsJobQueuedEvent() { $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $client = $this->fakeConnector(); + $queue = $this->app['queue']->connection('sqs'); + $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); $queue->push(new FakeJob, queue: '1'); $queue->pushOn('2', new FakeJob); @@ -429,17 +443,73 @@ public function testItEmitsJobQueuedEvent() ], $eventsFake->emitted); } - public function testTimestampsAreTheSameForBulkPush() + public function testItRespectsDispatchAfterTransaction() { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $client = $this->fakeConnector(); + $this->app['config']->set('queue.connections.sqs.after_commit', true); + $queue = $this->app['queue']->connection('sqs'); + $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); - $queue->bulk([new FakeJob, new FakeJob]); + DB::beginTransaction(); - $this->assertCount(2, $eventsFake->emitted); - // IMPORTANT: Do not freeze time to fix this test. - $this->assertSame($eventsFake->emitted[0]['timestamp'], $eventsFake->emitted[1]['timestamp']); + $queue->push(new FakeJob, queue: '1'); + $queue->pushOn('2', new FakeJob); + $queue->pushRaw('', queue: '3'); + $queue->later(1, new FakeJob, queue: '4'); + $queue->laterOn('5', 1, new FakeJob); + $queue->bulk([new FakeJob, new FakeJob], queue: '6'); + + $this->travel(10)->minutes(); + DB::commit(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '3', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '1', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '2', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '4', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '5', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + ], $eventsFake->emitted); } public function testItCapturesDurationForMultipleJobs() @@ -619,20 +689,50 @@ public function testForgetReturnsFalseWithoutPriorFind() $this->assertEmpty($eventsFake->emitted); } - private function fakeEvents() + /** + * @return MockInterface + */ + private function fakeConnector() { - return new class('test-socket') extends Events + $client = $this->mock(SqsClient::class); + + $this->app->instance(QueueConnector::class, new QueueConnector(new class($client) implements ConnectorInterface { - public array $emitted = []; + public function __construct(private $client) + { + // + } - public function emitMany(array $payloads): void + public function connect($config) { - $this->emitted = [ - ...$this->emitted, - ...$payloads, - ]; + return new SqsQueue( + $this->client, + $config['queue'], + $config['prefix'] ?? '', + $config['suffix'] ?? '', + $config['after_commit'] ?? null, + $config['overflow'] ?? [], + ); } - }; + }, $this->app)); + + return $client; + } + + private function fakeEvents() + { + return $this->app->instance(Events::class, new class('test-socket') extends Events + { + public array $emitted = []; + + public function emitMany(array $payloads): void + { + $this->emitted = [ + ...$this->emitted, + ...$payloads, + ]; + } + }); } private function fakeQueue() @@ -660,3 +760,11 @@ private function fakeFailer() return new FileFailedJobProvider(tempnam(sys_get_temp_dir(), 'cloud_failed_job_test_')); } } + +class MyJob +{ + public function fire() + { + // + } +} From ad9279e370e04fce4a76585072444eeef33a4282 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 12 May 2026 14:09:15 +0000 Subject: [PATCH 345/596] Apply fixes from StyleCI --- tests/Foundation/Cloud/QueueTest.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 064a961dd8d0..ea51bb1e2aa6 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -714,7 +714,7 @@ public function connect($config) $config['overflow'] ?? [], ); } - }, $this->app)); + }, $this->app)); return $client; } @@ -723,16 +723,16 @@ private function fakeEvents() { return $this->app->instance(Events::class, new class('test-socket') extends Events { - public array $emitted = []; - - public function emitMany(array $payloads): void - { - $this->emitted = [ - ...$this->emitted, - ...$payloads, - ]; - } - }); + public array $emitted = []; + + public function emitMany(array $payloads): void + { + $this->emitted = [ + ...$this->emitted, + ...$payloads, + ]; + } + }); } private function fakeQueue() From 37ad48c4a140a8f5d6878f2515f479267f12a685 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 13 May 2026 11:03:01 +0800 Subject: [PATCH 346/596] [13.x] Remove Composer `github-oauth` credentials on Linux & Windows Actions (#60095) * Disable `composer config` output executed via GitHub Actions Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki --- .github/workflows/databases-nightly.yml | 24 ++++++ .github/workflows/databases.yml | 108 ++++++++++++++++++++++++ .github/workflows/facades.yml | 12 +++ .github/workflows/queues.yml | 36 ++++++++ .github/workflows/redis.yml | 24 ++++++ .github/workflows/static-analysis.yml | 12 +++ .github/workflows/tests.yml | 24 ++++++ 7 files changed, 240 insertions(+) diff --git a/.github/workflows/databases-nightly.yml b/.github/workflows/databases-nightly.yml index c7074e605a03..dba1920d1f8f 100644 --- a/.github/workflows/databases-nightly.yml +++ b/.github/workflows/databases-nightly.yml @@ -32,6 +32,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -75,6 +87,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 43d6ce29dd27..623150fbd037 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -36,6 +36,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -80,6 +92,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -123,6 +147,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -167,6 +203,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -213,6 +261,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -259,6 +319,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -303,6 +375,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -348,6 +432,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -384,6 +480,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index a412b2f74fd1..61e750ff11dc 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -28,6 +28,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 7703c1d5c8d3..6656ff461952 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -25,6 +25,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -57,6 +69,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -109,6 +133,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/redis.yml b/.github/workflows/redis.yml index d45e7dead357..19a854112b1b 100644 --- a/.github/workflows/redis.yml +++ b/.github/workflows/redis.yml @@ -37,6 +37,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -83,6 +95,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index d66649d25584..df82ac498901 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -29,6 +29,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a763d5dd183..e074b3d09837 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,6 +64,18 @@ jobs: REDIS_CONFIGURE_OPTS: --enable-redis --enable-redis-igbinary --enable-redis-msgpack --enable-redis-lzf --with-liblzf --enable-redis-zstd --with-libzstd --enable-redis-lz4 --with-liblz4 REDIS_LIBS: liblz4-dev, liblzf-dev, libzstd-dev + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" @@ -124,6 +136,18 @@ jobs: tools: composer:v2 coverage: none + - name: Remove Composer `github-oauth` credentials (Linux) + if: runner.os == 'Linux' + run: | + rm -f ~/.composer/auth.json + rm -f ~/.config/composer/auth.json + + - name: Remove Composer `github-oauth` credentials (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" + - name: Set Framework version run: composer config version "13.x-dev" From f39e7bc616e1d6245f21aad5face8b5bea845f9f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 13 May 2026 16:12:48 +0800 Subject: [PATCH 347/596] =?UTF-8?q?Revert=20"[13.x]=20Remove=20Composer=20?= =?UTF-8?q?`github-oauth`=20credentials=20on=20Linux=20&=20Windows=20?= =?UTF-8?q?=E2=80=A6"=20(#60100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 37ad48c4a140a8f5d6878f2515f479267f12a685. --- .github/workflows/databases-nightly.yml | 24 ------ .github/workflows/databases.yml | 108 ------------------------ .github/workflows/facades.yml | 12 --- .github/workflows/queues.yml | 36 -------- .github/workflows/redis.yml | 24 ------ .github/workflows/static-analysis.yml | 12 --- .github/workflows/tests.yml | 24 ------ 7 files changed, 240 deletions(-) diff --git a/.github/workflows/databases-nightly.yml b/.github/workflows/databases-nightly.yml index dba1920d1f8f..c7074e605a03 100644 --- a/.github/workflows/databases-nightly.yml +++ b/.github/workflows/databases-nightly.yml @@ -32,18 +32,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -87,18 +75,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 623150fbd037..43d6ce29dd27 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -36,18 +36,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -92,18 +80,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -147,18 +123,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -203,18 +167,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -261,18 +213,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -319,18 +259,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -375,18 +303,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -432,18 +348,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -480,18 +384,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index 61e750ff11dc..a412b2f74fd1 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -28,18 +28,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 6656ff461952..7703c1d5c8d3 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -25,18 +25,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -69,18 +57,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -133,18 +109,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/redis.yml b/.github/workflows/redis.yml index 19a854112b1b..d45e7dead357 100644 --- a/.github/workflows/redis.yml +++ b/.github/workflows/redis.yml @@ -37,18 +37,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -95,18 +83,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index df82ac498901..d66649d25584 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -29,18 +29,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e074b3d09837..7a763d5dd183 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,18 +64,6 @@ jobs: REDIS_CONFIGURE_OPTS: --enable-redis --enable-redis-igbinary --enable-redis-msgpack --enable-redis-lzf --with-liblzf --enable-redis-zstd --with-libzstd --enable-redis-lz4 --with-liblz4 REDIS_LIBS: liblz4-dev, liblzf-dev, libzstd-dev - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" @@ -136,18 +124,6 @@ jobs: tools: composer:v2 coverage: none - - name: Remove Composer `github-oauth` credentials (Linux) - if: runner.os == 'Linux' - run: | - rm -f ~/.composer/auth.json - rm -f ~/.config/composer/auth.json - - - name: Remove Composer `github-oauth` credentials (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Remove-Item -Force -ErrorAction SilentlyContinue "$env:APPDATA\Composer\auth.json" - - name: Set Framework version run: composer config version "13.x-dev" From 3e675181ed7c7e279ad0603fd145d0c0da1ad03f Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 May 2026 23:39:45 +1000 Subject: [PATCH 348/596] Use config for queue suffix and prefix (#60094) --- src/Illuminate/Foundation/Cloud/Queue.php | 25 ++++++- .../Foundation/Cloud/QueueConnector.php | 6 +- tests/Foundation/Cloud/QueueTest.php | 73 ++++++++++++++----- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php index e5dbfdad11bb..fe3c5c611924 100644 --- a/src/Illuminate/Foundation/Cloud/Queue.php +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -33,14 +33,33 @@ class Queue implements QueueContract, ClearableQueue */ protected $processingJobStartedAt = null; + /** + * The queue prefix. + * + * @var string + */ + protected $prefix; + + /** + * The queue suffix. + * + * @var string + */ + protected $suffix; + /** * Create a new Queue instance. */ public function __construct( protected QueueContract $queue, protected Events $events, + protected array $config, ) { - // + $this->prefix = array_key_exists('prefix', $config) && is_string($config['prefix']) + ? $config['prefix'].'/' + : ''; + + $this->suffix = $config['suffix'] ?? ''; } /** @@ -359,8 +378,8 @@ protected function startProcessingJob($queue, $job) protected function normalizeQueue($queue) { return Str::of($this->queue->getQueue($queue)) - ->chopStart($_SERVER['SQS_PREFIX'].'/') - ->chopEnd($_SERVER['SQS_SUFFIX']) + ->chopStart($this->prefix) + ->chopEnd($this->suffix) ->toString(); } diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index da42cbe09a16..23926b1174ac 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -31,7 +31,11 @@ public function __construct( */ public function connect(array $config): Queue { - $queue = new Queue($this->connector->connect($config), $this->app[Events::class]); + $queue = new Queue( + $this->connector->connect($config), + $this->app[Events::class], + $config, + ); $this->configureQueue($queue); diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index ea51bb1e2aa6..d491f9b99885 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -18,6 +18,7 @@ use Illuminate\Queue\Jobs\FakeJob; use Illuminate\Queue\SqsQueue; use Illuminate\Queue\Worker; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; @@ -49,17 +50,20 @@ protected function setUp(): void Worker::$restartable = true; Worker::$pausable = true; $_SERVER['LARAVEL_CLOUD'] = $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - $_SERVER['SQS_PREFIX'] = 'https://sqs.us-east-2.amazonaws.com/1234567'; - $_SERVER['SQS_SUFFIX'] = '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'; parent::setUp(); + + $this->app['config']->set([ + 'queue.connections.sqs.prefix' => 'https://sqs.us-east-2.amazonaws.com/1234567', + 'queue.connections.sqs.suffix' => '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', + ]); } protected function tearDown(): void { parent::tearDown(); - unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX'], $_SERVER['LARAVEL_CLOUD_REGION']); + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); Worker::$restartable = true; Worker::$pausable = true; } @@ -193,7 +197,7 @@ public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJ { $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, []); $queue->pop(); @@ -205,7 +209,7 @@ public function testItEmitsStartedEventWhenJobIsSuccessfullyPopped() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -223,7 +227,7 @@ public function testItEmitsProcessedEventWhenNextJobIsAboutToPop() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -252,7 +256,7 @@ public function testItDoesNotEmitEventsForTheSameJobAfterItHasBeenProcessed() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -268,7 +272,7 @@ public function testItRemembersTheQueueForTheProcessedEvent() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop = [new FakeJob, new FakeJob]; $queue->pop('first'); @@ -309,7 +313,7 @@ public function testItEmitsFailedJobEvents() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $failerFake = $this->fakeFailer(); $failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']); $failedJobProvider->setQueue($queue); @@ -356,7 +360,7 @@ public function testItEmitsReleasedJobEvents() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop[] = $jobFake = new FakeJob; $queue->pop(); @@ -386,8 +390,7 @@ public function testItEmitsJobQueuedEvent() Cloud::configureManagedQueues($this->app); Cloud::bootManagedQueues($this->app); $eventsFake = $this->fakeEvents(); - $client = $this->fakeConnector(); - $queue = $this->app['queue']->connection('sqs'); + [$queue, $client] = $this->mockedQueue(); $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); $queue->push(new FakeJob, queue: '1'); @@ -449,9 +452,8 @@ public function testItRespectsDispatchAfterTransaction() Cloud::configureManagedQueues($this->app); Cloud::bootManagedQueues($this->app); $eventsFake = $this->fakeEvents(); - $client = $this->fakeConnector(); $this->app['config']->set('queue.connections.sqs.after_commit', true); - $queue = $this->app['queue']->connection('sqs'); + [$queue, $client] = $this->mockedQueue(); $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); DB::beginTransaction(); @@ -517,7 +519,7 @@ public function testItCapturesDurationForMultipleJobs() $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop = [new FakeJob, new FakeJob]; $queue->pop(); @@ -536,7 +538,7 @@ public function testItCapturesUtcTime() $this->travelTo(Carbon::parse('2000-01-02 03:04:05.060708', 'Australia/Melbourne')); $eventsFake = $this->fakeEvents(); $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake); + $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -689,10 +691,41 @@ public function testForgetReturnsFalseWithoutPriorFind() $this->assertEmpty($eventsFake->emitted); } + public function testItUsesConfigValuesToNormalizeQueueName() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); + + unset($_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX']); + + $queue->push(new FakeJob, queue: 'https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'); + + $this->assertSame('my-queue', $eventsFake->emitted[0]['queue']); + } + + public function testItHandlesMissingPrefixAndSuffixConfig() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + $this->app['config']->set('queue.connections.sqs', Arr::except($this->app['config']->get('queue.connections.sqs'), ['prefix', 'suffix'])); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); + + unset($_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX']); + + $queue->push(new FakeJob, queue: 'https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'); + + $this->assertSame('https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', $eventsFake->emitted[0]['queue']); + } + /** - * @return MockInterface + * @return array{Queue, MockInterface} */ - private function fakeConnector() + private function mockedQueue() { $client = $this->mock(SqsClient::class); @@ -716,7 +749,7 @@ public function connect($config) } }, $this->app)); - return $client; + return [$this->app['queue']->connection('sqs'), $client]; } private function fakeEvents() @@ -750,7 +783,7 @@ public function getQueue($queue) { $queue ??= 'default'; - return $_SERVER['SQS_PREFIX'].'/'.$queue.$_SERVER['SQS_SUFFIX']; + return config('queue.connections.sqs.prefix').'/'.$queue.config('queue.connections.sqs.suffix'); } }; } From 0d89d7c0be7c8b1f38c1216b3739645d9efd61f5 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Wed, 13 May 2026 10:44:10 -0300 Subject: [PATCH 349/596] feat: support concurrency run timeouts (#60105) - add optional timeout arguments to concurrency run contracts - apply custom timeouts to process driver pool commands - document facade timeout usage and cover process behavior --- src/Illuminate/Concurrency/ForkDriver.php | 3 ++- src/Illuminate/Concurrency/ProcessDriver.php | 11 +++++++--- src/Illuminate/Concurrency/SyncDriver.php | 3 ++- .../Contracts/Concurrency/Driver.php | 3 ++- .../Support/Facades/Concurrency.php | 2 +- .../Concurrency/ConcurrencyTest.php | 20 +++++++++++++++++++ 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Concurrency/ForkDriver.php b/src/Illuminate/Concurrency/ForkDriver.php index 52d05e7be831..da4b5b85f8e7 100644 --- a/src/Illuminate/Concurrency/ForkDriver.php +++ b/src/Illuminate/Concurrency/ForkDriver.php @@ -2,6 +2,7 @@ namespace Illuminate\Concurrency; +use Carbon\CarbonInterval; use Closure; use Illuminate\Contracts\Concurrency\Driver; use Illuminate\Support\Arr; @@ -15,7 +16,7 @@ class ForkDriver implements Driver /** * Run the given tasks concurrently and return an array containing the results. */ - public function run(Closure|array $tasks): array + public function run(Closure|array $tasks, CarbonInterval|int|null $timeout = null): array { $tasks = Arr::wrap($tasks); diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index 823a6ba8584b..a770f658c9fe 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -2,6 +2,7 @@ namespace Illuminate\Concurrency; +use Carbon\CarbonInterval; use Closure; use Exception; use Illuminate\Console\Application; @@ -29,17 +30,21 @@ public function __construct(protected ProcessFactory $processFactory) * * @throws \Throwable */ - public function run(Closure|array $tasks): array + public function run(Closure|array $tasks, CarbonInterval|int|null $timeout = null): array { $command = Application::formatCommandString('invoke-serialized-closure'); - $results = $this->processFactory->pool(function (Pool $pool) use ($tasks, $command) { + $results = $this->processFactory->pool(function (Pool $pool) use ($tasks, $command, $timeout) { foreach (Arr::wrap($tasks) as $key => $task) { - $pool->as($key)->path(base_path())->env([ + $process = $pool->as($key)->path(base_path())->env([ 'LARAVEL_INVOKABLE_CLOSURE' => base64_encode( serialize(new SerializableClosure($task)) ), ])->command($command); + + if (! is_null($timeout)) { + $process->timeout($timeout); + } } })->start()->wait(); diff --git a/src/Illuminate/Concurrency/SyncDriver.php b/src/Illuminate/Concurrency/SyncDriver.php index a2f27bae54a8..1b46b8f71812 100644 --- a/src/Illuminate/Concurrency/SyncDriver.php +++ b/src/Illuminate/Concurrency/SyncDriver.php @@ -2,6 +2,7 @@ namespace Illuminate\Concurrency; +use Carbon\CarbonInterval; use Closure; use Illuminate\Contracts\Concurrency\Driver; use Illuminate\Support\Collection; @@ -14,7 +15,7 @@ class SyncDriver implements Driver /** * Run the given tasks concurrently and return an array containing the results. */ - public function run(Closure|array $tasks): array + public function run(Closure|array $tasks, CarbonInterval|int|null $timeout = null): array { return Collection::wrap($tasks)->map( fn ($task) => $task() diff --git a/src/Illuminate/Contracts/Concurrency/Driver.php b/src/Illuminate/Contracts/Concurrency/Driver.php index 901f613b349b..f5e18e9afa7a 100644 --- a/src/Illuminate/Contracts/Concurrency/Driver.php +++ b/src/Illuminate/Contracts/Concurrency/Driver.php @@ -2,6 +2,7 @@ namespace Illuminate\Contracts\Concurrency; +use Carbon\CarbonInterval; use Closure; use Illuminate\Support\Defer\DeferredCallback; @@ -10,7 +11,7 @@ interface Driver /** * Run the given tasks concurrently and return an array containing the results. */ - public function run(Closure|array $tasks): array; + public function run(Closure|array $tasks, CarbonInterval|int|null $timeout = null): array; /** * Defer the execution of the given tasks. diff --git a/src/Illuminate/Support/Facades/Concurrency.php b/src/Illuminate/Support/Facades/Concurrency.php index 4fa32d502aba..d7a6c3f6638b 100644 --- a/src/Illuminate/Support/Facades/Concurrency.php +++ b/src/Illuminate/Support/Facades/Concurrency.php @@ -17,7 +17,7 @@ * @method static void purge(string|null $name = null) * @method static \Illuminate\Concurrency\ConcurrencyManager extend(string $name, \Closure $callback) * @method static \Illuminate\Concurrency\ConcurrencyManager setApplication(\Illuminate\Contracts\Foundation\Application $app) - * @method static array run(\Closure|array $tasks) + * @method static array run(\Closure|array $tasks, \Carbon\CarbonInterval|int|null $timeout = null) * @method static \Illuminate\Support\Defer\DeferredCallback defer(\Closure|array $tasks) * * @see \Illuminate\Concurrency\ConcurrencyManager diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index 3f2615eba38f..83f31faae841 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -89,6 +89,26 @@ public function testOutputIsMappedToArrayInput() // $this->assertEquals(4, $forkOutput['second']); } + public function testProcessDriverRunMayUseCustomTimeout() + { + $factory = $this->app->make(ProcessFactory::class); + + $factory->fake(fn () => $factory->result(json_encode([ + 'successful' => true, + 'result' => serialize('result'), + ]))); + + $result = (new ProcessDriver($factory))->run([ + fn () => 'result', + ], timeout: 120); + + $this->assertSame(['result'], $result); + + $factory->assertRan(function ($process) { + return $process->timeout === 120; + }); + } + public function testDriverCanBeResolvedUsingBackedEnum() { $this->assertInstanceOf( From 18ddccd73e2a5e5938da6b04702c78eba806382f Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Wed, 13 May 2026 09:45:52 -0400 Subject: [PATCH 350/596] allow closure for ThrottlesExceptions (#60103) --- .../Queue/Middleware/ThrottlesExceptions.php | 22 +++++++++++-- .../ThrottlesExceptionsWithRedis.php | 2 +- .../Queue/ThrottlesExceptionsTest.php | 32 +++++++++++++++++++ .../ThrottlesExceptionsWithRedisTest.php | 32 +++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 79c00fd95a27..7ecb5b71c3ff 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -2,6 +2,7 @@ namespace Illuminate\Queue\Middleware; +use Closure; use Illuminate\Cache\RateLimiter; use Illuminate\Container\Container; use Throwable; @@ -39,7 +40,7 @@ class ThrottlesExceptions /** * The number of minutes to wait before retrying the job after an exception. * - * @var int + * @var int|(\Closure(\Throwable): int) */ protected $retryAfterMinutes = 0; @@ -137,7 +138,7 @@ public function handle($job, $next) $this->limiter->hit($jobKey, $this->decaySeconds); - return $job->release($this->retryAfterMinutes * 60); + return $job->release($this->getTimeUntilNextRetryAfterException($throwable)); } } @@ -234,7 +235,7 @@ public function withPrefix(string $prefix) /** * Specify the number of minutes a job should be delayed when it is released (before it has reached its max exceptions). * - * @param int $backoff + * @param int|(\Closure(\Throwable): int) $backoff * @return $this */ public function backoff($backoff) @@ -244,6 +245,21 @@ public function backoff($backoff) return $this; } + /** + * Get the number of seconds that should elapse before the job is retried after an exception. + * + * @param \Throwable $throwable + * @return int + */ + protected function getTimeUntilNextRetryAfterException(Throwable $throwable) + { + $backoff = $this->retryAfterMinutes instanceof Closure + ? call_user_func($this->retryAfterMinutes, $throwable) + : $this->retryAfterMinutes; + + return $backoff * 60; + } + /** * Get the cache key associated for the rate limiter. * diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php index 1e3dd10cff7a..7d7a302f9f39 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -80,7 +80,7 @@ public function handle($job, $next) $this->limiter->acquire(); - return $job->release($this->retryAfterMinutes * 60); + return $job->release($this->getTimeUntilNextRetryAfterException($throwable)); } } diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index f9b205c3b8bc..5203915cff66 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -317,6 +317,38 @@ public function release() $this->assertTrue($job->handled); } + public function testItCanBackoffUsingException() + { + $job = new class + { + public $releasedAfter; + + public function release($delay) + { + $this->releasedAfter = $delay; + + return $this; + } + }; + $expectedException = new RuntimeException('Whoops!'); + $receivedException = null; + $next = function () use ($expectedException) { + throw $expectedException; + }; + + $middleware = (new ThrottlesExceptions())->backoff(function ($throwable) use (&$receivedException) { + $receivedException = $throwable; + + return 5; + }); + + $result = $middleware->handle($job, $next); + + $this->assertSame($job, $result); + $this->assertSame($expectedException, $receivedException); + $this->assertSame(300, $job->releasedAfter); + } + public function testReportingExceptions() { $this->spy(ExceptionHandler::class) diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php index 356e8dbfdcb9..847ff2564cfd 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -150,6 +150,38 @@ public function release() $middleware->report(fn () => false); $middleware->handle($job, $next); } + + public function testItCanBackoffUsingException() + { + $job = new class + { + public $releasedAfter; + + public function release($delay) + { + $this->releasedAfter = $delay; + + return $this; + } + }; + $expectedException = new RuntimeException('Whoops!'); + $receivedException = null; + $next = function () use ($expectedException) { + throw $expectedException; + }; + + $middleware = (new ThrottlesExceptionsWithRedis())->backoff(function ($throwable) use (&$receivedException) { + $receivedException = $throwable; + + return 5; + }); + + $result = $middleware->handle($job, $next); + + $this->assertSame($job, $result); + $this->assertSame($expectedException, $receivedException); + $this->assertSame(300, $job->releasedAfter); + } } class CircuitBreakerWithRedisTestJob From 7898d05cadf893aded0c8c408cbc3a31c3cf040c Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Wed, 13 May 2026 15:58:52 +0200 Subject: [PATCH 351/596] feat: Add enum support to contextual attribute binding (#60092) Support UnitEnum and backed enums in Container attribute bindings: - Auth, Authenticated, and Cache attributes now accept enum values - Updated parameter types to accept UnitEnum|string|null - Added comprehensive tests for unit and backed enum bindings - Tests cover AuthGuardUnitEnum, AuthGuardBackedEnum, CacheStoreUnitEnum, and CacheStoreBackedEnum --- src/Illuminate/Container/Attributes/Auth.php | 3 +- .../Container/Attributes/Authenticated.php | 3 +- src/Illuminate/Container/Attributes/Cache.php | 3 +- .../ContextualAttributeBindingTest.php | 60 +++++++++++++++++-- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Container/Attributes/Auth.php b/src/Illuminate/Container/Attributes/Auth.php index 4cf0c1a4cc68..be6d69c0ce3a 100644 --- a/src/Illuminate/Container/Attributes/Auth.php +++ b/src/Illuminate/Container/Attributes/Auth.php @@ -5,6 +5,7 @@ use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; +use UnitEnum; #[Attribute(Attribute::TARGET_PARAMETER)] class Auth implements ContextualAttribute @@ -12,7 +13,7 @@ class Auth implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public ?string $guard = null) + public function __construct(public UnitEnum|string|null $guard = null) { } diff --git a/src/Illuminate/Container/Attributes/Authenticated.php b/src/Illuminate/Container/Attributes/Authenticated.php index ffbba4553719..a2b2d6d3e73e 100644 --- a/src/Illuminate/Container/Attributes/Authenticated.php +++ b/src/Illuminate/Container/Attributes/Authenticated.php @@ -5,6 +5,7 @@ use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; +use UnitEnum; #[Attribute(Attribute::TARGET_PARAMETER)] class Authenticated implements ContextualAttribute @@ -12,7 +13,7 @@ class Authenticated implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public ?string $guard = null) + public function __construct(public UnitEnum|string|null $guard = null) { } diff --git a/src/Illuminate/Container/Attributes/Cache.php b/src/Illuminate/Container/Attributes/Cache.php index 2b7b1f78e038..0b8636848ddb 100644 --- a/src/Illuminate/Container/Attributes/Cache.php +++ b/src/Illuminate/Container/Attributes/Cache.php @@ -5,6 +5,7 @@ use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; +use UnitEnum; #[Attribute(Attribute::TARGET_PARAMETER)] class Cache implements ContextualAttribute @@ -12,7 +13,7 @@ class Cache implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public ?string $store = null) + public function __construct(public UnitEnum|string|null $store = null) { } diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index 041f1087698e..c357b8fbde94 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -145,6 +145,18 @@ public function testAuthedAttribute() return $guard; }); + $manager->shouldReceive('guard')->with(AuthGuardUnitEnum::unit)->andReturnUsing(function () { + $guard = m::mock(GuardContract::class); + $guard->shouldReceive('user')->andReturn(m:mock(AuthenticatableContract::class)); + + return $guard; + }); + $manager->shouldReceive('guard')->with(AuthGuardBackedEnum::Backed)->andReturnUsing(function () { + $guard = m::mock(GuardContract::class); + $guard->shouldReceive('user')->andReturn(m:mock(AuthenticatableContract::class)); + + return $guard; + }); return $manager; }); @@ -159,6 +171,8 @@ public function testCacheAttribute() $manager = m::mock(CacheManager::class); $manager->shouldReceive('store')->with('foo')->andReturn(m::mock(CacheRepository::class)); $manager->shouldReceive('store')->with('bar')->andReturn(m::mock(CacheRepository::class)); + $manager->shouldReceive('store')->with(CacheStoreUnitEnum::unit)->andReturn(m::mock(CacheRepository::class)); + $manager->shouldReceive('store')->with(CacheStoreBackedEnum::Backed)->andReturn(m::mock(CacheRepository::class)); return $manager; }); @@ -201,6 +215,8 @@ public function testAuthAttribute() $manager = m::mock(AuthManager::class); $manager->shouldReceive('guard')->with('foo')->andReturn(m::mock(GuardContract::class)); $manager->shouldReceive('guard')->with('bar')->andReturn(m::mock(GuardContract::class)); + $manager->shouldReceive('guard')->with(AuthGuardUnitEnum::unit)->andReturn(m::mock(GuardContract::class)); + $manager->shouldReceive('guard')->with(AuthGuardBackedEnum::Backed)->andReturn(m::mock(GuardContract::class)); return $manager; }); @@ -372,6 +388,26 @@ enum StorageDiskBackedEnum: string case Backed = 'backed'; } +enum AuthGuardUnitEnum +{ + case unit; +} + +enum AuthGuardBackedEnum: string +{ + case Backed = 'backed'; +} + +enum CacheStoreUnitEnum +{ + case unit; +} + +enum CacheStoreBackedEnum: string +{ + case Backed = 'backed'; +} + interface ContainerTestContract { } @@ -479,15 +515,23 @@ public function __construct(public bool $param) final class AuthedTest { - public function __construct(#[Authenticated('foo')] AuthenticatableContract $foo, #[CurrentUser('bar')] AuthenticatableContract $bar) - { + public function __construct( + #[Authenticated('foo')] AuthenticatableContract $foo, + #[CurrentUser('bar')] AuthenticatableContract $bar, + #[Authenticated(AuthGuardUnitEnum::unit)] AuthenticatableContract $unit, + #[CurrentUser(AuthGuardBackedEnum::Backed)] AuthenticatableContract $backed, + ) { } } final class CacheTest { - public function __construct(#[Cache('foo')] CacheRepository $foo, #[Cache('bar')] CacheRepository $bar) - { + public function __construct( + #[Cache('foo')] CacheRepository $foo, + #[Cache('bar')] CacheRepository $bar, + #[Cache(CacheStoreUnitEnum::unit)] CacheRepository $unit, + #[Cache(CacheStoreBackedEnum::Backed)] CacheRepository $backed, + ) { } } @@ -521,8 +565,12 @@ public function __construct(#[Database('foo')] Connection $foo, #[Database('bar' final class GuardTest { - public function __construct(#[Auth('foo')] GuardContract $foo, #[Auth('bar')] GuardContract $bar) - { + public function __construct( + #[Auth('foo')] GuardContract $foo, + #[Auth('bar')] GuardContract $bar, + #[Auth(AuthGuardUnitEnum::unit)] GuardContract $unit, + #[Auth(AuthGuardBackedEnum::Backed)] GuardContract $backed, + ) { } } From 7e5320693d8b3ed36e12db24882358f28bb4459b Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Wed, 13 May 2026 15:59:17 +0200 Subject: [PATCH 352/596] Add foreignUuidFor schema helper (#60091) Introduce an explicit Blueprint helper for UUID-backed Eloquent relationships so migrations can mirror foreignIdFor while keeping the column type intentionally UUID-based. --- src/Illuminate/Database/Schema/Blueprint.php | 18 +++++++++++++ .../Database/DatabaseSchemaBlueprintTest.php | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 5e1f8f68c507..7f51787dd3e4 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -1071,6 +1071,24 @@ public function foreignIdFor($model, $column = null) ->referencesModelColumn($model->getKeyName()); } + /** + * Create a foreign UUID column for the given model. + * + * @param \Illuminate\Database\Eloquent\Model|string $model + * @param string|null $column + * @return \Illuminate\Database\Schema\ForeignIdColumnDefinition + */ + public function foreignUuidFor($model, $column = null) + { + if (is_string($model)) { + $model = new $model; + } + + return $this->foreignUuid($column ?: $model->getForeignKey()) + ->table($model->getTable()) + ->referencesModelColumn($model->getKeyName()); + } + /** * Create a new float column on the table. * diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 61cf20c21824..e0a3b3602540 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -453,6 +453,19 @@ public function testGenerateRelationshipColumnWithUuidModel() ], $getSql('MySql')); } + public function testGenerateUuidRelationshipColumnWithUuidModel() + { + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'posts', function ($table) { + $table->foreignUuidFor(Fixtures\Models\EloquentModelUsingUuid::class); + })->toSql(); + }; + + $this->assertEquals([ + 'alter table `posts` add `model_using_uuid_id` char(36) not null', + ], $getSql('MySql')); + } + public function testGenerateRelationshipColumnWithUlidModel() { $getSql = function ($grammar) { @@ -484,6 +497,20 @@ public function testGenerateRelationshipConstrainedColumn() ], $getSql('MySql')); } + public function testGenerateUuidRelationshipConstrainedColumn() + { + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'posts', function ($table) { + $table->foreignUuidFor(Fixtures\Models\EloquentModelUsingUuid::class)->constrained(); + })->toSql(); + }; + + $this->assertEquals([ + 'alter table `posts` add `model_using_uuid_id` char(36) not null', + 'alter table `posts` add constraint `posts_model_using_uuid_id_foreign` foreign key (`model_using_uuid_id`) references `model` (`id`)', + ], $getSql('MySql')); + } + public function testGenerateRelationshipForModelWithNonStandardPrimaryKeyName() { $getSql = function ($grammar) { From 073a2446ef0c83f26ac080369d5cfd119d6048ff Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Wed, 13 May 2026 19:59:55 +0600 Subject: [PATCH 353/596] Add unicode modifier to SeeInHtml normalize whitespace regex (#60090) --- src/Illuminate/Testing/Constraints/SeeInHtml.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Testing/Constraints/SeeInHtml.php b/src/Illuminate/Testing/Constraints/SeeInHtml.php index cf9a17e79f66..16740f19c63b 100644 --- a/src/Illuminate/Testing/Constraints/SeeInHtml.php +++ b/src/Illuminate/Testing/Constraints/SeeInHtml.php @@ -122,7 +122,7 @@ protected function normalize(string $value): ?string $value = strip_tags($value); $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); $value = trim($value); - $value = preg_replace('/\s+/', ' ', $value); + $value = preg_replace('/\s+/u', ' ', $value); return $value; } From 7aee53c9256de6fd3f3492dc52014fdd3964e6ca Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Wed, 13 May 2026 20:00:22 +0600 Subject: [PATCH 354/596] Replace @return with @var on docblocks for properties (#60087) --- src/Illuminate/Mail/SendQueuedMailable.php | 2 +- src/Illuminate/Pagination/CursorPaginator.php | 2 +- src/Illuminate/Pagination/Paginator.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Mail/SendQueuedMailable.php b/src/Illuminate/Mail/SendQueuedMailable.php index 916ec250439e..df987cde931f 100644 --- a/src/Illuminate/Mail/SendQueuedMailable.php +++ b/src/Illuminate/Mail/SendQueuedMailable.php @@ -44,7 +44,7 @@ class SendQueuedMailable /** * The maximum number of unhandled exceptions to allow before failing. * - * @return int|null + * @var int|null */ public $maxExceptions; diff --git a/src/Illuminate/Pagination/CursorPaginator.php b/src/Illuminate/Pagination/CursorPaginator.php index 9c323562d98a..28765cfb7faf 100644 --- a/src/Illuminate/Pagination/CursorPaginator.php +++ b/src/Illuminate/Pagination/CursorPaginator.php @@ -28,7 +28,7 @@ class CursorPaginator extends AbstractCursorPaginator implements Arrayable, Arra /** * Indicates whether there are more items in the data source. * - * @return bool + * @var bool */ protected $hasMore; diff --git a/src/Illuminate/Pagination/Paginator.php b/src/Illuminate/Pagination/Paginator.php index bf43969fd8cd..724e6264f4b2 100644 --- a/src/Illuminate/Pagination/Paginator.php +++ b/src/Illuminate/Pagination/Paginator.php @@ -26,9 +26,9 @@ class Paginator extends AbstractPaginator implements Arrayable, ArrayAccess, Countable, IteratorAggregate, Jsonable, JsonSerializable, PaginatorContract { /** - * Determine if there are more items in the data source. + * Indicates if there are more items in the data source. * - * @return bool + * @var bool */ protected $hasMore; From d821f6ab061ec6239249197553d6720c030494d1 Mon Sep 17 00:00:00 2001 From: RP SOHAG <66528080+rpsohag@users.noreply.github.com> Date: Wed, 13 May 2026 20:00:37 +0600 Subject: [PATCH 355/596] Fix grammatical error in Lottery::alwaysLose() PHPDoc comment (#60086) The PHPDoc comment said "result in a lose" but "lose" is a verb. Changed to "loss" (noun) to match the corresponding alwaysWin() method which uses "win" (noun). --- src/Illuminate/Support/Lottery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Lottery.php b/src/Illuminate/Support/Lottery.php index 32e32955865b..f700b6967278 100644 --- a/src/Illuminate/Support/Lottery.php +++ b/src/Illuminate/Support/Lottery.php @@ -189,7 +189,7 @@ public static function alwaysWin($callback = null) } /** - * Force the lottery to always result in a lose. + * Force the lottery to always result in a loss. * * @param callable|null $callback * @return void From 7d4b11059489e16d95d2402c420d1d1e54bcb799 Mon Sep 17 00:00:00 2001 From: RP SOHAG <66528080+rpsohag@users.noreply.github.com> Date: Wed, 13 May 2026 20:01:32 +0600 Subject: [PATCH 356/596] Fix typo in Sleep::microsecond() PHPDoc comment (#60085) The PHPDoc comment said "Sleep for on microsecond" instead of "Sleep for one microsecond", which was inconsistent with similar methods like minute(), second(), and millisecond(). --- src/Illuminate/Support/Sleep.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index fc969ba6f0dc..35ef55685a38 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -239,7 +239,7 @@ public function microseconds() } /** - * Sleep for on microsecond. + * Sleep for one microsecond. * * @return $this */ From a0c6ad03b380287015287d8d5a0fa2459e2332fd Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 13 May 2026 15:38:40 +0000 Subject: [PATCH 357/596] Update version to v13.9.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 71ac4c3a4a28..9ba83aec2310 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 = '13.8.0'; + const VERSION = '13.9.0'; /** * The base path for the Laravel installation. From fdce39bfd72b53f7f339e0819420569888e8ea96 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 13 May 2026 15:40:52 +0000 Subject: [PATCH 358/596] Update CHANGELOG --- CHANGELOG.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9febb915454..1e2d2701c5bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,51 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.8.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.9.0...13.x) + +## [v13.9.0](https://github.com/laravel/framework/compare/v13.8.0...v13.9.0) - 2026-05-13 + +* [13.x] Fix issue using custom aws credential providers by [@iWader](https://github.com/iWader) in https://github.com/laravel/framework/pull/60000 +* [13.x] Revert "Correct Factory::configure [@return](https://github.com/return) to $this (#59963)" by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/60004 +* [13.x] Replace `mb_split` with `preg_split` by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/60012 +* [13.x] Keep calls to implode() consistent by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/60013 +* [13.x] update `rand()` to `mt_rand()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/60018 +* [13.x] remove `mt_srand()` deprecated "mode" argument by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/60020 +* [13.x] Remove useless `fail-fast` option by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/60019 +* [13.x] Prefer spaceship operator when possible by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/60015 +* [13.x] Fix incorrectly opened DocBlocks by [@CasEbb](https://github.com/CasEbb) in https://github.com/laravel/framework/pull/60014 +* [13.x] Ensure that the named arguments are sorted during a call by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/60017 +* [13.x] Rely on guzzlehttp/psr7 for nested multipart array expansion - Fix #59992 by [@RomainMazB](https://github.com/RomainMazB) in https://github.com/laravel/framework/pull/59984 +* [13.x] Add PreparesForDispatch interface for Jobs by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/59879 +* Add support to scoped filesystem for Cloud by [@jeremynikolic](https://github.com/jeremynikolic) in https://github.com/laravel/framework/pull/60030 +* [13.x] Unused `$parameters` in `validate*case()` by [@weshooper](https://github.com/weshooper) in https://github.com/laravel/framework/pull/60024 +* [13.x] Narrow attachment url scheme by [@benbjurstrom](https://github.com/benbjurstrom) in https://github.com/laravel/framework/pull/60034 +* [13.x] Skip allocation in mergeFillable/Appends/Hidden/Visible when input is empty by [@olivier-zenchef](https://github.com/olivier-zenchef) in https://github.com/laravel/framework/pull/60008 +* add generic return types to `Builder` paginate methods by [@levikl](https://github.com/levikl) in https://github.com/laravel/framework/pull/60045 +* [13.x] Make PendingDispatch conditionable by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/60047 +* [13.x] Display error in `queue:pause` when `Worker` isn't pausable by [@weshooper](https://github.com/weshooper) in https://github.com/laravel/framework/pull/60023 +* [13.x] Add tests for `Attachment::fromUrl()` URL scheme validation by [@mdalikadar](https://github.com/mdalikadar) in https://github.com/laravel/framework/pull/60054 +* [13.x] Fix [@params](https://github.com/params) typo in toPrettyJson docblocks by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/60050 +* [13.x] re-add docblock for `apply()` method by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/60055 +* [13.x] Add unicode modifier to preg_split by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/60056 +* [13.x] Add name to MigrationStarted/MigrationEnded events by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60059 +* [13.x] Ability to override the Worker timeout exit code by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60072 +* [13.x] Add method to convert a Password instance to a passwordrules string by [@imliam](https://github.com/imliam) in https://github.com/laravel/framework/pull/60070 +* add index for database performance by [@DGarbs51](https://github.com/DGarbs51) in https://github.com/laravel/framework/pull/60073 +* [13.x] Add optional disk storage for large SQS queue payloads by [@Orrison](https://github.com/Orrison) in https://github.com/laravel/framework/pull/59734 +* [13.x] Cloud queue metrics by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60074 +* [13.x] reset Lottery on test case teardown by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60083 +* [13.x] Add support for `after_commit` for Cloud queue metrics by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60078 +* [13.x] Remove Composer `github-oauth` credentials on Linux & Windows Actions by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60095 +* Revert "[13.x] Remove Composer `github-oauth` credentials on Linux & Windows Actions" by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60100 +* [13.x] Support config caching with Cloud queues by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60094 +* [13.x] Support Concurrency Run Timeouts by [@dbpolito](https://github.com/dbpolito) in https://github.com/laravel/framework/pull/60105 +* [13.x] Allow passing a Closure to `ThrottlesExceptions` middleware by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60103 +* [13.x] Add enum support to contextual attribute binding by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/60092 +* [13.x] Add foreignUuidFor schema helper by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/60091 +* [13.x] Add unicode modifier to SeeInHtml normalize whitespace regex by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/60090 +* [13.x] Replace [@return](https://github.com/return) with [@var](https://github.com/var) on property docblocks by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/60087 +* Fix grammatical error in Lottery::alwaysLose() PHPDoc comment by [@rpsohag](https://github.com/rpsohag) in https://github.com/laravel/framework/pull/60086 +* Fix typo in Sleep::microsecond() PHPDoc comment by [@rpsohag](https://github.com/rpsohag) in https://github.com/laravel/framework/pull/60085 ## [v13.8.0](https://github.com/laravel/framework/compare/v13.7.0...v13.8.0) - 2026-05-05 From 01e8beccabf36678288d23f9611431dc6a75ca1b Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 15 May 2026 01:26:09 +1000 Subject: [PATCH 359/596] Improve queue metric tests (#60124) --- tests/Foundation/Cloud/QueueTest.php | 277 +++++++++++++++++++++++++-- 1 file changed, 258 insertions(+), 19 deletions(-) diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index d491f9b99885..68bc3dce5695 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -14,10 +14,12 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Queue\Connectors\ConnectorInterface; use Illuminate\Queue\Connectors\SqsConnector; +use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Queue\Failed\FileFailedJobProvider; use Illuminate\Queue\Jobs\FakeJob; use Illuminate\Queue\SqsQueue; use Illuminate\Queue\Worker; +use Illuminate\Queue\WorkerStopReason; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; @@ -196,8 +198,7 @@ public function testItDoesNotBindCloudQueueWhenManagedQueuesIsInactive() public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() { $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, []); + [$queue] = $this->fakeQueue(); $queue->pop(); @@ -208,8 +209,7 @@ public function testItEmitsStartedEventWhenJobIsSuccessfullyPopped() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -226,8 +226,7 @@ public function testItEmitsProcessedEventWhenNextJobIsAboutToPop() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -255,8 +254,7 @@ public function testItDoesNotEmitEventsForTheSameJobAfterItHasBeenProcessed() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -271,8 +269,7 @@ public function testItRemembersTheQueueForTheProcessedEvent() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop = [new FakeJob, new FakeJob]; $queue->pop('first'); @@ -312,8 +309,7 @@ public function testItEmitsFailedJobEvents() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $failerFake = $this->fakeFailer(); $failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']); $failedJobProvider->setQueue($queue); @@ -359,8 +355,7 @@ public function testItEmitsReleasedJobEvents() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop[] = $jobFake = new FakeJob; $queue->pop(); @@ -446,6 +441,220 @@ public function testItEmitsJobQueuedEvent() ], $eventsFake->emitted); } + public function testItEmitsReleasedEventWhenWorkerStopsBecauseItTimedOut() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $this->travel(2)->seconds(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:07.060708', + 'type' => 'released', + 'queue' => 'default', + 'duration_ms' => 2000, + ], + ], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItEmitsProcessedEventWhenWorkerStopsForReasonsOtherThanTimedOut() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + $reasons = [ + WorkerStopReason::Interrupted, + WorkerStopReason::LostConnection, + WorkerStopReason::MaxJobsExceeded, + WorkerStopReason::MaxMemoryExceeded, + WorkerStopReason::MaxTimeExceeded, + WorkerStopReason::QueueEmpty, + WorkerStopReason::ReceivedRestartSignal, + ]; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + foreach ($reasons as $index => $reason) { + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, $reason)); + + $this->assertSame([ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 0, + ], $eventsFake->emitted[($index * 2) + 1]); + } + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItEmitsProcessedEventWhenWorkerStopsWithoutAReason() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->app['events']->dispatch(new WorkerStopping); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testWorkerStoppingListenerEmitsFailedTypeWhenProcessingJobHasFailed() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->fail(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + + $this->assertSame('failed', $eventsFake->emitted[1]['type']); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testWorkerStoppingListenerEmitsReleasedTypeWhenProcessingJobWasReleased() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->release(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::MaxJobsExceeded)); + + $this->assertSame('released', $eventsFake->emitted[1]['type']); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testWorkerStoppingListenerDoesNothingWhenNoJobIsProcessing() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + $this->fakeQueue(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::QueueEmpty)); + + $this->assertSame([], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItDoesNotRegisterWorkerStoppingListenerWhenNotRunningQueueWork() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'tinker']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + ], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + public function testItRespectsDispatchAfterTransaction() { $this->travelTo('2000-01-02 03:04:05.060708'); @@ -518,8 +727,7 @@ public function testItCapturesDurationForMultipleJobs() { $this->travelTo('2000-01-02 03:04:05.060708'); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop = [new FakeJob, new FakeJob]; $queue->pop(); @@ -537,8 +745,7 @@ public function testItCapturesUtcTime() date_default_timezone_set('Australia/Melbourne'); $this->travelTo(Carbon::parse('2000-01-02 03:04:05.060708', 'Australia/Melbourne')); $eventsFake = $this->fakeEvents(); - $queueFake = $this->fakeQueue(); - $queue = new Queue($queueFake, $eventsFake, $this->app['config']->get('queue.connections.sqs')); + [$queue, $queueFake] = $this->fakeQueue(); $queueFake->jobsToPop[] = new FakeJob; $queue->pop(); @@ -749,6 +956,8 @@ public function connect($config) } }, $this->app)); + $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + return [$this->app['queue']->connection('sqs'), $client]; } @@ -768,9 +977,12 @@ public function emitMany(array $payloads): void }); } + /** + * @return array{Queue, object{jobsToPop: array}} + */ private function fakeQueue() { - return new class($this->app, [], null) extends QueueFake + $fakeQueue = new class($this->app, [], null) extends QueueFake { public array $jobsToPop = []; @@ -785,7 +997,34 @@ public function getQueue($queue) return config('queue.connections.sqs.prefix').'/'.$queue.config('queue.connections.sqs.suffix'); } + + public function setConfig(array $config) + { + return $this; + } + + public function setContainer($container) + { + return $this; + } }; + + $this->app->instance(QueueConnector::class, new QueueConnector(new class($fakeQueue) implements ConnectorInterface + { + public function __construct(private $fakeQueue) + { + // + } + + public function connect($config) + { + return $this->fakeQueue; + } + }, $this->app)); + + $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + + return [$this->app['queue']->connection('sqs'), $fakeQueue]; } private function fakeFailer() From a53646d1da80a0a15e0ddff158b76742583a38c4 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 15 May 2026 01:26:19 +1000 Subject: [PATCH 360/596] [12.x] Back port cloud queues (#60122) * [13.x] Cloud queue metrics (#60074) * Cloud queue metrics * Reset static state before running tests * Add binding test * formatting --------- Co-authored-by: Taylor Otwell * Support `after_commit` for queue metrics (#60078) * Use config for queue suffix and prefix (#60094) * Improve queue metric tests * Back port #58341 * Back port #59310 * Back port #59370 * Lint --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Cloud.php | 72 +- src/Illuminate/Foundation/Cloud/Events.php | 217 ++++ .../Foundation/Cloud/FailedJobProvider.php | 202 ++++ src/Illuminate/Foundation/Cloud/Queue.php | 397 +++++++ .../Foundation/Cloud/QueueConnector.php | 93 ++ .../Queue/Events/WorkerStopping.php | 2 + src/Illuminate/Queue/Worker.php | 46 +- src/Illuminate/Queue/WorkerStopReason.php | 15 + tests/Foundation/Cloud/QueueTest.php | 1042 +++++++++++++++++ .../Foundation/FoundationAliasLoaderTest.php | 8 + tests/Integration/Foundation/CloudTest.php | 68 -- tests/Queue/QueueWorkerTest.php | 52 +- 12 files changed, 2109 insertions(+), 105 deletions(-) create mode 100644 src/Illuminate/Foundation/Cloud/Events.php create mode 100644 src/Illuminate/Foundation/Cloud/FailedJobProvider.php create mode 100644 src/Illuminate/Foundation/Cloud/Queue.php create mode 100644 src/Illuminate/Foundation/Cloud/QueueConnector.php create mode 100644 src/Illuminate/Queue/WorkerStopReason.php create mode 100644 tests/Foundation/Cloud/QueueTest.php diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index c0c0e2e40350..5cae0e761622 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -3,9 +3,13 @@ namespace Illuminate\Foundation; use Illuminate\Database\Migrations\Migrator; +use Illuminate\Foundation\Bootstrap\BootProviders; use Illuminate\Foundation\Bootstrap\HandleExceptions; use Illuminate\Foundation\Bootstrap\LoadConfiguration; -use Illuminate\Queue\Worker; +use Illuminate\Foundation\Cloud\Events; +use Illuminate\Foundation\Cloud\FailedJobProvider; +use Illuminate\Foundation\Cloud\QueueConnector; +use Illuminate\Queue\Connectors\SqsConnector; use Monolog\Formatter\JsonFormatter; use Monolog\Handler\SocketHandler; use PDO; @@ -35,6 +39,9 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); }, + BootProviders::class => function () use ($app) { + static::bootManagedQueues($app); + }, default => fn () => true, })(); } @@ -119,24 +126,39 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): */ public static function configureManagedQueues(Application $app): void { - if ((int) ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? 0) === 1) { - Worker::$restartable = false; - Worker::$pausable = false; + if (! Cloud::managedQueuesAreActive()) { + return; + } - $app['config']->set( - 'queue.connections.sqs.credentials', - 'ecs' - ); + $app['config']->set('queue.connections.sqs.credentials', 'ecs'); - if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { - $app['config']->set( - 'queue.connections.sqs.region', - $_SERVER['LARAVEL_CLOUD_REGION'] - ); - } + if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { + $app['config']->set('queue.connections.sqs.region', $_SERVER['LARAVEL_CLOUD_REGION']); } } + /** + * Boot managed queues if applicable. + */ + public static function bootManagedQueues(Application $app): void + { + if (! Cloud::managedQueuesAreActive()) { + return; + } + + $app->singleton(Events::class, fn () => new Events(Cloud::socket())); + $app->bind(QueueConnector::class, fn ($app) => new QueueConnector(new SqsConnector, $app)); + + $app['queue']->addConnector('sqs', $app->factory(QueueConnector::class)); + + $failer = $app['queue.failer']; + unset($app['queue.failer']); + + $app->singleton('queue.failer', fn ($app) => new FailedJobProvider( + $failer, $app[Events::class], $app['encrypter'], + )); + } + /** * Configure the Laravel Cloud log channels. */ @@ -155,11 +177,27 @@ public static function configureCloudLogging(Application $app): void 'includeStacktraces' => true, ], 'with' => [ - 'connectionString' => $_ENV['LARAVEL_CLOUD_LOG_SOCKET'] ?? - $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? - 'unix:///tmp/cloud-init.sock', + 'connectionString' => Cloud::socket(), 'persistent' => true, ], ]); } + + /** + * The cloud socket address. + */ + protected static function socket(): string + { + return $_ENV['LARAVEL_CLOUD_LOG_SOCKET'] ?? + $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? + 'unix:///tmp/cloud-init.sock'; + } + + /** + * Determine if managed queues are active. + */ + protected static function managedQueuesAreActive(): bool + { + return ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? null) === '1'; + } } diff --git a/src/Illuminate/Foundation/Cloud/Events.php b/src/Illuminate/Foundation/Cloud/Events.php new file mode 100644 index 000000000000..2d90ccad5b11 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/Events.php @@ -0,0 +1,217 @@ + $payload + */ + public function emit(array $payload): void + { + $this->emitMany([$payload]); + } + + /** + * Emit many events. + * + * @param list> $payloads + */ + public function emitMany(array $payloads): void + { + if ($payloads === []) { + return; + } + + try { + $this->ensureConnected(); + + $this->write($this->format($payloads)); + } catch (Throwable) { + // + } + } + + /** + * Write the payload to the socket. + * + * @param list> $payloads + */ + protected function write(string $payload): void + { + $originalPayloadLength = strlen($payload); + $written = 0; + $zeroLengthWrites = 0; + + while (true) { + $thisWrite = @fwrite($this->socket, $payload); + + if ($thisWrite === false) { + $e = new RuntimeException($this->withSocketMetaData('Unable to write to socket')); + + $this->disconnect(); + + throw $e; + } + + $written += $thisWrite; + + if ($written >= $originalPayloadLength) { + return; + } + + if ($thisWrite === 0) { + $zeroLengthWrites++; + } + + if ($zeroLengthWrites >= 5) { + $e = new RuntimeException($this->withSocketMetaData('Unable to write bytes to socket')); + + $this->disconnect(); + + throw $e; + } + + $payload = substr($payload, $thisWrite); + } + } + + /** + * Format the payload. + * + * @param list> $payloads + */ + protected function format(array $payloads): string + { + return array_reduce($payloads, function (string $carry, array $line) { + if ($carry !== '') { + $carry .= "\n"; + } + + return $carry .= json_encode($line, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE); + }, '')."\n"; + } + + /** + * Ensure the socket is connected. + */ + protected function ensureConnected(): void + { + if (! $this->connected()) { + $this->connect(); + } + } + + /** + * Connect the socket. + */ + protected function connect(): void + { + $socket = stream_socket_client( + address: $this->address, + error_code: $errorCode, + error_message: $errorMessage, + timeout: 2, + flags: STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, + ); + + if ($socket === false) { + throw new RuntimeException("Failed connecting to the socket: {$errorMessage} [{$errorCode}]"); + } + + if (! stream_set_timeout($socket, 2)) { + $e = new RuntimeException($this->withSocketMetaData('Failed configuring socket timeout')); + + $this->disconnect(); + + throw $e; + } + + $this->socket = $socket; + } + + /** + * Determine if the socket is connected. + */ + protected function connected(): bool + { + if (gettype($this->socket) !== 'resource') { + return false; + } + + if (feof($this->socket)) { + $this->disconnect(); + + return false; + } + + return true; + } + + /** + * Disconnect the socket. + */ + protected function disconnect(): void + { + if (gettype($this->socket) !== 'resource') { + $this->socket = null; + + return; + } + + try { + fclose($this->socket); + } catch (Throwable) { + // + } + + $this->socket = null; + } + + /** + * Decorate the message with the socket's meta data. + */ + protected function withSocketMetaData(string $message): string + { + $prefix = "{$message}\n---\n"; + + if (! $this->connected()) { + return "{$prefix}closed: true"; + } + + $meta = stream_get_meta_data($this->socket); + + return $prefix.array_reduce(array_keys($meta), function ($carry, $key) use ($meta) { + try { + return $carry.$key.': '.match ($meta[$key]) { + true => 'true', + false => 'false', + default => $meta[$key], + }."\n"; + } catch (Throwable) { + return $carry; + } + }, ''); + } +} diff --git a/src/Illuminate/Foundation/Cloud/FailedJobProvider.php b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php new file mode 100644 index 000000000000..9c0dd8dad795 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php @@ -0,0 +1,202 @@ + + */ + protected $loadedFailedJobs = []; + + /** + * Create a new instance. + */ + public function __construct( + protected FailedJobProviderInterface $failer, + protected Events $events, + protected StringEncrypter $encrypter, + ) { + // + } + + /** + * Log a failed job into storage. + * + * @param string $connection + * @param string $queue + * @param string $payload + * @param \Throwable $exception + * @return string|null + */ + public function log($connection, $queue, $payload, $exception) + { + if ($connection !== 'sqs') { + return $this->failer->log(...func_get_args()); + } + + if ($this->queue === null) { + throw new RuntimeException('The failed job provider does not have a configured queue'); + } + + $timestamp = CarbonImmutable::now('UTC'); + $processingJobDetails = $this->queue->processingJobDetails(); + + $this->events->emit([ + '_cloud_event' => 'failed_job', + 'id' => $id = Str::uuid7($timestamp)->toString(), + 'queue' => $processingJobDetails['queue'], + 'started_at' => $processingJobDetails['started_at']->toDateTimeString('microsecond'), + 'attempts' => $processingJobDetails['attempts'], + 'payload' => $payload, + 'exception' => (string) mb_convert_encoding($exception, 'UTF-8'), + ]); + + $this->queue->finishProcessingJob(timestamp: $timestamp); + + return $id; + } + + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return $this->failer->ids(...func_get_args()); + } + + /** + * Get a list of all of the failed jobs. + * + * @return array + */ + public function all() + { + return $this->failer->all(...func_get_args()); + } + + /** + * Get a single failed job. + * + * @param mixed $id + * @return object|null + */ + public function find($id) + { + if (! str_starts_with($id, 'https://')) { + return $this->failer->find($id); + } + + $response = Http::connectTimeout(10) + ->timeout(10) + ->retry(3, 1000, fn ($exception) => $exception instanceof ConnectionException) + ->throw() + ->get($id); + + return $this->loadedFailedJobs[$id] = json_decode($this->encrypter->decryptString($response->body()), flags: JSON_THROW_ON_ERROR); + } + + /** + * Delete a single failed job from storage. + * + * @param mixed $id + * @return bool + */ + public function forget($id) + { + if (! str_starts_with($id, 'https://')) { + return $this->failer->forget($id); + } + + if (is_null($job = $this->loadedFailedJobs[$id] ?? null)) { + return false; + } + + $this->events->emit([ + '_cloud_event' => 'failed_job', + 'id' => $job->id, + 'queue' => $job->queue, + 'retried_at' => CarbonImmutable::now('UTC')->toDateTimeString('microsecond'), + ]); + + return true; + } + + /** + * Flush all of the failed jobs from storage. + * + * @param int|null $hours + * @return void + */ + public function flush($hours = null) + { + $this->failer->flush(...func_get_args()); + } + + /** + * Count the failed jobs. + * + * @param string|null $connection + * @param string|null $queue + * @return int + */ + public function count($connection = null, $queue = null) + { + if (! $this->failer instanceof CountableFailedJobProvider) { + return 0; + } + + return $this->failer->count(...func_get_args()); + } + + /** + * Prune all of the entries older than the given date. + * + * @param \DateTimeInterface $before + * @return int + */ + public function prune(DateTimeInterface $before) + { + if (! $this->failer instanceof PrunableFailedJobProvider) { + return 0; + } + + return $this->failer->prune(...func_get_args()); + } + + /** + * Set the connected queue instance. + * + * @param \Illuminate\Foundation\Cloud\Queue $queue + * @return $this + */ + public function setQueue($queue) + { + $this->queue = $queue; + + return $this; + } +} diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php new file mode 100644 index 000000000000..fe3c5c611924 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -0,0 +1,397 @@ +prefix = array_key_exists('prefix', $config) && is_string($config['prefix']) + ? $config['prefix'].'/' + : ''; + + $this->suffix = $config['suffix'] ?? ''; + } + + /** + * Get the size of the queue. + * + * @param string|null $queue + * @return int + */ + public function size($queue = null) + { + return $this->queue->size(...func_get_args()); + } + + /** + * Get the number of pending jobs. + * + * @param string|null $queue + * @return int + */ + public function pendingSize($queue = null) + { + return $this->queue->pendingSize(...func_get_args()); + } + + /** + * Get the number of delayed jobs. + * + * @param string|null $queue + * @return int + */ + public function delayedSize($queue = null) + { + return $this->queue->delayedSize(...func_get_args()); + } + + /** + * Get the number of reserved jobs. + * + * @param string|null $queue + * @return int + */ + public function reservedSize($queue = null) + { + return $this->queue->reservedSize(...func_get_args()); + } + + /** + * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * + * @param string|null $queue + * @return int|null + */ + public function creationTimeOfOldestPendingJob($queue = null) + { + return $this->queue->creationTimeOfOldestPendingJob(...func_get_args()); + } + + /** + * Push a new job onto the queue. + * + * @param string|object $job + * @param mixed $data + * @param string|null $queue + * @return mixed + */ + public function push($job, $data = '', $queue = null) + { + return $this->queue->push(...func_get_args()); + } + + /** + * Push a new job onto the queue. + * + * @param string $queue + * @param string|object $job + * @param mixed $data + * @return mixed + */ + public function pushOn($queue, $job, $data = '') + { + return $this->queue->pushOn(...func_get_args()); + } + + /** + * Push a raw payload onto the queue. + * + * @param string $payload + * @param string|null $queue + * @return mixed + */ + public function pushRaw($payload, $queue = null, array $options = []) + { + $result = $this->queue->pushRaw(...func_get_args()); + + $this->finishQueueingJob($queue); + + return $result; + } + + /** + * Push a new job onto the queue after (n) seconds. + * + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string|null $queue + * @return mixed + */ + public function later($delay, $job, $data = '', $queue = null) + { + return $this->queue->later(...func_get_args()); + } + + /** + * Push a new job onto a specific queue after (n) seconds. + * + * @param string $queue + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @return mixed + */ + public function laterOn($queue, $delay, $job, $data = '') + { + return $this->queue->laterOn(...func_get_args()); + } + + /** + * Push an array of jobs onto the queue. + * + * @param array $jobs + * @param mixed $data + * @param string|null $queue + * @return mixed + */ + public function bulk($jobs, $data = '', $queue = null) + { + return $this->queue->bulk(...func_get_args()); + } + + /** + * Pop the next job off of the queue. + * + * @param string|null $queue + * @return \Illuminate\Contracts\Queue\Job|null + */ + public function pop($queue = null) + { + $this->finishProcessingJob(); + + $job = $this->queue->pop(...func_get_args()); + + $this->startProcessingJob($queue, $job); + + return $job; + } + + /** + * Delete all of the jobs from the queue. + * + * @param string $queue + * @return int + */ + public function clear($queue) + { + return $this->queue->clear(...func_get_args()); + } + + /** + * Get the connection name for the queue. + * + * @return string + */ + public function getConnectionName() + { + return $this->queue->getConnectionName(); + } + + /** + * Set the connection name for the queue. + * + * @param string $name + * @return $this + */ + public function setConnectionName($name) + { + $this->queue->setConnectionName(...func_get_args()); + + return $this; + } + + /** + * Set the queue configuration array. + * + * @param array $config + * @return $this + */ + public function setConfig($config) + { + $this->queue->setConfig(...func_get_args()); + + return $this; + } + + /** + * Get the queueable options from the job. + * + * @param mixed $job + * @param string|null $queue + * @param string $payload + * @param \DateTimeInterface|\DateInterval|int|null $delay + * @return array{DelaySeconds?: int, MessageGroupId?: string, MessageDeduplicationId?: string} + */ + public function getQueueableOptions($job, $queue, $payload, $delay = null): array + { + if (! method_exists($this->queue, 'getQueueableOptions')) { + return []; + } + + return $this->queue->getQueueableOptions(...func_get_args()); + } + + /** + * Finish processing the current job and emit a queue event. + * + * @param string $default + * @param \Carbon\CarbonImmutable|null $timestamp + * @return void + */ + public function finishProcessingJob($default = 'processed', $timestamp = null) + { + if (! $this->processingJob) { + return; + } + + $timestamp ??= CarbonImmutable::now('UTC'); + + $this->events->emit([ + '_cloud_event' => 'queue', + 'timestamp' => $timestamp->toDateTimeString('microsecond'), + 'type' => match (true) { + $this->processingJob->hasFailed() => 'failed', + $this->processingJob->isReleased() => 'released', + default => $default, + }, + 'queue' => $this->processingQueue, + 'duration_ms' => (int) $this->processingJobStartedAt->diffInMilliseconds($timestamp), + ]); + + $this->processingQueue + = $this->processingJob + = $this->processingJobStartedAt + = null; + } + + /** + * Last job details resolver. + * + * @return array{queue: string, attempts: int, started_at: CarbonImmutable} + */ + public function processingJobDetails() + { + return [ + 'queue' => $this->processingQueue, + 'attempts' => $this->processingJob->attempts(), + 'started_at' => $this->processingJobStartedAt, + ]; + } + + /** + * Handle jobs finishing being queued. + * + * @param string $queue + */ + public function finishQueueingJob($queue) + { + $this->events->emit([ + '_cloud_event' => 'queue', + 'timestamp' => CarbonImmutable::now('UTC')->toDateTimeString('microsecond'), + 'type' => 'queued', + 'queue' => $this->normalizeQueue($queue), + ]); + } + + /** + * Handle a job being popped. + * + * @param string|null $queue + * @param \Illuminate\Contracts\Queue\Job|null $job + * @return void + */ + protected function startProcessingJob($queue, $job) + { + if (! $job) { + return; + } + + $this->processingJob = $job; + $this->processingQueue = $this->normalizeQueue($queue); + $this->processingJobStartedAt = CarbonImmutable::now('UTC'); + + $this->events->emit([ + '_cloud_event' => 'queue', + 'timestamp' => $this->processingJobStartedAt->toDateTimeString('microsecond'), + 'type' => 'started', + 'queue' => $this->processingQueue, + ]); + } + + /** + * Normalize the queue name. + * + * @param string|null $queue + * @return string + */ + protected function normalizeQueue($queue) + { + return Str::of($this->queue->getQueue($queue)) + ->chopStart($this->prefix) + ->chopEnd($this->suffix) + ->toString(); + } + + /** + * Dynamically pass method calls to the underlying queue. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->forwardDecoratedCallTo($this->queue, $method, $parameters); + } +} diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php new file mode 100644 index 000000000000..23926b1174ac --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -0,0 +1,93 @@ +connector->connect($config), + $this->app[Events::class], + $config, + ); + + $this->configureQueue($queue); + + if (! $this->app->runningConsoleCommand('queue:work')) { + return $queue; + } + + $this->configureWorker($queue); + $this->configureFailedJobProvider($queue); + + return $queue; + } + + /** + * Configure the queue. + */ + protected function configureQueue(Queue $queue): void + { + $this->app['events']->listen(fn (JobQueued $event) => $event->connectionName === $queue->getConnectionName() + ? $queue->finishQueueingJob($event->queue) + : null); + } + + /** + * Configure the queue worker. + */ + protected function configureWorker(Queue $queue): void + { + Worker::$restartable = false; + Worker::$pausable = false; + + $this->app['events']->listen(fn (WorkerStopping $event) => match ($event->reason) { + WorkerStopReason::TimedOut => $queue->finishProcessingJob(default: 'released'), + default => $queue->finishProcessingJob(), + }); + + static::$reservedMemory = str_repeat('x', 32768); + + register_shutdown_function(function () use ($queue) { + static::$reservedMemory = null; + + if (! is_null($error = error_get_last()) && in_array($error['type'], [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE])) { + $queue->finishProcessingJob(default: 'released'); + } + }); + } + + /** + * Configure the failed job provider. + */ + protected function configureFailedJobProvider(Queue $queue): void + { + $this->app['queue.failer']->setQueue($queue); + } +} diff --git a/src/Illuminate/Queue/Events/WorkerStopping.php b/src/Illuminate/Queue/Events/WorkerStopping.php index d4cf0ef1fbf0..fe996bbdead1 100644 --- a/src/Illuminate/Queue/Events/WorkerStopping.php +++ b/src/Illuminate/Queue/Events/WorkerStopping.php @@ -9,10 +9,12 @@ class WorkerStopping * * @param int $status The worker exit status. * @param \Illuminate\Queue\WorkerOptions|null $workerOptions The worker options. + * @param \Illuminate\Queue\WorkerStopReason|null $reason The reason why the worker is stopping. */ public function __construct( public $status = 0, public $workerOptions = null, + public $reason = null, ) { } } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index b2cb656863d7..946b311258ef 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -85,6 +85,13 @@ class Worker */ public $shouldQuit = false; + /** + * Indicates if the worker lost its connection. + * + * @var bool + */ + public $lostConnection = false; + /** * Indicates if the worker is paused. * @@ -168,10 +175,10 @@ public function daemon($connectionName, $queue, WorkerOptions $options) // if it is we will just pause this worker for a given amount of time and // make sure we do not need to kill this worker process off completely. if (! $this->daemonShouldRun($options, $connectionName, $queue)) { - $status = $this->pauseWorker($options, $lastRestart); + [$status, $reason] = $this->pauseWorker($options, $lastRestart); if (! is_null($status)) { - return $this->stop($status, $options); + return $this->stop($status, $options, $reason); } continue; @@ -214,12 +221,12 @@ public function daemon($connectionName, $queue, WorkerOptions $options) // Finally, we will check to see if we have exceeded our memory limits or if // the queue should restart based on other indications. If so, we'll stop // this worker and let whatever is "monitoring" it restart the process. - $status = $this->stopIfNecessary( + [$status, $reason] = $this->stopIfNecessary( $options, $lastRestart, $startTime, $jobsProcessed, $job ); if (! is_null($status)) { - return $this->stop($status, $options); + return $this->stop($status, $options, $reason); } } } @@ -255,7 +262,7 @@ protected function registerTimeoutHandler($job, WorkerOptions $options) )); } - $this->kill(static::EXIT_ERROR, $options); + $this->kill(static::EXIT_ERROR, $options, WorkerStopReason::TimedOut); }, true); pcntl_alarm( @@ -305,7 +312,7 @@ protected function daemonShouldRun(WorkerOptions $options, $connectionName, $que * * @param \Illuminate\Queue\WorkerOptions $options * @param int $lastRestart - * @return int|null + * @return array|null */ protected function pauseWorker(WorkerOptions $options, $lastRestart) { @@ -322,17 +329,18 @@ protected function pauseWorker(WorkerOptions $options, $lastRestart) * @param int $startTime * @param int $jobsProcessed * @param mixed $job - * @return int|null + * @return array|null */ protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $job = null) { return match (true) { - $this->shouldQuit => static::EXIT_SUCCESS, - $this->memoryExceeded($options->memory) => static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, - $this->queueShouldRestart($lastRestart) => static::EXIT_SUCCESS, - $options->stopWhenEmpty && is_null($job) => static::EXIT_SUCCESS, - $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => static::EXIT_SUCCESS, - $options->maxJobs && $jobsProcessed >= $options->maxJobs => static::EXIT_SUCCESS, + $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], + $this->shouldQuit => [static::EXIT_SUCCESS, WorkerStopReason::Interrupted], + $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], + $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], + $options->stopWhenEmpty && is_null($job) => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmpty], + $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], + $options->maxJobs && $jobsProcessed >= $options->maxJobs => [static::EXIT_SUCCESS, WorkerStopReason::MaxJobsExceeded], default => null }; } @@ -449,7 +457,7 @@ protected function runJob($job, $connectionName, WorkerOptions $options) protected function stopWorkerIfLostConnection($e) { if ($this->causedByLostConnection($e)) { - $this->shouldQuit = true; + $this->lostConnection = true; } } @@ -824,11 +832,12 @@ public function memoryExceeded($memoryLimit) * * @param int $status * @param WorkerOptions|null $options + * @param WorkerStopReason|null $reason * @return int */ - public function stop($status = 0, $options = null) + public function stop($status = 0, $options = null, $reason = null) { - $this->events->dispatch(new WorkerStopping($status, $options)); + $this->events->dispatch(new WorkerStopping($status, $options, $reason)); return $status; } @@ -838,11 +847,12 @@ public function stop($status = 0, $options = null) * * @param int $status * @param \Illuminate\Queue\WorkerOptions|null $options + * @param \Illuminate\Queue\WorkerStopReason|null $reason * @return never */ - public function kill($status = 0, $options = null) + public function kill($status = 0, $options = null, $reason = null) { - $this->events->dispatch(new WorkerStopping($status, $options)); + $this->events->dispatch(new WorkerStopping($status, $options, $reason)); if (extension_loaded('posix')) { posix_kill(getmypid(), SIGKILL); diff --git a/src/Illuminate/Queue/WorkerStopReason.php b/src/Illuminate/Queue/WorkerStopReason.php new file mode 100644 index 000000000000..8591e94743bc --- /dev/null +++ b/src/Illuminate/Queue/WorkerStopReason.php @@ -0,0 +1,15 @@ +set('app.key', Str::random(32)); + } + + protected function setUp(): void + { + Worker::$restartable = true; + Worker::$pausable = true; + $_SERVER['LARAVEL_CLOUD'] = $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + + parent::setUp(); + + $this->app['config']->set([ + 'queue.connections.sqs.prefix' => 'https://sqs.us-east-2.amazonaws.com/1234567', + 'queue.connections.sqs.suffix' => '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', + ]); + } + + protected function tearDown(): void + { + parent::tearDown(); + + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + Worker::$restartable = true; + Worker::$pausable = true; + } + + public function testItDisablesQueueRestartPollingForManagedQueues() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + Cloud::bootManagedQueues($this->app); + $this->assertTrue(Worker::$restartable); + + $this->app['queue']->connection('sqs'); + $this->assertFalse(Worker::$restartable); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItDisablesQueuePausePollingForManagedQueues() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + Cloud::bootManagedQueues($this->app); + $this->assertTrue(Worker::$pausable); + + $this->app['queue']->connection('sqs'); + $this->assertFalse(Worker::$pausable); + } finally { + $_SERVER['argv'] = $argv; + } + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function testItConfiguresManagedQueueCredentials() + { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function testItDoesNotConfigureManagedQueuesWhenNotEnabled() + { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + Cloud::configureManagedQueues($this->app); + + $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); + } + + #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] + public function testItConfiguresManagedQueueRegion() + { + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; + + try { + Cloud::configureManagedQueues($this->app); + + $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); + } finally { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + } + } + + public function testItSetSqsCredentialsToEcs() + { + $this->assertSame(null, Config::get('queue.connections.sqs.credentials')); + + Cloud::configureManagedQueues($this->app); + + $this->assertSame('ecs', Config::get('queue.connections.sqs.credentials')); + } + + public function testItSetsTheSqsRegion() + { + $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); + + Cloud::configureManagedQueues($this->app); + $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); + + $_SERVER['LARAVEL_CLOUD_REGION'] = 'eu-central-1'; + Cloud::configureManagedQueues($this->app); + + $this->assertSame('eu-central-1', Config::get('queue.connections.sqs.region')); + } + + public function testItBindsQueueConnectorAndNewsUpSqsConnector() + { + $this->app->bind(SqsConnector::class, fn () => throw new RuntimeException('Should not be resolved')); + Cloud::bootManagedQueues($this->app); + + $this->app[QueueConnector::class]; + } + + public function testItBindsCloudQueue() + { + Cloud::bootManagedQueues($this->app); + + $this->assertInstanceOf(Queue::class, $this->app['queue']->connection('sqs')); + } + + public function testItBindsCloudEventsAsSingleton() + { + Cloud::bootManagedQueues($this->app); + + $this->assertFalse($this->app->resolved(Events::class)); + $this->assertSame($this->app[Events::class], $this->app[Events::class]); + } + + public function testItBindsTheQueueFailer() + { + Cloud::bootManagedQueues($this->app); + + $this->assertInstanceOf(FailedJobProvider::class, $this->app['queue.failer']); + } + + public function testItDoesNotBindCloudQueueWhenManagedQueuesIsInactive() + { + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + + Cloud::bootManagedQueues($this->app); + + $this->assertInstanceOf(SqsQueue::class, $this->app['queue']->connection('sqs')); + } + + public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() + { + $eventsFake = $this->fakeEvents(); + [$queue] = $this->fakeQueue(); + + $queue->pop(); + + $this->assertSame([], $eventsFake->emitted); + } + + public function testItEmitsStartedEventWhenJobIsSuccessfullyPopped() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->assertSame([[ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ]], $eventsFake->emitted); + } + + public function testItEmitsProcessedEventWhenNextJobIsAboutToPop() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $this->travel(1)->second(); + $queue->pop(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:06.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 1000, + ], + ], $eventsFake->emitted); + } + + public function testItDoesNotEmitEventsForTheSameJobAfterItHasBeenProcessed() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $queue->pop(); + $queue->pop(); + $queue->pop(); + + $this->assertCount(2, $eventsFake->emitted); + } + + public function testItRemembersTheQueueForTheProcessedEvent() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop = [new FakeJob, new FakeJob]; + $queue->pop('first'); + $queue->pop('second'); + $queue->pop('third'); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'first', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'first', + 'duration_ms' => 0, + ], [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'second', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'second', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } + + public function testItEmitsFailedJobEvents() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + $failerFake = $this->fakeFailer(); + $failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']); + $failedJobProvider->setQueue($queue); + $this->app[FailedJobProvider::class] = $failedJobProvider; + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->fail(); + Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]); + $failedJobProvider->log('sqs', 'default', ['payload' => 'here'], new RuntimeException('Whoops!')); + Str::createUuidsNormally(); + $queue->pop(); + + unset($eventsFake->emitted[1]['exception']); + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'failed_job', + 'id' => '00dc709e-90c4-70c2-87c8-9b7127d20e8f', + 'queue' => 'default', + 'started_at' => '2000-01-02 03:04:05.060708', + 'attempts' => 1, + 'payload' => [ + 'payload' => 'here', + ], + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'failed', + 'queue' => 'default', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } + + public function testItEmitsReleasedJobEvents() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->release(); + $queue->pop(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'released', + 'queue' => 'default', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } + + public function testItEmitsJobQueuedEvent() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); + + $queue->push(new FakeJob, queue: '1'); + $queue->pushOn('2', new FakeJob); + $queue->pushRaw('', queue: '3'); + $queue->later(1, new FakeJob, queue: '4'); + $queue->laterOn('5', 1, new FakeJob); + $queue->bulk([new FakeJob, new FakeJob], queue: '6'); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '1', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '2', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '3', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '4', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '5', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + ], $eventsFake->emitted); + } + + public function testItEmitsReleasedEventWhenWorkerStopsBecauseItTimedOut() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $this->travel(2)->seconds(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:07.060708', + 'type' => 'released', + 'queue' => 'default', + 'duration_ms' => 2000, + ], + ], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItEmitsProcessedEventWhenWorkerStopsForReasonsOtherThanTimedOut() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + $reasons = [ + WorkerStopReason::Interrupted, + WorkerStopReason::LostConnection, + WorkerStopReason::MaxJobsExceeded, + WorkerStopReason::MaxMemoryExceeded, + WorkerStopReason::MaxTimeExceeded, + WorkerStopReason::QueueEmpty, + WorkerStopReason::ReceivedRestartSignal, + ]; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + foreach ($reasons as $index => $reason) { + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, $reason)); + + $this->assertSame([ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 0, + ], $eventsFake->emitted[($index * 2) + 1]); + } + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItEmitsProcessedEventWhenWorkerStopsWithoutAReason() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->app['events']->dispatch(new WorkerStopping); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 0, + ], + ], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testWorkerStoppingListenerEmitsFailedTypeWhenProcessingJobHasFailed() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->fail(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + + $this->assertSame('failed', $eventsFake->emitted[1]['type']); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testWorkerStoppingListenerEmitsReleasedTypeWhenProcessingJobWasReleased() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = $jobFake = new FakeJob; + $queue->pop(); + $jobFake->release(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::MaxJobsExceeded)); + + $this->assertSame('released', $eventsFake->emitted[1]['type']); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testWorkerStoppingListenerDoesNothingWhenNoJobIsProcessing() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'queue:work']; + + try { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + $this->fakeQueue(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::QueueEmpty)); + + $this->assertSame([], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItDoesNotRegisterWorkerStoppingListenerWhenNotRunningQueueWork() + { + $argv = $_SERVER['argv']; + $_SERVER['argv'] = ['artisan', 'tinker']; + + try { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + + $this->app['events']->dispatch(new WorkerStopping(0, null, WorkerStopReason::TimedOut)); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + ], $eventsFake->emitted); + } finally { + $_SERVER['argv'] = $argv; + } + } + + public function testItRespectsDispatchAfterTransaction() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + $this->app['config']->set('queue.connections.sqs.after_commit', true); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); + + DB::beginTransaction(); + + $queue->push(new FakeJob, queue: '1'); + $queue->pushOn('2', new FakeJob); + $queue->pushRaw('', queue: '3'); + $queue->later(1, new FakeJob, queue: '4'); + $queue->laterOn('5', 1, new FakeJob); + $queue->bulk([new FakeJob, new FakeJob], queue: '6'); + + $this->travel(10)->minutes(); + DB::commit(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:04:05.060708', + 'type' => 'queued', + 'queue' => '3', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '1', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '2', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '4', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '5', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-02 03:14:05.060708', + 'type' => 'queued', + 'queue' => '6', + ], + ], $eventsFake->emitted); + } + + public function testItCapturesDurationForMultipleJobs() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop = [new FakeJob, new FakeJob]; + $queue->pop(); + $this->travel(1)->second(); + $queue->pop(); + $this->travel(0.5)->second(); + $queue->pop(); + + $this->assertSame(1000, $eventsFake->emitted[1]['duration_ms']); + $this->assertSame(500, $eventsFake->emitted[3]['duration_ms']); + } + + public function testItCapturesUtcTime() + { + date_default_timezone_set('Australia/Melbourne'); + $this->travelTo(Carbon::parse('2000-01-02 03:04:05.060708', 'Australia/Melbourne')); + $eventsFake = $this->fakeEvents(); + [$queue, $queueFake] = $this->fakeQueue(); + + $queueFake->jobsToPop[] = new FakeJob; + $queue->pop(); + $this->travel(1)->second(); + $queue->pop(); + + $this->assertSame([ + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-01 16:04:05.060708', + 'type' => 'started', + 'queue' => 'default', + ], + [ + '_cloud_event' => 'queue', + 'timestamp' => '2000-01-01 16:04:06.060708', + 'type' => 'processed', + 'queue' => 'default', + 'duration_ms' => 1000, + ], + ], $eventsFake->emitted); + } + + public function testFindProxiesToFailerForNonUrls() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $job = $provider->find('not-a-url'); + + $this->assertNull($job); + } + + public function testFindGetsUrlAndDecryptsResponse() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $payload = ['id' => 'test-job-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{"job":"App\\\\Jobs\\\\TestJob"}']; + $encrypted = Crypt::encryptString(json_encode($payload)); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response($encrypted), + ]); + + $result = $provider->find('https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + + $this->assertIsObject($result); + $this->assertSame('test-job-id', $result->id); + $this->assertSame('sqs', $result->connection); + $this->assertSame('default', $result->queue); + $this->assertSame('{"job":"App\\\\Jobs\\\\TestJob"}', $result->payload); + Http::assertSent(fn ($request) => $request->url() === 'https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + } + + public function testFindReturnsNullWhenDecryptionFails() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response('not-valid-encrypted-data'), + ]); + + try { + $provider->find('https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + $this->fail(); + } catch (Throwable $e) { + $this->assertInstanceOf(DecryptException::class, $e); + } + } + + public function testFindReturnsNullWhenHttpRequestFails() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response('Server Error', 500), + ]); + + try { + $provider->find('https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); + $this->fail(); + } catch (Throwable $e) { + $this->assertInstanceOf(RequestException::class, $e); + } + } + + public function testForgetProxiesToFailerForNonUrls() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + // First log a job to the failer with a UUID + $uuid = (string) Str::uuid(); + $failer->log('database', 'default', json_encode(['uuid' => $uuid]), new \Exception('test')); + $jobId = $failer->ids()[0]; + + // Forget should delegate to the underlying failer + $result = $provider->forget($jobId); + + $this->assertTrue($result); + $this->assertEmpty($failer->ids()); + } + + public function testForgetEmitsEventAfterFind() + { + $this->travelTo('2000-01-02 03:04:05.060708'); + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $payload = ['id' => 'forget-test-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{}']; + $encrypted = Crypt::encryptString(json_encode($payload)); + + Http::fake([ + 'https://cloud.laravel.com/*' => Http::response($encrypted), + ]); + + $url = 'https://cloud.laravel.com/api/jobs/forget-test-id?signature=abc'; + $provider->find($url); + $result = $provider->forget($url); + + $this->assertTrue($result); + $this->assertSame([ + [ + '_cloud_event' => 'failed_job', + 'id' => 'forget-test-id', + 'queue' => 'default', + 'retried_at' => '2000-01-02 03:04:05.060708', + ], + ], $eventsFake->emitted); + } + + public function testForgetReturnsFalseWithoutPriorFind() + { + $eventsFake = $this->fakeEvents(); + $failer = $this->fakeFailer(); + $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); + + $result = $provider->forget('https://cloud.laravel.com/api/jobs/some-id?signature=abc'); + + $this->assertFalse($result); + $this->assertEmpty($eventsFake->emitted); + } + + public function testItUsesConfigValuesToNormalizeQueueName() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); + + unset($_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX']); + + $queue->push(new FakeJob, queue: 'https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'); + + $this->assertSame('my-queue', $eventsFake->emitted[0]['queue']); + } + + public function testItHandlesMissingPrefixAndSuffixConfig() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + $this->app['config']->set('queue.connections.sqs', Arr::except($this->app['config']->get('queue.connections.sqs'), ['prefix', 'suffix'])); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); + + unset($_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX']); + + $queue->push(new FakeJob, queue: 'https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'); + + $this->assertSame('https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', $eventsFake->emitted[0]['queue']); + } + + /** + * @return array{Queue, MockInterface} + */ + private function mockedQueue() + { + $client = $this->mock(SqsClient::class); + + $this->app->instance(QueueConnector::class, new QueueConnector(new class($client) implements ConnectorInterface + { + public function __construct(private $client) + { + // + } + + public function connect($config) + { + return new SqsQueue( + $this->client, + $config['queue'], + $config['prefix'] ?? '', + $config['suffix'] ?? '', + $config['after_commit'] ?? null, + $config['overflow'] ?? [], + ); + } + }, $this->app)); + + $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + + return [$this->app['queue']->connection('sqs'), $client]; + } + + private function fakeEvents() + { + return $this->app->instance(Events::class, new class('test-socket') extends Events + { + public array $emitted = []; + + public function emitMany(array $payloads): void + { + $this->emitted = [ + ...$this->emitted, + ...$payloads, + ]; + } + }); + } + + /** + * @return array{Queue, object{jobsToPop: array}} + */ + private function fakeQueue() + { + $fakeQueue = new class($this->app, [], null) extends QueueFake + { + public array $jobsToPop = []; + + public function pop($queue = null) + { + return array_shift($this->jobsToPop); + } + + public function getQueue($queue) + { + $queue ??= 'default'; + + return config('queue.connections.sqs.prefix').'/'.$queue.config('queue.connections.sqs.suffix'); + } + + public function setConfig(array $config) + { + return $this; + } + + public function setContainer($container) + { + return $this; + } + }; + + $this->app->instance(QueueConnector::class, new QueueConnector(new class($fakeQueue) implements ConnectorInterface + { + public function __construct(private $fakeQueue) + { + // + } + + public function connect($config) + { + return $this->fakeQueue; + } + }, $this->app)); + + $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + + return [$this->app['queue']->connection('sqs'), $fakeQueue]; + } + + private function fakeFailer() + { + return new FileFailedJobProvider(tempnam(sys_get_temp_dir(), 'cloud_failed_job_test_')); + } +} + +class MyJob +{ + public function fire() + { + // + } +} diff --git a/tests/Foundation/FoundationAliasLoaderTest.php b/tests/Foundation/FoundationAliasLoaderTest.php index 7889727027a6..08c64039dc8a 100755 --- a/tests/Foundation/FoundationAliasLoaderTest.php +++ b/tests/Foundation/FoundationAliasLoaderTest.php @@ -7,6 +7,14 @@ class FoundationAliasLoaderTest extends TestCase { + public function setUp(): void + { + parent::setUp(); + + AliasLoader::setInstance(null); + AliasLoader::setFacadeNamespace('Facades\\'); + } + public function testLoaderCanBeCreatedAndRegisteredOnce() { $loader = AliasLoader::getInstance(['foo' => 'bar']); diff --git a/tests/Integration/Foundation/CloudTest.php b/tests/Integration/Foundation/CloudTest.php index 72feb5a55c29..a20f64a5aa81 100644 --- a/tests/Integration/Foundation/CloudTest.php +++ b/tests/Integration/Foundation/CloudTest.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Integration\Foundation; use Illuminate\Foundation\Cloud; -use Illuminate\Queue\Worker; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; @@ -54,73 +53,6 @@ public function test_it_can_configure_disks() unset($_SERVER['LARAVEL_CLOUD_DISK_CONFIG']); } - public function test_it_disables_queue_restart_polling_for_managed_queues() - { - Worker::$restartable = true; - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertFalse(Worker::$restartable); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); - Worker::$restartable = true; - } - } - - public function test_it_disables_queue_pause_polling_for_managed_queues() - { - Worker::$pausable = true; - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertFalse(Worker::$pausable); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); - Worker::$pausable = true; - } - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function test_it_configures_managed_queue_credentials() - { - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); - } - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function test_it_does_not_configure_managed_queues_when_not_enabled() - { - Cloud::configureManagedQueues($this->app); - - $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function test_it_configures_managed_queue_region() - { - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); - } - } - public function test_it_respects_log_levels() { if (isset($_SERVER['LOG_LEVEL'])) { diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index 986f850ff426..9d500927847f 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -14,10 +14,12 @@ use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Queue\Events\WorkerStarting; +use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Queue\MaxAttemptsExceededException; use Illuminate\Queue\QueueManager; use Illuminate\Queue\Worker; use Illuminate\Queue\WorkerOptions; +use Illuminate\Queue\WorkerStopReason; use Illuminate\Support\Carbon; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -419,6 +421,52 @@ public function testWorkerStartingIsDispatched() $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerStarting::class))->once(); } + public function testWorkerStoppingIsDispatched() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmpty = true; + + $worker = $this->getWorker('default', ['queue' => [ + $firstJob = new WorkerFakeJob(), + $secondJob = new WorkerFakeJob(), + ]]); + + $worker->daemon('default', 'queue', $workerOptions); + + $this->assertTrue($firstJob->fired); + $this->assertTrue($secondJob->fired); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::QueueEmpty; + }))->once(); + } + + public function testWorkerStopsWithLostConnectionReason() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmpty = true; + + $worker = $this->getWorker('default', ['queue' => [ + $job = new WorkerFakeJob(function () { + throw new RuntimeException('server has gone away'); + }), + ]]); + + $worker->daemon('default', 'queue', $workerOptions); + + $this->assertTrue($job->fired); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::LostConnection; + })); + } + public function testJobReleasedEvent() { $e = new RuntimeException; @@ -485,9 +533,9 @@ public function sleep($seconds) $this->sleptFor = $seconds; } - public function stop($status = 0, $options = null) + public function stop($status = 0, $options = null, $reason = null) { - return $status; + return parent::stop($status, $options, $reason); } public function daemonShouldRun(WorkerOptions $options, $connectionName, $queue) From 70a838b1a6f12abaf5bebb76ea4f28fbd37451fa Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 14 May 2026 15:31:40 +0000 Subject: [PATCH 361/596] Update version to v12.59.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 5b420dbffcff..3fdb843cca62 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.58.0'; + const VERSION = '12.59.0'; /** * The base path for the Laravel installation. From b245246c8c297b95bb78b174bec97c880f73f6a8 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 14 May 2026 15:33:27 +0000 Subject: [PATCH 362/596] Update CHANGELOG --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ba083a38f4..e716e6c215a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.58.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.59.0...12.x) + +## [v12.59.0](https://github.com/laravel/framework/compare/v12.58.0...v12.59.0) - 2026-05-14 + +* [12.x] Disable pausing on managed queue workers by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/59871 +* [12.x] Fix infinite recursion when defining model scope with attribute as private by [@noefleury](https://github.com/noefleury) in https://github.com/laravel/framework/pull/59958 +* [12.x] Fix infinite recursion when middleware group referencing itself by [@noefleury](https://github.com/noefleury) in https://github.com/laravel/framework/pull/60002 +* [12.x] Backport #60000 to 12.x by [@iWader](https://github.com/iWader) in https://github.com/laravel/framework/pull/60006 +* [12.x] Narrow attachment url scheme by [@benbjurstrom](https://github.com/benbjurstrom) in https://github.com/laravel/framework/pull/60035 +* [12.x] backport #60045 to 12.x by [@levikl](https://github.com/levikl) in https://github.com/laravel/framework/pull/60052 +* [12.x] Back port cloud queues by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60122 ## [v12.58.0](https://github.com/laravel/framework/compare/v12.57.0...v12.58.0) - 2026-04-26 From 32ec0a08da6dc0cc6e35236308dc728d1d93b290 Mon Sep 17 00:00:00 2001 From: Sebastian Cabarcas Berrio <42840369+scabarcas17@users.noreply.github.com> Date: Thu, 14 May 2026 11:52:21 -0500 Subject: [PATCH 363/596] [13.x] Add tests for SeeInHtml constraint covering unicode whitespace (#60128) * Add tests for SeeInHtml constraint covering unicode whitespace * style: fix StyleCI quote style in SeeInHtmlTest --- tests/Testing/SeeInHtmlTest.php | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/Testing/SeeInHtmlTest.php diff --git a/tests/Testing/SeeInHtmlTest.php b/tests/Testing/SeeInHtmlTest.php new file mode 100644 index 000000000000..b2dd944c0477 --- /dev/null +++ b/tests/Testing/SeeInHtmlTest.php @@ -0,0 +1,71 @@ +assertTrue($constraint->matches(['

Hello World

'])); + $this->assertTrue($constraint->matches(['

Hello World

'])); + $this->assertTrue($constraint->matches(['

Hello World

'])); + } + + #[DataProvider('unicodeWhitespaceCharacters')] + public function testCollapsesRawUnicodeWhitespace(string $whitespace) + { + $constraint = new SeeInHtml('Hello World'); + + $this->assertTrue($constraint->matches(["

Hello{$whitespace}World

"])); + } + + public static function unicodeWhitespaceCharacters(): array + { + return [ + 'no-break space (U+00A0)' => ["\u{00A0}"], + 'en space (U+2002)' => ["\u{2002}"], + 'em space (U+2003)' => ["\u{2003}"], + 'thin space (U+2009)' => ["\u{2009}"], + 'ideographic space (U+3000)' => ["\u{3000}"], + ]; + } + + public function testCollapsesMultipleAsciiWhitespace() + { + $constraint = new SeeInHtml('Hello World'); + + $this->assertTrue($constraint->matches(['

Hello World

'])); + $this->assertTrue($constraint->matches(["

Hello\tWorld

"])); + $this->assertTrue($constraint->matches(["

Hello\nWorld

"])); + $this->assertTrue($constraint->matches(["

Hello \t\n World

"])); + } + + public function testFailsWhenValueIsAbsent() + { + $constraint = new SeeInHtml('Hello World'); + + $this->assertFalse($constraint->matches(['

Goodbye World

'])); + } + + public function testNegateInvertsTheAssertion() + { + $constraint = new SeeInHtml('Hello World', ordered: false, negate: true); + + $this->assertTrue($constraint->matches(['

Goodbye World

'])); + $this->assertFalse($constraint->matches(['

Hello World

'])); + } + + public function testOrderedRespectsSequenceAcrossUnicodeWhitespace() + { + $constraint = new SeeInHtml('Hello beautiful World', ordered: true); + + $this->assertTrue($constraint->matches(['Hello', 'beautiful', 'World'])); + $this->assertFalse($constraint->matches(['World', 'Hello'])); + } +} From b0c2aaca83737baed213d0957a57ff9f25d9ebd5 Mon Sep 17 00:00:00 2001 From: Fatih AYDIN Date: Thu, 14 May 2026 20:25:15 +0300 Subject: [PATCH 364/596] [13.x] Fix starts_with/ends_with rules rejecting numeric values (#60120) * fix(validation): allow numeric values in starts_with/ends_with rules `is_string` guard introduced in #59541 incorrectly rejects numeric values, breaking combinations like `['numeric', 'doesnt_start_with:0']` when the value is an integer from a JSON request. Cast to string after checking `is_string || is_numeric`, consistent with how `validateMinDigits` and `validateMaxDigits` were fixed in the same PR. * test(validation): add numeric value cases for starts_with/ends_with rules --- .../Concerns/ValidatesAttributes.php | 24 ++++++++++-- tests/Validation/ValidationValidatorTest.php | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 28e4d89431f7..edeed2af813b 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -2646,7 +2646,11 @@ public function validateSometimes() */ public function validateStartsWith($attribute, $value, $parameters) { - return is_string($value) && Str::startsWith($value, $parameters); + if (is_string($value) || is_numeric($value)) { + return Str::startsWith((string) $value, $parameters); + } + + return false; } /** @@ -2659,7 +2663,11 @@ public function validateStartsWith($attribute, $value, $parameters) */ public function validateDoesntStartWith($attribute, $value, $parameters) { - return is_string($value) && ! Str::startsWith($value, $parameters); + if (is_string($value) || is_numeric($value)) { + return ! Str::startsWith((string) $value, $parameters); + } + + return false; } /** @@ -2672,7 +2680,11 @@ public function validateDoesntStartWith($attribute, $value, $parameters) */ public function validateEndsWith($attribute, $value, $parameters) { - return is_string($value) && Str::endsWith($value, $parameters); + if (is_string($value) || is_numeric($value)) { + return Str::endsWith((string) $value, $parameters); + } + + return false; } /** @@ -2685,7 +2697,11 @@ public function validateEndsWith($attribute, $value, $parameters) */ public function validateDoesntEndWith($attribute, $value, $parameters) { - return is_string($value) && ! Str::endsWith($value, $parameters); + if (is_string($value) || is_numeric($value)) { + return ! Str::endsWith((string) $value, $parameters); + } + + return false; } /** diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index c462635f36f5..b7a0ced33f96 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -3235,6 +3235,12 @@ public function testValidateStartsWithDoesNotThrowOnNonStringValue() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => ['array', 'value']], ['x' => 'starts_with:arr']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'starts_with:1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'starts_with:2']); + $this->assertFalse($v->passes()); } public function testValidateEndsWithDoesNotThrowOnNonStringValue() @@ -3242,6 +3248,38 @@ public function testValidateEndsWithDoesNotThrowOnNonStringValue() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => ['array', 'value']], ['x' => 'ends_with:ue']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'ends_with:3']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'ends_with:2']); + $this->assertFalse($v->passes()); + } + + public function testValidateDoesntStartWithDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array', 'value']], ['x' => 'doesnt_start_with:arr']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'doesnt_start_with:0']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'doesnt_start_with:1']); + $this->assertFalse($v->passes()); + } + + public function testValidateDoesntEndWithDoesNotThrowOnNonStringValue() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['x' => ['array', 'value']], ['x' => 'doesnt_end_with:ue']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'doesnt_end_with:0']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 123], ['x' => 'doesnt_end_with:3']); + $this->assertFalse($v->passes()); } public function testValidateLowercaseDoesNotThrowOnNonStringValue() From 25ea91edc77375fdf74ca71f8c72024bc8f923ef Mon Sep 17 00:00:00 2001 From: Daniel Cadeau Date: Thu, 14 May 2026 19:25:41 +0200 Subject: [PATCH 365/596] Fix typo in docblock for listManagementOptions method in SesV2Transport (#60115) --- src/Illuminate/Mail/Transport/SesV2Transport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Mail/Transport/SesV2Transport.php b/src/Illuminate/Mail/Transport/SesV2Transport.php index 5ce6e900ae85..3052a0826b46 100644 --- a/src/Illuminate/Mail/Transport/SesV2Transport.php +++ b/src/Illuminate/Mail/Transport/SesV2Transport.php @@ -100,7 +100,7 @@ protected function doSend(SentMessage $message): void } /** - * Extract the SES list managenent options, if applicable. + * Extract the SES list management options, if applicable. * * @param \Symfony\Component\Mailer\SentMessage $message * @return array|null From 1906399903ddeb68b0cd6381f3050fc2e170c578 Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 14 May 2026 23:30:07 +0600 Subject: [PATCH 366/596] Fix typo in mergeAttributeFromCachedCasts() PHPDoc comment (#60112) --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 6836ceb8b611..bb03c3b98907 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1931,7 +1931,7 @@ protected function mergeAttributesFromCachedCasts() } /** - * Merge the a cast class and attribute cast attribute back into the model. + * Merge the cast class and attribute cast attribute back into the model. * * @return void */ From 003d650ac90c9c745420bd7c4a1248164b7b79e9 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 14 May 2026 18:30:36 +0100 Subject: [PATCH 367/596] 13.x-optimize-queue-pause (#60109) and this docblock Update docblock array_flip for perf dont type fam fix that test worker fix the tests jack omg how dare u not run the full suite --- src/Illuminate/Queue/QueueManager.php | 18 ++++++++++++++++ src/Illuminate/Queue/Worker.php | 24 ++++++++++++++------- tests/Integration/Queue/WorkCommandTest.php | 4 ++-- tests/Queue/QueuePauseResumeTest.php | 13 +++++++++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Queue/QueueManager.php b/src/Illuminate/Queue/QueueManager.php index 0cda8631b239..c7af54eaa3d6 100755 --- a/src/Illuminate/Queue/QueueManager.php +++ b/src/Illuminate/Queue/QueueManager.php @@ -284,6 +284,24 @@ public function isPaused($connection, $queue) ->get("illuminate:queue:paused:{$connection}:{$queue}", false); } + /** + * Determine which of the given queues are currently paused. + * + * @param string $connection + * @param array $queues + * @return array + */ + public function getPausedQueues($connection, $queues) + { + $keys = array_map(fn ($queue) => "illuminate:queue:paused:{$connection}:{$queue}", $queues); + + $states = $this->app['cache']->store()->many($keys); + + return array_values(array_filter( + $queues, fn ($queue) => $states["illuminate:queue:paused:{$connection}:{$queue}"] ?? false + )); + } + /** * Indicate that queue workers should not poll for restart or pause signals. * diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 74d3127bb35b..d3199836f5a5 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -418,8 +418,12 @@ protected function getNextJob($connection, $queue) return $job; } - foreach (explode(',', $queue) as $index => $queue) { - if ($this->queuePaused($connection->getConnectionName(), $queue)) { + $queues = explode(',', $queue); + + $paused = array_flip($this->getPausedQueues($connection->getConnectionName(), $queues)); + + foreach ($queues as $index => $queue) { + if (isset($paused[$queue])) { continue; } @@ -439,19 +443,23 @@ protected function getNextJob($connection, $queue) } /** - * Determine if a given connection and queue is paused. + * Determine which of the given queues are currently paused. * * @param string $connectionName - * @param string $queue - * @return bool + * @param array $queues + * @return array */ - protected function queuePaused($connectionName, $queue) + protected function getPausedQueues($connectionName, $queues) { if (! static::$pausable) { - return false; + return []; + } + + if ($this->cache === null) { + return []; } - return $this->cache && $this->manager->isPaused($connectionName, $queue); + return $this->manager->getPausedQueues($connectionName, $queues); } /** diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 940100720d8f..628c7c2d4cbd 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -195,7 +195,7 @@ public function testDisableLastRestartCheck() $cache = m::mock(Repository::class); $cache->shouldNotReceive('get')->with('illuminate:queue:restart'); - $cache->shouldReceive('get')->with(m::pattern('/^illuminate:queue:paused:/'), false); + $cache->shouldReceive('many')->andReturn([]); $cacheManager = m::mock(CacheManager::class); $cacheManager->shouldReceive('driver')->andReturn($cache); @@ -225,7 +225,7 @@ public function testDisablePauseQueueCheck() $cache = m::mock(Repository::class); $cache->shouldReceive('get')->with('illuminate:queue:restart')->andReturn(null); - $cache->shouldNotReceive('get')->with(m::pattern('/^illuminate:queue:paused:/'), false); + $cache->shouldNotReceive('many'); $cacheManager = m::mock(CacheManager::class); $cacheManager->shouldReceive('driver')->andReturn($cache); diff --git a/tests/Queue/QueuePauseResumeTest.php b/tests/Queue/QueuePauseResumeTest.php index 34042b98d9d8..34b13b058271 100644 --- a/tests/Queue/QueuePauseResumeTest.php +++ b/tests/Queue/QueuePauseResumeTest.php @@ -161,6 +161,19 @@ public function testResumeDispatchesQueueResumedEvent() $this->assertSame('notifications', $dispatchedEvent->queue); } + public function testGetPausedQueues() + { + $this->assertSame([], $this->manager->getPausedQueues('redis', ['default', 'emails'])); + + $this->manager->pause('redis', 'emails'); + $this->manager->pause('redis', 'notifications'); + + $this->assertSame( + ['emails', 'notifications'], + $this->manager->getPausedQueues('redis', ['default', 'emails', 'notifications']) + ); + } + public function testParsingQueueString() { $parser = new class() From 44c67226f042cfbb623a8a74acb1142b4cf722fc Mon Sep 17 00:00:00 2001 From: "Md. Al-Mosabbir Rakib" <34891461+mosabbirrakib@users.noreply.github.com> Date: Thu, 14 May 2026 23:30:54 +0600 Subject: [PATCH 368/596] Fix typo in preg_replace_array() PHPDoc comment (#60111) --- src/Illuminate/Support/helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/helpers.php b/src/Illuminate/Support/helpers.php index 22f48b02e856..1225db9d4ee2 100644 --- a/src/Illuminate/Support/helpers.php +++ b/src/Illuminate/Support/helpers.php @@ -280,7 +280,7 @@ function optional($value = null, ?callable $callback = null) if (! function_exists('preg_replace_array')) { /** - * Replace a given pattern with each value in the array in sequentially. + * Replace a given pattern with each value in the array sequentially. * * @param string $pattern * @param array $replacements From b325a0c4df0f424763ceee1a50e26b8cee67d278 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 14 May 2026 17:31:40 +0000 Subject: [PATCH 369/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 18bab2aa9468..a38620a1d95e 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -20,6 +20,7 @@ * @method static void pauseFor(string $connection, string $queue, \DateTimeInterface|\DateInterval|int $ttl) * @method static void resume(string $connection, string $queue) * @method static bool isPaused(string $connection, string $queue) + * @method static array getPausedQueues(string $connection, array $queues) * @method static void withoutInterruptionPolling() * @method static void extend(string $driver, \Closure $resolver) * @method static void addConnector(string $driver, \Closure $resolver) From 5c50074af3ebd2d9c6330498f77c3f333ea920c0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 14 May 2026 13:31:12 -0500 Subject: [PATCH 370/596] add storage store (#60131) --- config/cache.php | 10 +- src/Illuminate/Cache/CacheManager.php | 16 ++ src/Illuminate/Cache/StorageStore.php | 322 +++++++++++++++++++++++ tests/Cache/CacheManagerTest.php | 32 +++ tests/Cache/CacheStorageStoreTest.php | 113 ++++++++ tests/Cache/Fixtures/ArrayFilesystem.php | 149 +++++++++++ 6 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Cache/StorageStore.php create mode 100644 tests/Cache/CacheStorageStoreTest.php create mode 100644 tests/Cache/Fixtures/ArrayFilesystem.php diff --git a/config/cache.php b/config/cache.php index 807344931eb3..923671e14e53 100644 --- a/config/cache.php +++ b/config/cache.php @@ -27,8 +27,8 @@ | same cache driver to group types of items stored in your caches. | | Supported drivers: "array", "database", "file", "memcached", - | "redis", "dynamodb", "octane", "session", - | "failover", "null" + | "redis", "dynamodb", "storage", "octane", + | "session", "failover", "null" | */ @@ -58,6 +58,12 @@ 'lock_path' => storage_path('framework/cache/data'), ], + 'storage' => [ + 'driver' => 'storage', + 'disk' => env('CACHE_STORAGE_DISK'), + 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'), + ], + 'memcached' => [ 'driver' => 'memcached', 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 0ee3c7e479ab..fc3b50ebd00f 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -302,6 +302,22 @@ protected function createFileDriver(array $config) ); } + /** + * Create an instance of the storage cache driver. + * + * @param array $config + * @return \Illuminate\Cache\Repository + */ + protected function createStorageDriver(array $config) + { + return $this->repository(new StorageStore( + $this->app['filesystem']->disk($config['disk'] ?? null), + $config['path'] ?? '', + $this->getPrefix($config), + $this->getSerializableClasses($config), + ), $config); + } + /** * Create an instance of the Memcached cache driver. * diff --git a/src/Illuminate/Cache/StorageStore.php b/src/Illuminate/Cache/StorageStore.php new file mode 100644 index 000000000000..2e46c88a36fe --- /dev/null +++ b/src/Illuminate/Cache/StorageStore.php @@ -0,0 +1,322 @@ +disk = $disk; + $this->directory = trim($directory, '/'); + $this->prefix = $prefix; + $this->serializableClasses = $serializableClasses; + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + return $this->getPayload($key)['data'] ?? null; + } + + /** + * Store an item in the cache for a given number of seconds. + * + * @param string $key + * @param mixed $value + * @param int $seconds + * @return bool + */ + public function put($key, $value, $seconds) + { + return $this->disk->put( + $this->path($key), $this->expiration($seconds).serialize($value) + ); + } + + /** + * Store an item in the cache if the key doesn't exist. + * + * @param string $key + * @param mixed $value + * @param int $seconds + * @return bool + */ + public function add($key, $value, $seconds) + { + if (! is_null($this->get($key))) { + return false; + } + + return $this->put($key, $value, $seconds); + } + + /** + * Increment the value of an item in the cache. + * + * @param string $key + * @param mixed $value + * @return int + */ + public function increment($key, $value = 1) + { + $raw = $this->getPayload($key); + + return tap(((int) $raw['data']) + $value, function ($newValue) use ($key, $raw) { + $this->put($key, $newValue, $raw['time'] ?? 0); + }); + } + + /** + * Decrement the value of an item in the cache. + * + * @param string $key + * @param mixed $value + * @return int + */ + public function decrement($key, $value = 1) + { + return $this->increment($key, $value * -1); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return bool + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Adjust the expiration time of a cached item. + * + * @param string $key + * @param int $seconds + * @return bool + */ + public function touch($key, $seconds) + { + $payload = $this->getPayload($key); + + if (is_null($payload['data'])) { + return false; + } + + return $this->put($key, $payload['data'], $seconds); + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + $forgotten = $this->disk->delete($this->path($key)); + + if ($forgotten) { + $this->disk->delete($this->path("illuminate:cache:flexible:created:{$key}")); + } + + return $forgotten; + } + + /** + * Remove all items from the cache. + * + * @return bool + */ + public function flush() + { + if ($this->directory === '') { + $files = $this->disk->allFiles(); + + return $files === [] || $this->disk->delete($files); + } + + return $this->disk->deleteDirectory($this->directory) + && $this->disk->makeDirectory($this->directory); + } + + /** + * Retrieve an item and expiry time from the cache by key. + * + * @param string $key + * @return array + */ + protected function getPayload($key) + { + $path = $this->path($key); + + try { + if (is_null($contents = $this->disk->get($path))) { + return $this->emptyPayload(); + } + + $expire = substr($contents, 0, 10); + } catch (Exception) { + return $this->emptyPayload(); + } + if ($this->currentTime() >= $expire) { + $this->forget($key); + + return $this->emptyPayload(); + } + + try { + $data = $this->unserialize(substr($contents, 10)); + } catch (Exception) { + $this->forget($key); + + return $this->emptyPayload(); + } + + $time = $expire - $this->currentTime(); + + return compact('data', 'time'); + } + + /** + * Unserialize the given value. + * + * @param string $value + * @return mixed + */ + protected function unserialize($value) + { + if ($this->serializableClasses !== null) { + return unserialize($value, ['allowed_classes' => $this->serializableClasses]); + } + + return unserialize($value); + } + + /** + * Get a default empty payload for the cache. + * + * @return array + */ + protected function emptyPayload() + { + return ['data' => null, 'time' => null]; + } + + /** + * Get the full path for the given cache key. + * + * @param string $key + * @return string + */ + public function path($key) + { + $parts = array_slice(str_split($hash = sha1($this->prefix.$key), 2), 0, 2); + + return trim($this->directory.'/'.implode('/', $parts).'/'.$hash, '/'); + } + + /** + * Get the expiration time based on the given seconds. + * + * @param int $seconds + * @return int + */ + protected function expiration($seconds) + { + $time = $this->availableAt($seconds); + + return $seconds === 0 || $time > 9999999999 ? 9999999999 : $time; + } + + /** + * Get the filesystem disk instance. + * + * @return \Illuminate\Contracts\Filesystem\Filesystem + */ + public function getDisk() + { + return $this->disk; + } + + /** + * Get the working directory of the cache. + * + * @return string + */ + public function getDirectory() + { + return $this->directory; + } + + /** + * Get the cache key prefix. + * + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Set the cache key prefix. + * + * @param string $prefix + * @return void + */ + public function setPrefix($prefix) + { + $this->prefix = $prefix; + } +} diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index 0a1319b0daf2..bbdb1a1a0990 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -5,10 +5,12 @@ use Illuminate\Cache\ArrayStore; use Illuminate\Cache\CacheManager; use Illuminate\Cache\NullStore; +use Illuminate\Cache\StorageStore; use Illuminate\Config\Repository; use Illuminate\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Events\Dispatcher as Event; +use Illuminate\Tests\Cache\Fixtures\ArrayFilesystem; use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -103,6 +105,36 @@ public function testItCanBuildRepositories() $this->assertInstanceOf(NullStore::class, $nullCache->getStore()); } + public function testItCanCreateStorageDriver() + { + $disk = new ArrayFilesystem; + + $filesystem = m::mock(); + $filesystem->shouldReceive('disk')->with('s3')->once()->andReturn($disk); + + $app = $this->getApp([ + 'cache' => [ + 'prefix' => 'cache:', + 'stores' => [ + 'storage' => [ + 'driver' => 'storage', + 'disk' => 's3', + 'path' => 'cache', + ], + ], + ], + ]); + $app->instance('filesystem', $filesystem); + + $cacheManager = new CacheManager($app); + $store = $cacheManager->store('storage')->getStore(); + + $this->assertInstanceOf(StorageStore::class, $store); + $this->assertSame($disk, $store->getDisk()); + $this->assertSame('cache', $store->getDirectory()); + $this->assertSame('cache:', $store->getPrefix()); + } + public function testItMakesRepositoryWhenContainerHasNoDispatcher() { $userConfig = [ diff --git a/tests/Cache/CacheStorageStoreTest.php b/tests/Cache/CacheStorageStoreTest.php new file mode 100644 index 000000000000..045cb2088141 --- /dev/null +++ b/tests/Cache/CacheStorageStoreTest.php @@ -0,0 +1,113 @@ +assertTrue($store->put('foo', 'bar', 60)); + $this->assertSame('bar', $store->get('foo')); + $this->assertStringStartsWith('cache/', $store->path('foo')); + } + + public function testExpiredItemsReturnNullAndGetDeleted() + { + Carbon::setTestNow(Carbon::now()); + + $disk = new ArrayFilesystem; + $store = new StorageStore($disk, 'cache'); + + $store->put('foo', 'bar', 1); + + Carbon::setTestNow(Carbon::now()->addSeconds(2)); + + $this->assertNull($store->get('foo')); + $this->assertFalse($disk->exists($store->path('foo'))); + } + + public function testAddDoesNotOverwriteExistingValues() + { + $store = new StorageStore(new ArrayFilesystem, 'cache'); + + $this->assertTrue($store->add('foo', 'bar', 60)); + $this->assertFalse($store->add('foo', 'baz', 60)); + $this->assertSame('bar', $store->get('foo')); + } + + public function testIncrementAndDecrementRetainExpiration() + { + Carbon::setTestNow(Carbon::now()); + + $store = new StorageStore(new ArrayFilesystem, 'cache'); + $store->put('foo', 5, 60); + + $this->assertSame(7, $store->increment('foo', 2)); + $this->assertSame(4, $store->decrement('foo', 3)); + + Carbon::setTestNow(Carbon::now()->addSeconds(61)); + + $this->assertNull($store->get('foo')); + } + + public function testTouchUpdatesExpiration() + { + Carbon::setTestNow(Carbon::now()); + + $store = new StorageStore(new ArrayFilesystem, 'cache'); + $store->put('foo', 'bar', 2); + + Carbon::setTestNow(Carbon::now()->addSecond()); + + $this->assertTrue($store->touch('foo', 60)); + + Carbon::setTestNow(Carbon::now()->addSecond()); + + $this->assertSame('bar', $store->get('foo')); + } + + public function testForgetRemovesFlexibleCreatedKeyOnlyWhenParentIsForgotten() + { + $disk = new ArrayFilesystem; + $store = new StorageStore($disk, 'cache'); + + $store->put('illuminate:cache:flexible:created:foo', true, 60); + + $this->assertFalse($store->forget('foo')); + $this->assertTrue($disk->exists($store->path('illuminate:cache:flexible:created:foo'))); + + $store->put('foo', 'bar', 60); + + $this->assertTrue($store->forget('foo')); + $this->assertFalse($disk->exists($store->path('foo'))); + $this->assertFalse($disk->exists($store->path('illuminate:cache:flexible:created:foo'))); + } + + public function testFlushRemovesScopedDirectory() + { + $disk = new ArrayFilesystem; + $store = new StorageStore($disk, 'cache'); + + $store->put('foo', 'bar', 60); + $disk->put('other/file', 'value'); + + $this->assertTrue($store->flush()); + $this->assertNull($store->get('foo')); + $this->assertTrue($disk->exists('other/file')); + } +} diff --git a/tests/Cache/Fixtures/ArrayFilesystem.php b/tests/Cache/Fixtures/ArrayFilesystem.php new file mode 100644 index 000000000000..9faff21c71ec --- /dev/null +++ b/tests/Cache/Fixtures/ArrayFilesystem.php @@ -0,0 +1,149 @@ +files) || $this->files($path) !== []; + } + + public function get($path) + { + return $this->files[$path] ?? null; + } + + public function readStream($path) + { + return null; + } + + public function put($path, $contents, $options = []) + { + $this->files[$path] = $contents; + + return true; + } + + public function putFile($path, $file = null, $options = []) + { + return false; + } + + public function putFileAs($path, $file, $name = null, $options = []) + { + return false; + } + + public function writeStream($path, $resource, array $options = []) + { + return false; + } + + public function getVisibility($path) + { + return Filesystem::VISIBILITY_PRIVATE; + } + + public function setVisibility($path, $visibility) + { + return true; + } + + public function prepend($path, $data) + { + return false; + } + + public function append($path, $data) + { + return false; + } + + public function delete($paths) + { + $deleted = false; + + foreach ((array) $paths as $path) { + if (array_key_exists($path, $this->files)) { + unset($this->files[$path]); + + $deleted = true; + } + } + + return $deleted; + } + + public function copy($from, $to) + { + return false; + } + + public function move($from, $to) + { + return false; + } + + public function size($path) + { + return strlen($this->files[$path] ?? ''); + } + + public function lastModified($path) + { + return 0; + } + + public function files($directory = null, $recursive = false) + { + $directory = trim((string) $directory, '/'); + + return array_values(array_filter(array_keys($this->files), function ($path) use ($directory) { + return $directory === '' || str_starts_with($path, $directory.'/'); + })); + } + + public function allFiles($directory = null) + { + return $this->files($directory, true); + } + + public function directories($directory = null, $recursive = false) + { + return []; + } + + public function allDirectories($directory = null) + { + return []; + } + + public function makeDirectory($path) + { + return true; + } + + public function deleteDirectory($directory) + { + $deleted = false; + + foreach ($this->allFiles($directory) as $path) { + unset($this->files[$path]); + + $deleted = true; + } + + return $deleted; + } +} From 071ac5c36f8ce4a673420acdc622b67be5e05655 Mon Sep 17 00:00:00 2001 From: RP SOHAG <66528080+rpsohag@users.noreply.github.com> Date: Fri, 15 May 2026 00:36:16 +0600 Subject: [PATCH 371/596] Fix typo in Builder::getRelation() comment (#60130) * Fix typo in Builder::getRelation() comment * Update Builder.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 841426aa4b68..7d69849ac64d 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -965,7 +965,7 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) */ public function getRelation($name) { - // We want to run a relationship query without any constrains so that we will + // We want to do a relationship query without any constraints so that we will // not have to remove these where clauses manually which gets really hacky // and error prone. We don't want constraints because we add eager ones. $relation = Relation::noConstraints(function () use ($name) { From 7b2b2fe508d4a8013c5262d8d7dbb443d1dabd5e Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 15 May 2026 08:21:09 -0500 Subject: [PATCH 372/596] urlencode paths (#60137) --- .../Filesystem/LocalFilesystemAdapter.php | 4 +-- .../Filesystem/ReceiveFileTest.php | 27 ++++++++++++++++++- .../Integration/Filesystem/ServeFileTest.php | 24 ++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index 67ed1df9a0d6..5ddb2f9c0ab0 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -82,7 +82,7 @@ public function temporaryUrl($path, $expiration, array $options = []) return $url->to($url->temporarySignedRoute( 'storage.'.$this->disk, $expiration, - ['path' => $path], + ['path' => rawurlencode($path)], absolute: false )); } @@ -115,7 +115,7 @@ public function temporaryUploadUrl($path, $expiration, array $options = []) 'url' => $url->to($url->temporarySignedRoute( 'storage.'.$this->disk.'.upload', $expiration, - ['path' => $path, 'upload' => true], + ['path' => rawurlencode($path), 'upload' => true], absolute: false )), 'headers' => [], diff --git a/tests/Integration/Filesystem/ReceiveFileTest.php b/tests/Integration/Filesystem/ReceiveFileTest.php index 600b5506c465..a2aed581f996 100644 --- a/tests/Integration/Filesystem/ReceiveFileTest.php +++ b/tests/Integration/Filesystem/ReceiveFileTest.php @@ -13,7 +13,10 @@ class ReceiveFileTest extends TestCase protected function setUp(): void { $this->beforeApplicationDestroyed(function () { - Storage::delete('receive-file-test.txt'); + Storage::delete([ + 'receive-file-test.txt', + 'receive-file-test.txt?pad=x', + ]); }); parent::setUp(); @@ -73,4 +76,26 @@ public function testUploadUrlCannotBeUsedForDownload() $response->assertForbidden(); } + + public function testItCanReceiveAFileWithUriDelimitersInThePath() + { + $result = Storage::temporaryUploadUrl('receive-file-test.txt?pad=x', Carbon::now()->addMinute()); + + $response = $this->call('PUT', $result['url'], [], [], [], [], 'Hello Question'); + + $response->assertNoContent(); + Storage::assertExists('receive-file-test.txt?pad=x', 'Hello Question'); + Storage::assertMissing('receive-file-test.txt'); + } + + public function testUriDelimitersInThePathCannotHideAnExpiredUploadUrl() + { + $result = Storage::temporaryUploadUrl('receive-file-test.txt?pad=x', Carbon::now()->subMinute()); + + $response = $this->call('PUT', $result['url'], [], [], [], [], 'Hello Question'); + + $response->assertForbidden(); + Storage::assertMissing('receive-file-test.txt'); + Storage::assertMissing('receive-file-test.txt?pad=x'); + } } diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php index 616bffe42c12..7a87a1e15647 100644 --- a/tests/Integration/Filesystem/ServeFileTest.php +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -14,10 +14,14 @@ protected function setUp(): void { $this->afterApplicationCreated(function () { Storage::put('serve-file-test.txt', 'Hello World'); + Storage::put('serve-file-test.txt?pad=x', 'Hello Question'); }); $this->beforeApplicationDestroyed(function () { - Storage::delete('serve-file-test.txt'); + Storage::delete([ + 'serve-file-test.txt', + 'serve-file-test.txt?pad=x', + ]); }); parent::setUp(); @@ -51,4 +55,22 @@ public function testItWill403OnWrongSignature() $response->assertForbidden(); } + + public function testItCanServeAFileWithUriDelimitersInThePath() + { + $url = Storage::temporaryUrl('serve-file-test.txt?pad=x', Carbon::now()->addMinute()); + + $response = $this->get($url); + + $this->assertSame('Hello Question', $response->streamedContent()); + } + + public function testUriDelimitersInThePathCannotHideAnExpiredUrl() + { + $url = Storage::temporaryUrl('serve-file-test.txt?pad=x', Carbon::now()->subMinute()); + + $response = $this->get($url); + + $response->assertForbidden(); + } } From 96b256d9186c622b828a6eacc6c746e2f33a8555 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 15 May 2026 08:22:19 -0500 Subject: [PATCH 373/596] encode paths --- src/Illuminate/Filesystem/LocalFilesystemAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index e246c8b5b0bb..03377e0f4c80 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -80,7 +80,7 @@ public function temporaryUrl($path, $expiration, array $options = []) return $url->to($url->temporarySignedRoute( 'storage.'.$this->disk, $expiration, - ['path' => $path], + ['path' => rawurldecode($path)], absolute: false )); } @@ -111,7 +111,7 @@ public function temporaryUploadUrl($path, $expiration, array $options = []) 'url' => $url->to($url->temporarySignedRoute( 'storage.'.$this->disk.'.upload', $expiration, - ['path' => $path, 'upload' => true], + ['path' => rawurlencode($path), 'upload' => true], absolute: false )), 'headers' => [], From 912f11dd27ba2895c4fdf52cc17b977dd67987b6 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 15 May 2026 15:56:24 +0100 Subject: [PATCH 374/596] [13.x] Add WorkerIdle event (#60134) * 13.x add worker idle event * surface the name too * use options... --- src/Illuminate/Queue/Events/WorkerIdle.php | 20 ++++++++++++++++++++ src/Illuminate/Queue/Worker.php | 3 +++ tests/Queue/QueueWorkerTest.php | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 src/Illuminate/Queue/Events/WorkerIdle.php diff --git a/src/Illuminate/Queue/Events/WorkerIdle.php b/src/Illuminate/Queue/Events/WorkerIdle.php new file mode 100644 index 000000000000..7d244127d0ed --- /dev/null +++ b/src/Illuminate/Queue/Events/WorkerIdle.php @@ -0,0 +1,20 @@ +sleep($options->rest); } } else { + $this->events->dispatch(new WorkerIdle($connectionName, $queue, $options)); + $this->sleep($options->sleep); } diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index c96d83db3659..30d0b9cfa832 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -15,6 +15,7 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\Events\JobReleasedAfterException; +use Illuminate\Queue\Events\WorkerIdle; use Illuminate\Queue\Events\WorkerStarting; use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Queue\MaxAttemptsExceededException; @@ -444,6 +445,23 @@ public function testWorkerStartingIsDispatched() $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerStarting::class))->once(); } + public function testWorkerIdleIsDispatched() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmpty = true; + + $worker = $this->getWorker('default', ['queue' => []]); + + $worker->daemon('default', 'queue', $workerOptions); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerIdle + && $event->connectionName === 'default' + && $event->queue === 'queue' + && $event->workerOptions === $workerOptions; + }))->once(); + } + public function testWorkerStoppingIsDispatched() { $workerOptions = new WorkerOptions(); From 3df151337a2226f296915af70502ff6845357513 Mon Sep 17 00:00:00 2001 From: Sebastian Cabarcas Berrio <42840369+scabarcas17@users.noreply.github.com> Date: Fri, 15 May 2026 09:56:42 -0500 Subject: [PATCH 375/596] [13.x] Replace @return with @var on docblocks for properties in tests (#60132) --- tests/Foundation/FoundationAuthenticationTest.php | 2 +- tests/Integration/Database/DatabaseTestCase.php | 2 +- ...tTransactionWithAfterCommitUsingDatabaseTransactionsTest.php | 2 +- ...oquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php | 2 +- tests/Integration/Queue/QueueTestCase.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Foundation/FoundationAuthenticationTest.php b/tests/Foundation/FoundationAuthenticationTest.php index 110747cd6b69..6d3f04ec87a4 100644 --- a/tests/Foundation/FoundationAuthenticationTest.php +++ b/tests/Foundation/FoundationAuthenticationTest.php @@ -21,7 +21,7 @@ class FoundationAuthenticationTest extends TestCase protected $app; /** - * @return array + * @var array */ protected $credentials = [ 'email' => 'someone@laravel.com', diff --git a/tests/Integration/Database/DatabaseTestCase.php b/tests/Integration/Database/DatabaseTestCase.php index 14f78bd71e0a..55b87ff7255d 100644 --- a/tests/Integration/Database/DatabaseTestCase.php +++ b/tests/Integration/Database/DatabaseTestCase.php @@ -12,7 +12,7 @@ abstract class DatabaseTestCase extends TestCase /** * The current database driver. * - * @return string + * @var string */ protected $driver; diff --git a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php index 620df3d0b9c7..e3bccff1b0bb 100644 --- a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php +++ b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php @@ -13,7 +13,7 @@ class EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest extends Te /** * The current database driver. * - * @return string + * @var string */ protected $driver; diff --git a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php index 49a66da7c1c9..4f03fcccc4e5 100644 --- a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php +++ b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php @@ -13,7 +13,7 @@ class EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest extends TestCas /** * The current database driver. * - * @return string + * @var string */ protected $driver; diff --git a/tests/Integration/Queue/QueueTestCase.php b/tests/Integration/Queue/QueueTestCase.php index 1c14cbf18f4d..f43b108e3561 100644 --- a/tests/Integration/Queue/QueueTestCase.php +++ b/tests/Integration/Queue/QueueTestCase.php @@ -13,7 +13,7 @@ abstract class QueueTestCase extends TestCase /** * The current database driver. * - * @return string + * @var string */ protected $driver; From f3455c56af2d0fb2f540a3e23a55cd6862c0bc76 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 15 May 2026 16:08:57 +0100 Subject: [PATCH 376/596] [13.x] Pass WorkerOptions to Pausing/Resuming/Interrupted (#60135) * 13.x-pass-workeroptions * import it up --- .../Queue/Events/WorkerInterrupted.php | 4 ++++ src/Illuminate/Queue/Events/WorkerPausing.php | 4 ++++ src/Illuminate/Queue/Events/WorkerResuming.php | 4 ++++ src/Illuminate/Queue/Worker.php | 16 ++++++++-------- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Queue/Events/WorkerInterrupted.php b/src/Illuminate/Queue/Events/WorkerInterrupted.php index 474f9f79c79e..1b4aec4c9988 100644 --- a/src/Illuminate/Queue/Events/WorkerInterrupted.php +++ b/src/Illuminate/Queue/Events/WorkerInterrupted.php @@ -2,6 +2,8 @@ namespace Illuminate\Queue\Events; +use Illuminate\Queue\WorkerOptions; + class WorkerInterrupted { /** @@ -10,11 +12,13 @@ class WorkerInterrupted * @param int $signal The signal that interrupted the worker. * @param string|null $connectionName * @param string|null $queue + * @param WorkerOptions|null $workerOptions */ public function __construct( public int $signal, public ?string $connectionName = null, public ?string $queue = null, + public ?WorkerOptions $workerOptions = null, ) { } } diff --git a/src/Illuminate/Queue/Events/WorkerPausing.php b/src/Illuminate/Queue/Events/WorkerPausing.php index df8445d01f7e..5e8cf9137dda 100644 --- a/src/Illuminate/Queue/Events/WorkerPausing.php +++ b/src/Illuminate/Queue/Events/WorkerPausing.php @@ -2,6 +2,8 @@ namespace Illuminate\Queue\Events; +use Illuminate\Queue\WorkerOptions; + class WorkerPausing { /** @@ -9,10 +11,12 @@ class WorkerPausing * * @param string|null $connectionName * @param string|null $queue + * @param WorkerOptions|null $workerOptions */ public function __construct( public ?string $connectionName = null, public ?string $queue = null, + public ?WorkerOptions $workerOptions = null, ) { } } diff --git a/src/Illuminate/Queue/Events/WorkerResuming.php b/src/Illuminate/Queue/Events/WorkerResuming.php index 106f7a0215c6..9029dd1f687c 100644 --- a/src/Illuminate/Queue/Events/WorkerResuming.php +++ b/src/Illuminate/Queue/Events/WorkerResuming.php @@ -2,6 +2,8 @@ namespace Illuminate\Queue\Events; +use Illuminate\Queue\WorkerOptions; + class WorkerResuming { /** @@ -9,10 +11,12 @@ class WorkerResuming * * @param string|null $connectionName * @param string|null $queue + * @param WorkerOptions|null $workerOptions */ public function __construct( public ?string $connectionName = null, public ?string $queue = null, + public ?WorkerOptions $workerOptions = null, ) { } } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index f6bc8493c79b..86b4cb09e090 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -187,7 +187,7 @@ public function __construct( public function daemon($connectionName, $queue, WorkerOptions $options) { if ($supportsAsyncSignals = $this->supportsAsyncSignals()) { - $this->listenForSignals($connectionName, $queue); + $this->listenForSignals($connectionName, $queue, $options); } $lastRestart = $this->getTimestampOfLastQueueRestart(); @@ -839,30 +839,30 @@ protected function getTimestampOfLastQueueRestart() * @param string|null $queue * @return void */ - protected function listenForSignals($connectionName = null, $queue = null) + protected function listenForSignals($connectionName = null, $queue = null, $options = null) { pcntl_async_signals(true); foreach ([SIGQUIT, SIGTERM, SIGINT] as $signal) { - pcntl_signal($signal, function (int $signal) use ($connectionName, $queue) { + pcntl_signal($signal, function (int $signal) use ($connectionName, $queue, $options) { $this->shouldQuit = true; - $this->events->dispatch(new WorkerInterrupted($signal, $connectionName, $queue)); + $this->events->dispatch(new WorkerInterrupted($signal, $connectionName, $queue, $options)); $this->notifyJobOfSignal($signal); }); } - pcntl_signal(SIGUSR2, function () use ($queue, $connectionName) { + pcntl_signal(SIGUSR2, function () use ($queue, $connectionName, $options) { $this->paused = true; - $this->events->dispatch(new WorkerPausing($connectionName, $queue)); + $this->events->dispatch(new WorkerPausing($connectionName, $queue, $options)); }); - pcntl_signal(SIGCONT, function () use ($connectionName, $queue) { + pcntl_signal(SIGCONT, function () use ($connectionName, $queue, $options) { $this->paused = false; - $this->events->dispatch(new WorkerResuming($connectionName, $queue)); + $this->events->dispatch(new WorkerResuming($connectionName, $queue, $options)); }); } From e5337cd83bc9c9cd8ae89da328b45496e5f91f3f Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sat, 16 May 2026 21:40:08 +0100 Subject: [PATCH 377/596] Update Worker.php (#60145) --- src/Illuminate/Queue/Worker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 86b4cb09e090..40f55ecc4d37 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -330,7 +330,7 @@ protected function timeoutForJob($job, WorkerOptions $options) */ protected function daemonShouldRun(WorkerOptions $options, $connectionName, $queue) { - return ! ((($this->isDownForMaintenance)() && ! $options->force) || + return ! ((! $options->force && ($this->isDownForMaintenance)()) || $this->paused || $this->events->until(new Looping($connectionName, $queue)) === false); } From b2c515a903d513859d6f4bc14600d7abd1add710 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sat, 16 May 2026 21:40:40 +0100 Subject: [PATCH 378/596] skip dem tests (#60143) --- tests/Integration/Filesystem/ReceiveFileTest.php | 3 +++ tests/Integration/Filesystem/ServeFileTest.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/Integration/Filesystem/ReceiveFileTest.php b/tests/Integration/Filesystem/ReceiveFileTest.php index a2aed581f996..8d7ac005dfe5 100644 --- a/tests/Integration/Filesystem/ReceiveFileTest.php +++ b/tests/Integration/Filesystem/ReceiveFileTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Storage; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; #[WithConfig('filesystems.disks.local.serve', true)] class ReceiveFileTest extends TestCase @@ -77,6 +78,7 @@ public function testUploadUrlCannotBeUsedForDownload() $response->assertForbidden(); } + #[RequiresOperatingSystem('Linux|Darwin')] public function testItCanReceiveAFileWithUriDelimitersInThePath() { $result = Storage::temporaryUploadUrl('receive-file-test.txt?pad=x', Carbon::now()->addMinute()); @@ -88,6 +90,7 @@ public function testItCanReceiveAFileWithUriDelimitersInThePath() Storage::assertMissing('receive-file-test.txt'); } + #[RequiresOperatingSystem('Linux|Darwin')] public function testUriDelimitersInThePathCannotHideAnExpiredUploadUrl() { $result = Storage::temporaryUploadUrl('receive-file-test.txt?pad=x', Carbon::now()->subMinute()); diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php index 7a87a1e15647..efc2cb8c3d13 100644 --- a/tests/Integration/Filesystem/ServeFileTest.php +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Storage; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; #[WithConfig('filesystems.disks.local.serve', true)] class ServeFileTest extends TestCase @@ -56,6 +57,7 @@ public function testItWill403OnWrongSignature() $response->assertForbidden(); } + #[RequiresOperatingSystem('Linux|Darwin')] public function testItCanServeAFileWithUriDelimitersInThePath() { $url = Storage::temporaryUrl('serve-file-test.txt?pad=x', Carbon::now()->addMinute()); @@ -65,6 +67,7 @@ public function testItCanServeAFileWithUriDelimitersInThePath() $this->assertSame('Hello Question', $response->streamedContent()); } + #[RequiresOperatingSystem('Linux|Darwin')] public function testUriDelimitersInThePathCannotHideAnExpiredUrl() { $url = Storage::temporaryUrl('serve-file-test.txt?pad=x', Carbon::now()->subMinute()); From df19aecd0d68b45df1ac4da7f003f6583ee9bb52 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Sat, 16 May 2026 16:41:34 -0400 Subject: [PATCH 379/596] Delimit aggregate alias (#60140) * Update tests * Wrap the aggregate alias --- .../Database/Query/Grammars/Grammar.php | 2 +- .../DatabaseMySqlQueryGrammarTest.php | 2 +- tests/Database/DatabaseQueryBuilderTest.php | 44 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 429ae1da4fd2..b61986f41729 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -139,7 +139,7 @@ protected function compileAggregate(Builder $query, $aggregate) $column = 'distinct '.$column; } - return 'select '.$aggregate['function'].'('.$column.') as aggregate'; + return 'select '.$aggregate['function'].'('.$column.') as '.$this->wrap('aggregate'); } /** diff --git a/tests/Database/DatabaseMySqlQueryGrammarTest.php b/tests/Database/DatabaseMySqlQueryGrammarTest.php index bf00d43b60e3..64c5926fffa2 100755 --- a/tests/Database/DatabaseMySqlQueryGrammarTest.php +++ b/tests/Database/DatabaseMySqlQueryGrammarTest.php @@ -52,7 +52,7 @@ public function testTimeoutWithAggregate() $builder->from('users')->timeout(10); $builder->aggregate = ['function' => 'count', 'columns' => ['*']]; $this->assertSame( - 'select /*+ MAX_EXECUTION_TIME(10000) */ count(*) as aggregate from `users`', + 'select /*+ MAX_EXECUTION_TIME(10000) */ count(*) as `aggregate` from `users`', $builder->toSql() ); } diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 8b3888fb0097..e5ddd15fd914 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -2003,31 +2003,31 @@ public function testMySqlUnionLimitsAndOffsets() public function testUnionAggregate() { - $expected = 'select count(*) as aggregate from ((select * from `posts`) union (select * from `videos`)) as `temp_table`'; + $expected = 'select count(*) as `aggregate` from ((select * from `posts`) union (select * from `videos`)) as `temp_table`'; $builder = $this->getMySqlBuilder(); $builder->getConnection()->shouldReceive('select')->once()->with($expected, [], true, []); $builder->getProcessor()->shouldReceive('processSelect')->once(); $builder->from('posts')->union($this->getMySqlBuilder()->from('videos'))->count(); - $expected = 'select count(*) as aggregate from ((select `id` from `posts`) union (select `id` from `videos`)) as `temp_table`'; + $expected = 'select count(*) as `aggregate` from ((select `id` from `posts`) union (select `id` from `videos`)) as `temp_table`'; $builder = $this->getMySqlBuilder(); $builder->getConnection()->shouldReceive('select')->once()->with($expected, [], true, []); $builder->getProcessor()->shouldReceive('processSelect')->once(); $builder->from('posts')->select('id')->union($this->getMySqlBuilder()->from('videos')->select('id'))->count(); - $expected = 'select count(*) as aggregate from ((select * from "posts") union (select * from "videos")) as "temp_table"'; + $expected = 'select count(*) as "aggregate" from ((select * from "posts") union (select * from "videos")) as "temp_table"'; $builder = $this->getPostgresBuilder(); $builder->getConnection()->shouldReceive('select')->once()->with($expected, [], true, []); $builder->getProcessor()->shouldReceive('processSelect')->once(); $builder->from('posts')->union($this->getPostgresBuilder()->from('videos'))->count(); - $expected = 'select count(*) as aggregate from (select * from (select * from "posts") union select * from (select * from "videos")) as "temp_table"'; + $expected = 'select count(*) as "aggregate" from (select * from (select * from "posts") union select * from (select * from "videos")) as "temp_table"'; $builder = $this->getSQLiteBuilder(); $builder->getConnection()->shouldReceive('select')->once()->with($expected, [], true, []); $builder->getProcessor()->shouldReceive('processSelect')->once(); $builder->from('posts')->union($this->getSQLiteBuilder()->from('videos'))->count(); - $expected = 'select count(*) as aggregate from (select * from (select * from [posts]) as [temp_table] union select * from (select * from [videos]) as [temp_table]) as [temp_table]'; + $expected = 'select count(*) as [aggregate] from (select * from (select * from [posts]) as [temp_table] union select * from (select * from [videos]) as [temp_table]) as [temp_table]'; $builder = $this->getSqlServerBuilder(); $builder->getConnection()->shouldReceive('select')->once()->with($expected, [], true, []); $builder->getProcessor()->shouldReceive('processSelect')->once(); @@ -2036,7 +2036,7 @@ public function testUnionAggregate() public function testHavingAggregate() { - $expected = 'select count(*) as aggregate from (select (select `count(*)` from `videos` where `posts`.`id` = `videos`.`post_id`) as `videos_count` from `posts` having `videos_count` > ?) as `temp_table`'; + $expected = 'select count(*) as `aggregate` from (select (select `count(*)` from `videos` where `posts`.`id` = `videos`.`post_id`) as `videos_count` from `posts` having `videos_count` > ?) as `temp_table`'; $builder = $this->getMySqlBuilder(); $builder->getConnection()->shouldReceive('getDatabaseName'); $builder->getConnection()->shouldReceive('select')->once()->with($expected, [0 => 1], true, [])->andReturn([['aggregate' => 1]]); @@ -2732,7 +2732,7 @@ public function testGetCountForPaginationWithBindings() $q->select('body')->from('posts')->where('id', 4); }, 'post'); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -2748,7 +2748,7 @@ public function testGetCountForPaginationWithColumnAliases() $columns = ['body as post_body', 'teaser', 'posts.created as published']; $builder->from('posts')->select($columns); - $builder->getConnection()->shouldReceive('select')->once()->with('select count("body", "teaser", "posts"."created") as aggregate from "posts"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count("body", "teaser", "posts"."created") as "aggregate" from "posts"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -2762,7 +2762,7 @@ public function testGetCountForPaginationWithUnion() $builder = $this->getBuilder(); $builder->from('posts')->select('id')->union($this->getBuilder()->from('videos')->select('id')); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -2776,7 +2776,7 @@ public function testGetCountForPaginationWithUnionOrders() $builder = $this->getBuilder(); $builder->from('posts')->select('id')->union($this->getBuilder()->from('videos')->select('id'))->latest(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -2790,7 +2790,7 @@ public function testGetCountForPaginationWithUnionLimitAndOffset() $builder = $this->getBuilder(); $builder->from('posts')->select('id')->union($this->getBuilder()->from('videos')->select('id'))->limit(15)->offset(1); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -3916,7 +3916,7 @@ public function testRawValueMethodReturnsSingleColumn() public function testAggregateFunctions() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -3934,7 +3934,7 @@ public function testAggregateFunctions() $this->assertTrue($results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -3942,7 +3942,7 @@ public function testAggregateFunctions() $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -3950,7 +3950,7 @@ public function testAggregateFunctions() $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -3958,7 +3958,7 @@ public function testAggregateFunctions() $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select avg("id") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select avg("id") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -3966,7 +3966,7 @@ public function testAggregateFunctions() $this->assertEquals(1, $results); $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select avg("id") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select avg("id") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); @@ -4017,8 +4017,8 @@ public function testDoesntExistsOr() public function testAggregateResetFollowedByGet() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); - $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 2]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 2]]); $builder->getConnection()->shouldReceive('select')->once()->with('select "column1", "column2" from "users"', [], true, [])->andReturn([['column1' => 'foo', 'column2' => 'bar']]); $builder->getProcessor()->shouldReceive('processSelect')->andReturnUsing(function ($builder, $results) { return $results; @@ -4035,7 +4035,7 @@ public function testAggregateResetFollowedByGet() public function testAggregateResetFollowedBySelectGet() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getConnection()->shouldReceive('select')->once()->with('select "column2", "column3" from "users"', [], true, [])->andReturn([['column2' => 'foo', 'column3' => 'bar']]); $builder->getProcessor()->shouldReceive('processSelect')->andReturnUsing(function ($builder, $results) { return $results; @@ -4050,7 +4050,7 @@ public function testAggregateResetFollowedBySelectGet() public function testAggregateResetFollowedByGetWithColumns() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getConnection()->shouldReceive('select')->once()->with('select "column2", "column3" from "users"', [], true, [])->andReturn([['column2' => 'foo', 'column3' => 'bar']]); $builder->getProcessor()->shouldReceive('processSelect')->andReturnUsing(function ($builder, $results) { return $results; @@ -4065,7 +4065,7 @@ public function testAggregateResetFollowedByGetWithColumns() public function testAggregateWithSubSelect() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', [], true, [])->andReturn([['aggregate' => 1]]); + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as "aggregate" from "users"', [], true, [])->andReturn([['aggregate' => 1]]); $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { return $results; }); From 44c1f9466543d5f3f05d07950f1545fa55fb9732 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sat, 16 May 2026 16:47:15 -0400 Subject: [PATCH 380/596] [13.x] Allow lifecycle and output callbacks on Schedule::group() (#60133) Adds support for `before`, `after`, `then`, `onSuccess`, `onFailure` (and their `*WithOutput` / `ping*` / `*OutputTo` variants) when chaining before `Schedule::group()`. Previously these methods reached `Schedule::__call`, which had no way to defer Event-only methods, and threw `BadMethodCallException`. `PendingEventAttributes::__call` now also recognizes a fixed list of deferred Event methods (in addition to user-registered Event macros) and records them to be replayed on every event in the group via `mergeAttributes`. Per-event callbacks declared inside the group closure still apply on top, after the group-level ones. --- .../Scheduling/PendingEventAttributes.php | 33 ++++- .../Console/Scheduling/ScheduleGroupTest.php | 140 ++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php index 10eb7b85b8c0..25f43555f6e9 100644 --- a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php +++ b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php @@ -10,7 +10,36 @@ class PendingEventAttributes use ManagesAttributes, ManagesFrequencies; /** - * The recorded macro calls to replay on each event. + * Event lifecycle and output methods that should be deferred and replayed on each event in the group. + * + * @var array + */ + protected const DEFERRED_EVENT_METHODS = [ + 'before', + 'after', + 'then', + 'thenWithOutput', + 'onSuccess', + 'onSuccessWithOutput', + 'onFailure', + 'onFailureWithOutput', + 'pingBefore', + 'pingBeforeIf', + 'thenPing', + 'thenPingIf', + 'pingOnSuccess', + 'pingOnSuccessIf', + 'pingOnFailure', + 'pingOnFailureIf', + 'sendOutputTo', + 'appendOutputTo', + 'emailOutputTo', + 'emailWrittenOutputTo', + 'emailOutputOnFailure', + ]; + + /** + * The recorded macro and deferred method calls to replay on each event. * * @var array */ @@ -106,7 +135,7 @@ public function mergeAttributes(Event $event): void */ public function __call(string $method, array $parameters): mixed { - if (Event::hasMacro($method)) { + if (Event::hasMacro($method) || in_array($method, static::DEFERRED_EVENT_METHODS, true)) { $this->macros[] = [$method, $parameters]; return $this; diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index 4753425990b7..14cdfe393742 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -328,4 +328,144 @@ public function testNestedGroupInheritsEventMacros() Event::flushMacros(); } + + public function testGroupAppliesOnFailureCallbackToAllEvents() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls) { + $calls[] = 'group-failure'; + }) + ->group(function ($schedule) { + $schedule->command('inspire'); + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertCount(2, $events); + + $events[0]->finish(app(), 1); + $events[1]->finish(app(), 1); + + $this->assertSame(['group-failure', 'group-failure'], $calls); + } + + public function testGroupOnFailureCallbackDoesNotRunOnSuccess() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls) { + $calls[] = 'group-failure'; + }) + ->group(function ($schedule) { + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + + $this->assertSame([], $calls); + } + + public function testGroupAppliesOnSuccessCallbackToAllEvents() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onSuccess(function () use (&$calls) { + $calls[] = 'group-success'; + }) + ->group(function ($schedule) { + $schedule->command('inspire'); + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + $events[1]->finish(app(), 0); + + $this->assertSame(['group-success', 'group-success'], $calls); + } + + public function testGroupAppliesBeforeAndAfterCallbacksToAllEvents() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->before(function () use (&$calls) { + $calls[] = 'before'; + }) + ->after(function () use (&$calls) { + $calls[] = 'after'; + }) + ->then(function () use (&$calls) { + $calls[] = 'then'; + }) + ->group(function ($schedule) { + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->callBeforeCallbacks(app()); + $events[0]->finish(app(), 0); + + $this->assertSame(['before', 'after', 'then'], $calls); + } + + public function testGroupCallbacksCombineWithEventLevelCallbacks() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls) { + $calls[] = 'group'; + }) + ->group(function ($schedule) use (&$calls) { + $schedule->command('inspire')->onFailure(function () use (&$calls) { + $calls[] = 'event'; + }); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 1); + + $this->assertSame(['group', 'event'], $calls); + } + + public function testNestedGroupInheritsLifecycleCallbacks() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls) { + $calls[] = 'outer'; + }) + ->group(function ($schedule) use (&$calls) { + $schedule->command('inspire'); + $schedule->weekly() + ->onFailure(function () use (&$calls) { + $calls[] = 'inner'; + }) + ->group(function ($schedule) { + $schedule->command('inspire'); + }); + }); + + $events = $schedule->events(); + $this->assertCount(2, $events); + + $events[0]->finish(app(), 1); + $this->assertSame(['outer'], $calls); + + $events[1]->finish(app(), 1); + $this->assertSame(['outer', 'outer', 'inner'], $calls); + } } From b6d05cc39f58edd032c18fe344ffaf6b8d4bbc93 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sat, 16 May 2026 16:48:25 -0400 Subject: [PATCH 381/596] [13.x] Allow passing scheduled `Event` in callbacks (#60144) * wip * redundancy * types --- src/Illuminate/Console/Scheduling/Event.php | 48 ++++++++++++++++--- tests/Console/Scheduling/EventTest.php | 47 ++++++++++++++++++ .../Console/Scheduling/CallbackEventTest.php | 47 ++++++++++++++++++ 3 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 9e46d9f05b6b..c5069b271b5b 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -238,7 +238,7 @@ public function finish(Container $container, $exitCode) public function callBeforeCallbacks(Container $container) { foreach ($this->beforeCallbacks as $callback) { - $container->call($callback); + $this->callEventCallback($container, $callback); } } @@ -251,10 +251,44 @@ public function callBeforeCallbacks(Container $container) public function callAfterCallbacks(Container $container) { foreach ($this->afterCallbacks as $callback) { - $container->call($callback); + $this->callEventCallback($container, $callback); } } + /** + * Call the given event callback. + * + * @param \Illuminate\Contracts\Container\Container $container + * @param \Closure $callback + * @param array $parameters + * @return mixed + */ + protected function callEventCallback(Container $container, Closure $callback, array $parameters = []) + { + return $container->call($callback, array_merge( + $this->eventParametersForCallback($callback), $parameters + )); + } + + /** + * Get the event parameters for the given callback. + * + * @param \Closure $callback + * @return array + */ + protected function eventParametersForCallback(Closure $callback) + { + $parameters = $this->closureParameterTypes($callback); + + $eventParameterType = Arr::get($parameters, 'event'); + + if ($eventParameterType === null || ! is_a($this, $eventParameterType)) { + return []; + } + + return ['event' => $this]; + } + /** * Build the command string. * @@ -339,13 +373,13 @@ public function filtersPass($app) $this->lastChecked = Date::now(); foreach ($this->filters as $callback) { - if (! $app->call($callback)) { + if (! $this->callEventCallback($app, $callback)) { return false; } } foreach ($this->rejects as $callback) { - if ($app->call($callback)) { + if ($this->callEventCallback($app, $callback)) { return false; } } @@ -690,7 +724,7 @@ public function onSuccess(Closure $callback) return $this->then(function (Container $container) use ($callback) { if ($this->exitCode === 0) { - $container->call($callback); + $this->callEventCallback($container, $callback); } }); } @@ -725,7 +759,7 @@ public function onFailure(Closure $callback) return $this->then(function (Container $container) use ($callback) { if ($this->exitCode !== 0) { - $container->call($callback); + $this->callEventCallback($container, $callback); } }); } @@ -758,7 +792,7 @@ protected function withOutputCallback(Closure $callback, $onlyIfOutputExists = f return $onlyIfOutputExists && empty($output) ? null - : $container->call($callback, ['output' => new Stringable($output)]); + : $this->callEventCallback($container, $callback, ['output' => new Stringable($output)]); }; } diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 59e34a67f7db..91f70a366854 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -4,6 +4,7 @@ use Illuminate\Console\Scheduling\Event; use Illuminate\Console\Scheduling\EventMutex; +use Illuminate\Container\Container; use Illuminate\Support\Str; use Mockery as m; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; @@ -98,6 +99,52 @@ public function testCustomMutexName() $this->assertSame('fancy-command-description', $event->mutexName()); } + public function testBeforeAndAfterCallbacksCanReceiveEvent() + { + $container = new Container; + $beforeEvent = null; + $afterEvent = null; + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->before(function (Event $event) use (&$beforeEvent) { + $beforeEvent = $event; + }); + + $event->after(function (Event $event) use (&$afterEvent) { + $afterEvent = $event; + }); + + $event->callBeforeCallbacks($container); + $event->callAfterCallbacks($container); + + $this->assertSame($event, $beforeEvent); + $this->assertSame($event, $afterEvent); + } + + public function testFilterCallbacksCanReceiveEvent() + { + $container = new Container; + $filterEvent = null; + $rejectEvent = null; + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->when(function (Event $event) use (&$filterEvent) { + $filterEvent = $event; + + return true; + }); + + $event->skip(function (Event $event) use (&$rejectEvent) { + $rejectEvent = $event; + + return false; + }); + + $this->assertTrue($event->filtersPass($container)); + $this->assertSame($event, $filterEvent); + $this->assertSame($event, $rejectEvent); + } + public function testDaysOfMonthMethod() { $event = new Event(m::mock(EventMutex::class), 'php -i'); diff --git a/tests/Integration/Console/Scheduling/CallbackEventTest.php b/tests/Integration/Console/Scheduling/CallbackEventTest.php index 586857bd4151..0729fd86cc77 100644 --- a/tests/Integration/Console/Scheduling/CallbackEventTest.php +++ b/tests/Integration/Console/Scheduling/CallbackEventTest.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Console\Scheduling\CallbackEvent; use Illuminate\Console\Scheduling\EventMutex; +use Illuminate\Support\Stringable; use Mockery as m; use Orchestra\Testbench\TestCase; @@ -73,4 +74,50 @@ public function testExceptionBubbles() $event->run($this->app); } + + public function testOnSuccessCallbackCanReceiveEvent() + { + $callbackEvent = null; + + $event = (new CallbackEvent(m::mock(EventMutex::class), function () { + }))->onSuccess(function (CallbackEvent $event) use (&$callbackEvent) { + $callbackEvent = $event; + }); + + $event->run($this->app); + + $this->assertSame($event, $callbackEvent); + } + + public function testOnFailureCallbackCanReceiveEvent() + { + $callbackEvent = null; + + $event = (new CallbackEvent(m::mock(EventMutex::class), function () { + return false; + }))->onFailure(function (CallbackEvent $event) use (&$callbackEvent) { + $callbackEvent = $event; + }); + + $event->run($this->app); + + $this->assertSame($event, $callbackEvent); + } + + public function testOutputCallbackCanReceiveEvent() + { + $callbackEvent = null; + $outputValue = null; + + $event = (new CallbackEvent(m::mock(EventMutex::class), function () { + }))->onSuccess(function (Stringable $output, CallbackEvent $event) use (&$callbackEvent, &$outputValue) { + $callbackEvent = $event; + $outputValue = (string) $output; + }); + + $event->run($this->app); + + $this->assertSame($event, $callbackEvent); + $this->assertSame('', $outputValue); + } } From 1e54f4beb3350e04c63890c5721d00a30997d83f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 16 May 2026 15:51:01 -0500 Subject: [PATCH 382/596] wip --- src/Illuminate/Console/Scheduling/Event.php | 68 ++++++++++----------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index c5069b271b5b..4fba1f982e26 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -255,40 +255,6 @@ public function callAfterCallbacks(Container $container) } } - /** - * Call the given event callback. - * - * @param \Illuminate\Contracts\Container\Container $container - * @param \Closure $callback - * @param array $parameters - * @return mixed - */ - protected function callEventCallback(Container $container, Closure $callback, array $parameters = []) - { - return $container->call($callback, array_merge( - $this->eventParametersForCallback($callback), $parameters - )); - } - - /** - * Get the event parameters for the given callback. - * - * @param \Closure $callback - * @return array - */ - protected function eventParametersForCallback(Closure $callback) - { - $parameters = $this->closureParameterTypes($callback); - - $eventParameterType = Arr::get($parameters, 'event'); - - if ($eventParameterType === null || ! is_a($this, $eventParameterType)) { - return []; - } - - return ['event' => $this]; - } - /** * Build the command string. * @@ -796,6 +762,40 @@ protected function withOutputCallback(Closure $callback, $onlyIfOutputExists = f }; } + /** + * Call the given event callback. + * + * @param \Illuminate\Contracts\Container\Container $container + * @param \Closure $callback + * @param array $parameters + * @return mixed + */ + protected function callEventCallback(Container $container, Closure $callback, array $parameters = []) + { + return $container->call($callback, array_merge( + $this->eventParametersForCallback($callback), $parameters + )); + } + + /** + * Get the event parameters for the given callback. + * + * @param \Closure $callback + * @return array + */ + protected function eventParametersForCallback(Closure $callback) + { + $parameters = $this->closureParameterTypes($callback); + + $eventParameterType = Arr::get($parameters, 'event'); + + if ($eventParameterType === null || ! is_a($this, $eventParameterType)) { + return []; + } + + return ['event' => $this]; + } + /** * Get the summary of the event for display. * From f336ba79e744257f84ebf0d096b0ebc62e8e1a4d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 17 May 2026 09:37:23 -0500 Subject: [PATCH 383/596] validate against line breaks in emails (#60151) --- src/Illuminate/Mail/Mailables/Address.php | 6 ++ src/Illuminate/Mail/Message.php | 75 +++++++++++++++---- .../Concerns/ValidatesAttributes.php | 4 + tests/Mail/MailMailerTest.php | 32 ++++++++ tests/Validation/ValidationValidatorTest.php | 3 + 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Mail/Mailables/Address.php b/src/Illuminate/Mail/Mailables/Address.php index 6a03e920e78d..44a8696ff935 100644 --- a/src/Illuminate/Mail/Mailables/Address.php +++ b/src/Illuminate/Mail/Mailables/Address.php @@ -2,6 +2,8 @@ namespace Illuminate\Mail\Mailables; +use InvalidArgumentException; + class Address { /** @@ -26,6 +28,10 @@ class Address */ public function __construct(string $address, ?string $name = null) { + if (preg_match('/[\r\n]/', $address) > 0) { + throw new InvalidArgumentException('Email addresses may not contain line break characters.'); + } + $this->address = $address; $this->name = $name; } diff --git a/src/Illuminate/Mail/Message.php b/src/Illuminate/Mail/Message.php index 54da47430d9d..faef13ffbcad 100755 --- a/src/Illuminate/Mail/Message.php +++ b/src/Illuminate/Mail/Message.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Mail\Attachable; use Illuminate\Support\Collection; use Illuminate\Support\Traits\ForwardsCalls; +use InvalidArgumentException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Part\DataPart; @@ -53,8 +54,8 @@ public function __construct(Email $message) public function from($address, $name = null) { is_array($address) - ? $this->message->from(...$address) - : $this->message->from(new Address($address, (string) $name)); + ? $this->message->from(...$this->ensureAddressesAreSafe($address)) + : $this->message->from($this->createAddress($address, (string) $name)); return $this; } @@ -69,8 +70,8 @@ public function from($address, $name = null) public function sender($address, $name = null) { is_array($address) - ? $this->message->sender(...$address) - : $this->message->sender(new Address($address, (string) $name)); + ? $this->message->sender(...$this->ensureAddressesAreSafe($address)) + : $this->message->sender($this->createAddress($address, (string) $name)); return $this; } @@ -83,6 +84,8 @@ public function sender($address, $name = null) */ public function returnPath($address) { + $this->ensureAddressIsSafe($address); + $this->message->returnPath($address); return $this; @@ -100,8 +103,8 @@ public function to($address, $name = null, $override = false) { if ($override) { is_array($address) - ? $this->message->to(...$address) - : $this->message->to(new Address($address, (string) $name)); + ? $this->message->to(...$this->ensureAddressesAreSafe($address)) + : $this->message->to($this->createAddress($address, (string) $name)); return $this; } @@ -137,8 +140,8 @@ public function cc($address, $name = null, $override = false) { if ($override) { is_array($address) - ? $this->message->cc(...$address) - : $this->message->cc(new Address($address, (string) $name)); + ? $this->message->cc(...$this->ensureAddressesAreSafe($address)) + : $this->message->cc($this->createAddress($address, (string) $name)); return $this; } @@ -174,8 +177,8 @@ public function bcc($address, $name = null, $override = false) { if ($override) { is_array($address) - ? $this->message->bcc(...$address) - : $this->message->bcc(new Address($address, (string) $name)); + ? $this->message->bcc(...$this->ensureAddressesAreSafe($address)) + : $this->message->bcc($this->createAddress($address, (string) $name)); return $this; } @@ -226,28 +229,70 @@ protected function addAddresses($address, $name, $type) $addresses = (new Collection($address))->map(function ($address, $key) { if (is_string($key) && is_string($address)) { - return new Address($key, $address); + return $this->createAddress($key, $address); } if (is_array($address)) { - return new Address($address['email'] ?? $address['address'], $address['name'] ?? null); + return $this->createAddress($address['email'] ?? $address['address'], $address['name'] ?? null); } if (is_null($address)) { - return new Address($key); + return $this->createAddress($key); } - return $address; + return $this->ensureAddressIsSafe($address); })->all(); $this->message->{"{$type}"}(...$addresses); } else { - $this->message->{"add{$type}"}(new Address($address, (string) $name)); + $this->message->{"add{$type}"}($this->createAddress($address, (string) $name)); } return $this; } + /** + * Create a safe Symfony address instance. + * + * @param string $address + * @param string|null $name + * @return \Symfony\Component\Mime\Address + */ + protected function createAddress($address, $name = null) + { + $this->ensureAddressIsSafe($address); + + return new Address($address, (string) $name); + } + + /** + * Ensure the given address cannot inject additional headers or commands. + * + * @param mixed $address + * @return mixed + */ + protected function ensureAddressIsSafe($address) + { + $addressString = $address instanceof Address ? $address->getAddress() : $address; + + if (is_string($addressString) && preg_match('/[\r\n]/', $addressString) > 0) { + throw new InvalidArgumentException('Email addresses may not contain line break characters.'); + } + + return $address; + } + + /** + * Ensure the given addresses cannot inject additional headers or commands. + * + * @param array $addresses + * @return array + */ + protected function ensureAddressesAreSafe(array $addresses) + { + return array_map(fn ($address) => $this->ensureAddressIsSafe($address), $addresses); + } + /** * Add an address debug header for a list of recipients. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index edeed2af813b..32bd38dc2d5f 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -947,6 +947,10 @@ public function validateEmail($attribute, $value, $parameters) return false; } + if (preg_match('/[\r\n]/', (string) $value) > 0) { + return false; + } + $validations = (new Collection($parameters)) ->unique() ->map(fn ($validation) => match (true) { diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index 46e1090cc192..1f0eae2e2d10 100755 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -10,8 +10,10 @@ use Illuminate\Mail\Message; use Illuminate\Mail\Transport\ArrayTransport; use Illuminate\Support\HtmlString; +use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Address; class MailMailerTest extends TestCase { @@ -204,6 +206,36 @@ public function testToAllowsEmailAndName(): void $this->assertSame('Taylor Otwell', $recipients[0]->getName()); } + public function testMailerRejectsAddressesContainingLineBreaks(): void + { + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $mailer = new Mailer('array', $view, new ArrayTransport); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Email addresses may not contain line break characters.'); + + $mailer->send('foo', ['data'], function (Message $message) { + $message->to("\"foo\r\nBcc: victim@example.com\"@example.com")->from('hello@laravel.com'); + }); + } + + public function testMailerRejectsSymfonyAddressesContainingLineBreaks(): void + { + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $mailer = new Mailer('array', $view, new ArrayTransport); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Email addresses may not contain line break characters.'); + + $mailer->send('foo', ['data'], function (Message $message) { + $message->to(new Address("\"foo\r\nBcc: victim@example.com\"@example.com"))->from('hello@laravel.com'); + }); + } + public function testGlobalFromIsRespectedOnAllMessages(): void { $view = m::mock(Factory::class); diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index b7a0ced33f96..ea3ade02b330 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -4953,6 +4953,9 @@ public function __toString() $v = new Validator($trans, ['x' => 'foo@gmail.com'], ['x' => 'Email']); $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => "\"foo\r\nBcc: victim@example.com\"@example.com"], ['x' => 'Email']); + $this->assertFalse($v->passes()); } public function testValidateEmailWithInternationalCharacters() From 96e9a663db657cf37ef26cd794fda8ff60eaa451 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 17 May 2026 09:42:21 -0500 Subject: [PATCH 384/596] port fix --- src/Illuminate/Mail/Mailables/Address.php | 6 ++ src/Illuminate/Mail/Message.php | 75 +++++++++++++++---- .../Concerns/ValidatesAttributes.php | 4 + tests/Mail/MailMailerTest.php | 32 ++++++++ tests/Validation/ValidationValidatorTest.php | 3 + 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Mail/Mailables/Address.php b/src/Illuminate/Mail/Mailables/Address.php index 6a03e920e78d..44a8696ff935 100644 --- a/src/Illuminate/Mail/Mailables/Address.php +++ b/src/Illuminate/Mail/Mailables/Address.php @@ -2,6 +2,8 @@ namespace Illuminate\Mail\Mailables; +use InvalidArgumentException; + class Address { /** @@ -26,6 +28,10 @@ class Address */ public function __construct(string $address, ?string $name = null) { + if (preg_match('/[\r\n]/', $address) > 0) { + throw new InvalidArgumentException('Email addresses may not contain line break characters.'); + } + $this->address = $address; $this->name = $name; } diff --git a/src/Illuminate/Mail/Message.php b/src/Illuminate/Mail/Message.php index 54da47430d9d..faef13ffbcad 100755 --- a/src/Illuminate/Mail/Message.php +++ b/src/Illuminate/Mail/Message.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Mail\Attachable; use Illuminate\Support\Collection; use Illuminate\Support\Traits\ForwardsCalls; +use InvalidArgumentException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Part\DataPart; @@ -53,8 +54,8 @@ public function __construct(Email $message) public function from($address, $name = null) { is_array($address) - ? $this->message->from(...$address) - : $this->message->from(new Address($address, (string) $name)); + ? $this->message->from(...$this->ensureAddressesAreSafe($address)) + : $this->message->from($this->createAddress($address, (string) $name)); return $this; } @@ -69,8 +70,8 @@ public function from($address, $name = null) public function sender($address, $name = null) { is_array($address) - ? $this->message->sender(...$address) - : $this->message->sender(new Address($address, (string) $name)); + ? $this->message->sender(...$this->ensureAddressesAreSafe($address)) + : $this->message->sender($this->createAddress($address, (string) $name)); return $this; } @@ -83,6 +84,8 @@ public function sender($address, $name = null) */ public function returnPath($address) { + $this->ensureAddressIsSafe($address); + $this->message->returnPath($address); return $this; @@ -100,8 +103,8 @@ public function to($address, $name = null, $override = false) { if ($override) { is_array($address) - ? $this->message->to(...$address) - : $this->message->to(new Address($address, (string) $name)); + ? $this->message->to(...$this->ensureAddressesAreSafe($address)) + : $this->message->to($this->createAddress($address, (string) $name)); return $this; } @@ -137,8 +140,8 @@ public function cc($address, $name = null, $override = false) { if ($override) { is_array($address) - ? $this->message->cc(...$address) - : $this->message->cc(new Address($address, (string) $name)); + ? $this->message->cc(...$this->ensureAddressesAreSafe($address)) + : $this->message->cc($this->createAddress($address, (string) $name)); return $this; } @@ -174,8 +177,8 @@ public function bcc($address, $name = null, $override = false) { if ($override) { is_array($address) - ? $this->message->bcc(...$address) - : $this->message->bcc(new Address($address, (string) $name)); + ? $this->message->bcc(...$this->ensureAddressesAreSafe($address)) + : $this->message->bcc($this->createAddress($address, (string) $name)); return $this; } @@ -226,28 +229,70 @@ protected function addAddresses($address, $name, $type) $addresses = (new Collection($address))->map(function ($address, $key) { if (is_string($key) && is_string($address)) { - return new Address($key, $address); + return $this->createAddress($key, $address); } if (is_array($address)) { - return new Address($address['email'] ?? $address['address'], $address['name'] ?? null); + return $this->createAddress($address['email'] ?? $address['address'], $address['name'] ?? null); } if (is_null($address)) { - return new Address($key); + return $this->createAddress($key); } - return $address; + return $this->ensureAddressIsSafe($address); })->all(); $this->message->{"{$type}"}(...$addresses); } else { - $this->message->{"add{$type}"}(new Address($address, (string) $name)); + $this->message->{"add{$type}"}($this->createAddress($address, (string) $name)); } return $this; } + /** + * Create a safe Symfony address instance. + * + * @param string $address + * @param string|null $name + * @return \Symfony\Component\Mime\Address + */ + protected function createAddress($address, $name = null) + { + $this->ensureAddressIsSafe($address); + + return new Address($address, (string) $name); + } + + /** + * Ensure the given address cannot inject additional headers or commands. + * + * @param mixed $address + * @return mixed + */ + protected function ensureAddressIsSafe($address) + { + $addressString = $address instanceof Address ? $address->getAddress() : $address; + + if (is_string($addressString) && preg_match('/[\r\n]/', $addressString) > 0) { + throw new InvalidArgumentException('Email addresses may not contain line break characters.'); + } + + return $address; + } + + /** + * Ensure the given addresses cannot inject additional headers or commands. + * + * @param array $addresses + * @return array + */ + protected function ensureAddressesAreSafe(array $addresses) + { + return array_map(fn ($address) => $this->ensureAddressIsSafe($address), $addresses); + } + /** * Add an address debug header for a list of recipients. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 00d05af58f63..36ba2b2e0760 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -943,6 +943,10 @@ public function validateEmail($attribute, $value, $parameters) return false; } + if (preg_match('/[\r\n]/', (string) $value) > 0) { + return false; + } + $validations = (new Collection($parameters)) ->unique() ->map(fn ($validation) => match (true) { diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index 46e1090cc192..1f0eae2e2d10 100755 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -10,8 +10,10 @@ use Illuminate\Mail\Message; use Illuminate\Mail\Transport\ArrayTransport; use Illuminate\Support\HtmlString; +use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Address; class MailMailerTest extends TestCase { @@ -204,6 +206,36 @@ public function testToAllowsEmailAndName(): void $this->assertSame('Taylor Otwell', $recipients[0]->getName()); } + public function testMailerRejectsAddressesContainingLineBreaks(): void + { + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $mailer = new Mailer('array', $view, new ArrayTransport); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Email addresses may not contain line break characters.'); + + $mailer->send('foo', ['data'], function (Message $message) { + $message->to("\"foo\r\nBcc: victim@example.com\"@example.com")->from('hello@laravel.com'); + }); + } + + public function testMailerRejectsSymfonyAddressesContainingLineBreaks(): void + { + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $mailer = new Mailer('array', $view, new ArrayTransport); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Email addresses may not contain line break characters.'); + + $mailer->send('foo', ['data'], function (Message $message) { + $message->to(new Address("\"foo\r\nBcc: victim@example.com\"@example.com"))->from('hello@laravel.com'); + }); + } + public function testGlobalFromIsRespectedOnAllMessages(): void { $view = m::mock(Factory::class); diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 9920a81dd3ec..48651de865c4 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -4831,6 +4831,9 @@ public function __toString() $v = new Validator($trans, ['x' => 'foo@gmail.com'], ['x' => 'Email']); $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => "\"foo\r\nBcc: victim@example.com\"@example.com"], ['x' => 'Email']); + $this->assertFalse($v->passes()); } public function testValidateEmailWithInternationalCharacters() From 19b9d2fae6989de08a9781cef1384e3626b7c319 Mon Sep 17 00:00:00 2001 From: Wes Hooper Date: Sun, 17 May 2026 15:43:05 +0100 Subject: [PATCH 385/596] [13.x] Add `assertPushedOnce()` (#60150) --- src/Illuminate/Support/Testing/Fakes/QueueFake.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index afab3ecce0dc..9d6fb7503c99 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -148,6 +148,17 @@ public function assertPushedTimes($job, $times = 1) ); } + /** + * Assert if a job was pushed exactly once. + * + * @param string $job + * @return void + */ + public function assertPushedOnce($job) + { + $this->assertPushedTimes($job, 1); + } + /** * Assert if a job was pushed based on a truth-test callback. * From 57667978f43fc01170163c65d508166d69c406a4 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sun, 17 May 2026 14:43:37 +0000 Subject: [PATCH 386/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index a38620a1d95e..da23a3585ba9 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -56,6 +56,7 @@ * @method static \Illuminate\Support\Testing\Fakes\QueueFake except(array|string $jobsToBeQueued) * @method static void assertPushed(string|\Closure $job, callable|int|null $callback = null) * @method static void assertPushedTimes(string $job, int $times = 1) + * @method static void assertPushedOnce(string $job) * @method static void assertPushedOn(\UnitEnum|string $queue, string|\Closure $job, callable|null $callback = null) * @method static void assertPushedWithChain(string $job, array $expectedChain = [], callable|null $callback = null) * @method static void assertPushedWithoutChain(string $job, callable|null $callback = null) From 809d789689512d1f5d9a12102049627c90332154 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sun, 17 May 2026 10:43:45 -0400 Subject: [PATCH 387/596] allow for callables (#60148) --- src/Illuminate/Console/Scheduling/Event.php | 10 ++++-- tests/Console/Scheduling/EventTest.php | 35 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 4fba1f982e26..0dad9e51cbab 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -766,14 +766,18 @@ protected function withOutputCallback(Closure $callback, $onlyIfOutputExists = f * Call the given event callback. * * @param \Illuminate\Contracts\Container\Container $container - * @param \Closure $callback + * @param callable $callback * @param array $parameters * @return mixed */ - protected function callEventCallback(Container $container, Closure $callback, array $parameters = []) + protected function callEventCallback(Container $container, callable $callback, array $parameters = []) { + $eventParameters = $callback instanceof Closure + ? $this->eventParametersForCallback($callback) + : []; + return $container->call($callback, array_merge( - $this->eventParametersForCallback($callback), $parameters + $eventParameters, $parameters )); } diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 91f70a366854..b7e9f31ff19e 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -145,6 +145,41 @@ public function testFilterCallbacksCanReceiveEvent() $this->assertSame($event, $rejectEvent); } + public function testFilterCallbacksMayBeInvokableObjects() + { + $container = new Container; + $filter = new class + { + public int $calls = 0; + + public function __invoke(): bool + { + $this->calls++; + + return true; + } + }; + $reject = new class + { + public int $calls = 0; + + public function __invoke(): bool + { + $this->calls++; + + return false; + } + }; + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->when($filter); + $event->skip($reject); + + $this->assertTrue($event->filtersPass($container)); + $this->assertSame(1, $filter->calls); + $this->assertSame(1, $reject->calls); + } + public function testDaysOfMonthMethod() { $event = new Event(m::mock(EventMutex::class), 'php -i'); From 7c083565f2973e9d15c9a1fc882ab11705933a08 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Sun, 17 May 2026 18:14:33 +0330 Subject: [PATCH 388/596] [12.x] Fix Number::fileSize() handling of negative byte values (#60147) --- src/Illuminate/Support/Number.php | 2 +- tests/Support/SupportNumberTest.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 9e7918d1e8ff..8ee7966fab0d 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -209,7 +209,7 @@ public static function fileSize(int|float $bytes, int $precision = 0, ?int $maxP $unitCount = count($units); - for ($i = 0; ($bytes / 1024) > 0.9 && ($i < $unitCount - 1); $i++) { + for ($i = 0; (abs($bytes) / 1024) > 0.9 && ($i < $unitCount - 1); $i++) { $bytes /= 1024; } diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 7f7de7f3f1a2..0122bfdae5d0 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -186,6 +186,12 @@ public function testBytesToHuman() $this->assertSame('1 ZB', Number::fileSize(1024 ** 7)); $this->assertSame('1 YB', Number::fileSize(1024 ** 8)); $this->assertSame('1,024 YB', Number::fileSize(1024 ** 9)); + + $this->assertSame('-1 B', Number::fileSize(-1)); + $this->assertSame('-2 KB', Number::fileSize(-2048)); + $this->assertSame('-2.00 KB', Number::fileSize(-2048, precision: 2)); + $this->assertSame('-1.23 KB', Number::fileSize(-1264, precision: 2)); + $this->assertSame('-5 GB', Number::fileSize(-1024 * 1024 * 1024 * 5)); } public function testClamp() From 7da1aa44b82a37523f03f10903e94c8ddd04445c Mon Sep 17 00:00:00 2001 From: Fazle Rabbi <35403788+irabbi360@users.noreply.github.com> Date: Sun, 17 May 2026 20:46:42 +0600 Subject: [PATCH 389/596] Fix numeric property names being cast to integers in JsonSchema required array #60146 (#60149) --- src/Illuminate/JsonSchema/Serializer.php | 11 +++++++---- tests/JsonSchema/ObjectTypeTest.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/JsonSchema/Serializer.php b/src/Illuminate/JsonSchema/Serializer.php index 0c60c8382771..99811c8f96c9 100644 --- a/src/Illuminate/JsonSchema/Serializer.php +++ b/src/Illuminate/JsonSchema/Serializer.php @@ -53,10 +53,13 @@ public static function serialize(Types\Type $type): array if (count($attributes['properties']) === 0) { unset($attributes['properties']); } else { - $required = array_keys(array_filter( - $attributes['properties'], - static fn (Types\Type $property) => static::isRequired($property), - )); + $required = array_map( + 'strval', + array_keys(array_filter( + $attributes['properties'], + static fn (Types\Type $property) => static::isRequired($property), + )) + ); if ($required !== []) { $attributes['required'] = $required; diff --git a/tests/JsonSchema/ObjectTypeTest.php b/tests/JsonSchema/ObjectTypeTest.php index 362d20f32f2d..1551d858b3d7 100644 --- a/tests/JsonSchema/ObjectTypeTest.php +++ b/tests/JsonSchema/ObjectTypeTest.php @@ -78,6 +78,20 @@ public function test_it_may_be_initialized_with_a_closure_but_may_have_propertie ], $type->toArray()); } + public function test_numeric_string_property_names_remain_strings_in_required_array(): void + { + $type = JsonSchema::object([ + '1' => JsonSchema::string()->required(), + '4' => JsonSchema::string()->required(), + ]); + + $array = $type->toArray(); + + $this->assertSame(['1', '4'], $array['required']); + $this->assertIsString($array['required'][0]); + $this->assertIsString($array['required'][1]); + } + public function test_it_may_disable_additional_properties(): void { $type = JsonSchema::object()->default(['age' => 1])->withoutAdditionalProperties(); From 962d8f1ef7d0f7ff64e4856890b4ddb64d84c91a Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 17 May 2026 20:50:49 +0100 Subject: [PATCH 390/596] 13.x-workeroptions-looping (#60153) --- src/Illuminate/Queue/Events/Looping.php | 2 ++ src/Illuminate/Queue/Worker.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Events/Looping.php b/src/Illuminate/Queue/Events/Looping.php index 84088c3107b2..efe2295bd38a 100644 --- a/src/Illuminate/Queue/Events/Looping.php +++ b/src/Illuminate/Queue/Events/Looping.php @@ -9,10 +9,12 @@ class Looping * * @param string $connectionName The connection name. * @param string $queue The queue name. + * @param \Illuminate\Queue\WorkerOptions|null $workerOptions The worker options. */ public function __construct( public $connectionName, public $queue, + public $workerOptions = null, ) { } } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 40f55ecc4d37..7ecb843e887a 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -332,7 +332,7 @@ protected function daemonShouldRun(WorkerOptions $options, $connectionName, $que { return ! ((! $options->force && ($this->isDownForMaintenance)()) || $this->paused || - $this->events->until(new Looping($connectionName, $queue)) === false); + $this->events->until(new Looping($connectionName, $queue, $options)) === false); } /** From e48c51d231de06b4215a9f517e8a75ec78386fdd Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Mon, 18 May 2026 14:47:08 +0200 Subject: [PATCH 391/596] Support enum queue names in QueueFake (#60161) --- .../Support/Testing/Fakes/QueueFake.php | 38 ++++++++------ tests/Support/SupportTestingQueueFakeTest.php | 49 ++++++++++++++++++- 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index 9d6fb7503c99..fe3df9b8dc9e 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -428,11 +428,13 @@ public function connection($value = null) /** * Get the size of the queue. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return int */ public function size($queue = null) { + $queue = enum_value($queue); + return (new Collection($this->jobs)) ->flatten(1) ->filter(fn ($job) => $job['queue'] === $queue) @@ -442,7 +444,7 @@ public function size($queue = null) /** * Get the number of pending jobs. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return int */ public function pendingSize($queue = null) @@ -453,7 +455,7 @@ public function pendingSize($queue = null) /** * Get the number of delayed jobs. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return int */ public function delayedSize($queue = null) @@ -464,7 +466,7 @@ public function delayedSize($queue = null) /** * Get the number of reserved jobs. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return int */ public function reservedSize($queue = null) @@ -475,11 +477,13 @@ public function reservedSize($queue = null) /** * Get the pending jobs for the given queue. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return \Illuminate\Support\Collection */ public function pendingJobs($queue = null): Collection { + $queue = enum_value($queue); + return (new Collection($this->jobs)) ->flatten(1) ->filter(fn ($job) => $job['queue'] === $queue) @@ -496,7 +500,7 @@ public function pendingJobs($queue = null): Collection /** * Get the delayed jobs for the given queue. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return \Illuminate\Support\Collection */ public function delayedJobs($queue = null): Collection @@ -507,7 +511,7 @@ public function delayedJobs($queue = null): Collection /** * Get the reserved jobs for the given queue. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return \Illuminate\Support\Collection */ public function reservedJobs($queue = null): Collection @@ -557,7 +561,7 @@ public function allReservedJobs(): Collection /** * Get the creation timestamp of the oldest pending job, excluding delayed jobs. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return int|null */ public function creationTimeOfOldestPendingJob($queue = null) @@ -570,11 +574,13 @@ public function creationTimeOfOldestPendingJob($queue = null) * * @param string|object $job * @param mixed $data - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return mixed */ public function push($job, $data = '', $queue = null) { + $queue = enum_value($queue); + if ($this->shouldFakeJob($job)) { if ($job instanceof Closure) { $job = CallQueuedClosure::create($job); @@ -638,12 +644,14 @@ protected function shouldDispatchJob($job) * Push a raw payload onto the queue. * * @param string $payload - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @param array $options * @return mixed */ public function pushRaw($payload, $queue = null, array $options = []) { + $queue = enum_value($queue); + $this->rawPushes[] = [ 'payload' => $payload, 'queue' => $queue, @@ -657,7 +665,7 @@ public function pushRaw($payload, $queue = null, array $options = []) * @param \DateTimeInterface|\DateInterval|int $delay * @param string|object $job * @param mixed $data - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return mixed */ public function later($delay, $job, $data = '', $queue = null) @@ -668,7 +676,7 @@ public function later($delay, $job, $data = '', $queue = null) /** * Push a new job onto the queue. * - * @param string $queue + * @param \UnitEnum|string $queue * @param string|object $job * @param mixed $data * @return mixed @@ -681,7 +689,7 @@ public function pushOn($queue, $job, $data = '') /** * Push a new job onto a specific queue after (n) seconds. * - * @param string $queue + * @param \UnitEnum|string $queue * @param \DateTimeInterface|\DateInterval|int $delay * @param string|object $job * @param mixed $data @@ -695,7 +703,7 @@ public function laterOn($queue, $delay, $job, $data = '') /** * Pop the next job off of the queue. * - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return \Illuminate\Contracts\Queue\Job|null */ public function pop($queue = null) @@ -708,7 +716,7 @@ public function pop($queue = null) * * @param array $jobs * @param mixed $data - * @param string|null $queue + * @param \UnitEnum|string|null $queue * @return mixed */ public function bulk($jobs, $data = '', $queue = null) diff --git a/tests/Support/SupportTestingQueueFakeTest.php b/tests/Support/SupportTestingQueueFakeTest.php index 88b9093f2a38..66ed7062381b 100644 --- a/tests/Support/SupportTestingQueueFakeTest.php +++ b/tests/Support/SupportTestingQueueFakeTest.php @@ -96,6 +96,15 @@ public function testQueueSize() $this->assertEquals(1, $this->fake->size()); } + public function testQueueSizeAcceptsUnitEnums() + { + $this->fake->push($this->job, '', QueueNameEnumStub::Foo); + + $this->assertEquals(1, $this->fake->size('foo')); + $this->assertEquals(1, $this->fake->size(QueueNameEnumStub::Foo)); + $this->assertEquals(0, $this->fake->size(QueueNameEnumStub::Bar)); + } + public function testAssertNotPushed() { $this->fake->push($this->job); @@ -209,16 +218,26 @@ public function testAssertPushedUsingBulk() { $this->fake->assertNothingPushed(); - $queue = 'my-test-queue'; + $queue = QueueNameEnumStub::Foo; $this->fake->bulk([ $this->job, new JobStub, ], null, $queue); + $this->fake->assertPushedOn('foo', JobStub::class); $this->fake->assertPushedOn($queue, JobStub::class); $this->fake->assertPushed(JobStub::class, 2); } + public function testPushOnAndLaterOnAcceptUnitEnums() + { + $this->fake->pushOn(QueueNameEnumStub::Foo, $this->job); + $this->fake->laterOn(QueueNameEnumStub::Bar, 10, new JobToFakeStub); + + $this->fake->assertPushedOn('foo', JobStub::class); + $this->fake->assertPushedOn('bar', JobToFakeStub::class); + } + public function testAssertPushedWithChainUsingClassesOrObjectsArray() { $this->fake->push(new JobWithChainStub([ @@ -497,6 +516,17 @@ public function testPendingJobs() $this->assertSame(0, $pending->first()->attempts); } + public function testPendingJobsAcceptsUnitEnums() + { + $this->fake->push($this->job, '', QueueNameEnumStub::Foo); + $this->fake->push(new JobToFakeStub, '', QueueNameEnumStub::Bar); + + $pending = $this->fake->pendingJobs(QueueNameEnumStub::Foo); + + $this->assertCount(1, $pending); + $this->assertSame(JobStub::class, $pending->first()->name); + } + public function testAllPendingJobs() { $this->fake->push($this->job, '', 'foo'); @@ -523,6 +553,23 @@ public function testGetRawPushes() ], $actualPushedRaw); } + public function testRawPushesAcceptUnitEnums() + { + $this->fake->pushRaw('some-payload', QueueNameEnumStub::Foo, ['options' => 'yeah']); + + $this->assertEqualsCanonicalizing([ + ['payload' => 'some-payload', 'queue' => 'foo', 'options' => ['options' => 'yeah']], + ], $this->fake->rawPushes()); + + $pushedRaw = $this->fake->pushedRaw( + fn ($payload, $queue, $options) => $payload === 'some-payload' + && $queue === 'foo' + && $options['options'] === 'yeah' + ); + + $this->assertCount(1, $pushedRaw); + } + public function testPushedRaw() { $this->fake->pushRaw('some-payload', null, ['options' => 'yeah']); From 0cf0e7af5183e071fdc4559a76cbd82e0a1ecc6b Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 18 May 2026 12:47:43 +0000 Subject: [PATCH 392/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index da23a3585ba9..5fa7f3d9fc03 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -69,9 +69,9 @@ * @method static \Illuminate\Support\Collection pushedRaw(null|\Closure $callback = null) * @method static \Illuminate\Support\Collection listenersPushed(string $listenerClass, \Closure|null $callback = null) * @method static bool hasPushed(string $job) - * @method static \Illuminate\Support\Collection pendingJobs(string|null $queue = null) - * @method static \Illuminate\Support\Collection delayedJobs(string|null $queue = null) - * @method static \Illuminate\Support\Collection reservedJobs(string|null $queue = null) + * @method static \Illuminate\Support\Collection pendingJobs(\UnitEnum|string|null $queue = null) + * @method static \Illuminate\Support\Collection delayedJobs(\UnitEnum|string|null $queue = null) + * @method static \Illuminate\Support\Collection reservedJobs(\UnitEnum|string|null $queue = null) * @method static \Illuminate\Support\Collection allPendingJobs() * @method static \Illuminate\Support\Collection allDelayedJobs() * @method static \Illuminate\Support\Collection allReservedJobs() From 55ff9e2af38ac3ce0e7db30377109c19880814c3 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 18 May 2026 16:07:27 +0200 Subject: [PATCH 393/596] Remove obsolete @phpstan-ignore clauses (#60164) --- types/Support/Helpers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/Support/Helpers.php b/types/Support/Helpers.php index e9be774efbe5..999e55969d03 100644 --- a/types/Support/Helpers.php +++ b/types/Support/Helpers.php @@ -44,7 +44,7 @@ function testThrowIf(float|int $foo, ?DateTime $bar = null): void { rescue(fn () => assertType('never', throw_if(true, Exception::class))); - assertType('false', throw_if(false, Exception::class)); // @phpstan-ignore deadCode.unreachable + assertType('false', throw_if(false, Exception::class)); assertType('false', throw_if(empty($foo))); throw_if(is_float($foo)); assertType('int', $foo); @@ -63,7 +63,7 @@ function testThrowUnless(float|int $foo, ?DateTime $bar = null): void { assertType('true', throw_unless(true, Exception::class)); rescue(fn () => assertType('never', throw_unless(false, Exception::class))); - assertType('true', throw_unless(empty($foo))); // @phpstan-ignore deadCode.unreachable + assertType('true', throw_unless(empty($foo))); throw_unless(is_int($foo)); assertType('int', $foo); throw_unless($foo == false); From 3ff95bcad71c61848c2c75c51dcafdbc016826f8 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 18 May 2026 16:08:04 +0200 Subject: [PATCH 394/596] [13.x] Consistent test OS attributes (#60162) * Replace RequiresOperatingSystemFamily attributes with RequiresOperatingSystem * Fixup tearDown issue in SerializableClosureV1CacheRouteTest * Consistently use 'Linux|Darwin' matcher for tests --- tests/Filesystem/JoinPathsHelperTest.php | 2 +- .../Exceptions/Renderer/FrameTest.php | 4 +-- .../Concurrency/ConcurrencyTest.php | 2 +- .../Database/MySql/JoinLateralTest.php | 4 +-- .../Database/Postgres/JoinLateralTest.php | 4 +-- .../SerializableClosureV1CacheRouteTest.php | 6 +++-- tests/Process/ProcessTest.php | 26 +++++++++---------- 7 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/Filesystem/JoinPathsHelperTest.php b/tests/Filesystem/JoinPathsHelperTest.php index ddaba7a3a4a5..bdd5238a7b93 100644 --- a/tests/Filesystem/JoinPathsHelperTest.php +++ b/tests/Filesystem/JoinPathsHelperTest.php @@ -10,7 +10,7 @@ class JoinPathsHelperTest extends TestCase { - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] #[DataProvider('unixDataProvider')] public function testItCanMergePathsForUnix(string $expected, string $given) { diff --git a/tests/Foundation/Exceptions/Renderer/FrameTest.php b/tests/Foundation/Exceptions/Renderer/FrameTest.php index 734be0021c36..9954beb106cf 100644 --- a/tests/Foundation/Exceptions/Renderer/FrameTest.php +++ b/tests/Foundation/Exceptions/Renderer/FrameTest.php @@ -11,7 +11,7 @@ class FrameTest extends TestCase { - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] #[DataProvider('unixFileDataProvider')] public function test_it_normalizes_file_path_on_unix($frameData, $basePath, $expected) { @@ -81,7 +81,7 @@ public static function windowsFileDataProvider() ]; } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] #[DataProvider('unixIsFromVendorDataProvider')] public function test_it_determines_if_frame_is_from_vendor_on_unix($frameData, $basePath, $expected) { diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index 83f31faae841..0659d2d27df4 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -12,7 +12,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; -#[RequiresOperatingSystem('Linux|DAR')] +#[RequiresOperatingSystem('Linux|Darwin')] class ConcurrencyTest extends TestCase { protected function setUp(): void diff --git a/tests/Integration/Database/MySql/JoinLateralTest.php b/tests/Integration/Database/MySql/JoinLateralTest.php index 87548ddcc30a..95136114ad59 100644 --- a/tests/Integration/Database/MySql/JoinLateralTest.php +++ b/tests/Integration/Database/MySql/JoinLateralTest.php @@ -6,11 +6,11 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; -use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\Attributes\RequiresPhpExtension; #[RequiresPhpExtension('pdo_mysql')] -#[RequiresOperatingSystemFamily('Linux|Darwin')] +#[RequiresOperatingSystem('Linux|Darwin')] class JoinLateralTest extends MySqlTestCase { protected function afterRefreshingDatabase() diff --git a/tests/Integration/Database/Postgres/JoinLateralTest.php b/tests/Integration/Database/Postgres/JoinLateralTest.php index acab3781316b..e054eb140697 100644 --- a/tests/Integration/Database/Postgres/JoinLateralTest.php +++ b/tests/Integration/Database/Postgres/JoinLateralTest.php @@ -6,11 +6,11 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; -use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\Attributes\RequiresPhpExtension; #[RequiresPhpExtension('pdo_pgsql')] -#[RequiresOperatingSystemFamily('Linux|Darwin')] +#[RequiresOperatingSystem('Linux|Darwin')] class JoinLateralTest extends PostgresTestCase { protected function afterRefreshingDatabase() diff --git a/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php b/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php index b819ba9c27ab..a8f290751c6e 100644 --- a/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php +++ b/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php @@ -7,11 +7,11 @@ use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; -use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use function Illuminate\Filesystem\join_paths; -#[RequiresOperatingSystemFamily('Linux|Darwin')] +#[RequiresOperatingSystem('Linux|Darwin')] #[WithConfig('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF')] #[WithMigration] class SerializableClosureV1CacheRouteTest extends TestCase @@ -40,6 +40,8 @@ protected function setUp(): void #[\Override] protected function tearDown(): void { + parent::tearDown(); + unset($_ENV['APP_ROUTES_CACHE']); } diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 69e28a56841c..8f58a506b8b2 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -496,7 +496,7 @@ public function testFakeProcessesDontThrowIfFalse() $this->assertTrue(true); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanHaveErrorOutput() { $factory = new Factory; @@ -524,7 +524,7 @@ public function testFakeProcessesCanThrowWithoutOutput() $result->throw(); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanThrowWithoutOutput() { $this->expectException(ProcessFailedException::class); @@ -562,7 +562,7 @@ public function testFakeProcessesCanThrowWithErrorOutput() $result->throw(); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanThrowWithErrorOutput() { $this->expectException(ProcessFailedException::class); @@ -604,7 +604,7 @@ public function testFakeProcessesCanThrowWithOutput() $result->throw(); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanThrowWithOutput() { $this->expectException(ProcessFailedException::class); @@ -625,7 +625,7 @@ public function testRealProcessesCanThrowWithOutput() $result->throw(); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanTimeout() { $this->expectException(ProcessTimedOutException::class); @@ -639,7 +639,7 @@ public function testRealProcessesCanTimeout() $result->throw(); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testATimeoutCanBeSetWithACarbonInterval() { $this->expectException(ProcessTimedOutException::class); @@ -654,7 +654,7 @@ public function testATimeoutCanBeSetWithACarbonInterval() $result->throw(); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanThrowIfTrue() { $this->expectException(ProcessFailedException::class); @@ -665,7 +665,7 @@ public function testRealProcessesCanThrowIfTrue() $result->throwIf(true); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesDoesntThrowIfFalse() { $factory = new Factory; @@ -676,7 +676,7 @@ public function testRealProcessesDoesntThrowIfFalse() $this->assertTrue(true); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testRealProcessesCanUseStandardInput() { $factory = new Factory(); @@ -685,7 +685,7 @@ public function testRealProcessesCanUseStandardInput() $this->assertSame('foobar', $result->output()); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testProcessPipe() { $factory = new Factory; @@ -701,7 +701,7 @@ public function testProcessPipe() $this->assertSame("foo\n", $pipe->output()); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testProcessPipeFailed() { $factory = new Factory; @@ -717,7 +717,7 @@ public function testProcessPipeFailed() $this->assertTrue($pipe->failed()); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testProcessSimplePipe() { $factory = new Factory; @@ -733,7 +733,7 @@ public function testProcessSimplePipe() $this->assertSame("foo\n", $pipe->output()); } - #[RequiresOperatingSystem('Linux|DAR')] + #[RequiresOperatingSystem('Linux|Darwin')] public function testProcessSimplePipeFailed() { $factory = new Factory; From 5c31f488b542ad1b3f5708ff3d0a148f3b096569 Mon Sep 17 00:00:00 2001 From: Jarryd Tilbrook Date: Mon, 18 May 2026 22:37:42 +0800 Subject: [PATCH 395/596] Output cloud request ID in logs (#60156) * Output cloud request ID in logs * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Cloud.php | 3 +- .../Foundation/LaravelCloudJsonFormatter.php | 30 ++++++ .../LaravelCloudJsonFormatterTest.php | 101 ++++++++++++++++++ 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Foundation/LaravelCloudJsonFormatter.php create mode 100644 tests/Foundation/LaravelCloudJsonFormatterTest.php diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 1067de3757d2..0712eeb6fc15 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -10,7 +10,6 @@ use Illuminate\Foundation\Cloud\FailedJobProvider; use Illuminate\Foundation\Cloud\QueueConnector; use Illuminate\Queue\Connectors\SqsConnector; -use Monolog\Formatter\JsonFormatter; use Monolog\Handler\SocketHandler; use PDO; @@ -180,7 +179,7 @@ public static function configureCloudLogging(Application $app): void 'driver' => 'monolog', 'level' => $_ENV['LOG_LEVEL'] ?? $_SERVER['LOG_LEVEL'] ?? 'debug', 'handler' => SocketHandler::class, - 'formatter' => JsonFormatter::class, + 'formatter' => LaravelCloudJsonFormatter::class, 'formatter_with' => [ 'includeStacktraces' => true, ], diff --git a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php new file mode 100644 index 000000000000..942714e1eeef --- /dev/null +++ b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php @@ -0,0 +1,30 @@ +bound('request')) { + $requestId = $app->make('request')->header('X-Request-ID'); + + if ($requestId !== null) { + $normalized['cloud_request_id'] = $requestId; + } + } + + return $normalized; + } +} diff --git a/tests/Foundation/LaravelCloudJsonFormatterTest.php b/tests/Foundation/LaravelCloudJsonFormatterTest.php new file mode 100644 index 000000000000..b9ebcc222b7b --- /dev/null +++ b/tests/Foundation/LaravelCloudJsonFormatterTest.php @@ -0,0 +1,101 @@ +headers->set('X-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); + $app->instance('request', $request); + + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($this->createRecord()); + $decoded = json_decode($formatted, true); + + $this->assertEquals('550e8400-e29b-41d4-a716-446655440000', $decoded['cloud_request_id']); + } + + public function test_does_not_add_field_when_no_request_bound() + { + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($this->createRecord()); + $decoded = json_decode($formatted, true); + + $this->assertArrayNotHasKey('cloud_request_id', $decoded); + } + + public function test_does_not_add_field_when_no_header_present() + { + $app = Container::getInstance(); + $request = Request::create('/'); + $app->instance('request', $request); + + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($this->createRecord()); + $decoded = json_decode($formatted, true); + + $this->assertArrayNotHasKey('cloud_request_id', $decoded); + } + + public function test_preserves_existing_log_fields() + { + $app = Container::getInstance(); + $request = Request::create('/'); + $request->headers->set('X-Request-ID', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'); + $app->instance('request', $request); + + $record = new LogRecord( + message: 'Test message', + level: Level::Warning, + channel: 'my-channel', + datetime: new \DateTimeImmutable('2024-01-15 10:30:00'), + extra: ['extra_field' => 'extra_value'], + context: ['context_field' => 'context_value'], + ); + + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($record); + $decoded = json_decode($formatted, true); + + $this->assertEquals('Test message', $decoded['message']); + $this->assertEquals('WARNING', $decoded['level_name']); + $this->assertEquals('my-channel', $decoded['channel']); + $this->assertEquals('extra_value', $decoded['extra']['extra_field']); + $this->assertEquals('context_value', $decoded['context']['context_field']); + $this->assertEquals('6ba7b810-9dad-11d1-80b4-00c04fd430c8', $decoded['cloud_request_id']); + } +} From 864c94be79e948671e9b05e75b7c0562459aa72c Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Mon, 18 May 2026 11:31:43 -0400 Subject: [PATCH 396/596] [13.x] Optionally flush the SQS overflow store on queue:clear (#60138) * Add the config value Signed-off-by: Kevin Ullyott * Flush on clear Signed-off-by: Kevin Ullyott * Adjust tests for purge Signed-off-by: Kevin Ullyott --------- Signed-off-by: Kevin Ullyott --- config/queue.php | 1 + src/Illuminate/Queue/SqsQueue.php | 7 +++ tests/Queue/QueueSqsQueueTest.php | 92 ++++++++++++++++++++++++++++++- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/config/queue.php b/config/queue.php index 737a2464cb7a..771a3362de68 100644 --- a/config/queue.php +++ b/config/queue.php @@ -67,6 +67,7 @@ 'store' => env('SQS_OVERFLOW_STORE'), 'always' => false, 'delete_after_processing' => true, + 'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false), ], ], diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index 193235a75bd9..975016bd1429 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -451,6 +451,13 @@ public function clear($queue) $this->sqs->purgeQueue([ 'QueueUrl' => $this->getQueue($queue), ]); + + if (Arr::get($this->overflowStorage, 'enabled') + && Arr::get($this->overflowStorage, 'flush_on_clear')) { + $this->container->make('cache')->store( + Arr::get($this->overflowStorage, 'store') + )->flush(); + } }); } diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 8917b3a5aac8..300d99b9a397 100755 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -774,8 +774,17 @@ public function testPushRawDoesNotStoreToCacheWhenNotEnabled() $queue->pushRaw($largePayload, $this->queueName); } - public function testClearDoesNotFlushCacheStore() + public function testClearFlushesOverflowStoreWhenFlushOnClearEnabled() { + $store = m::mock(CacheRepository::class); + $store->shouldReceive('flush')->once(); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->once()->with('database')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->once()->with('cache')->andReturn($cache); + $queue = $this->getMockBuilder(SqsQueue::class) ->onlyMethods(['getQueue', 'size']) ->setConstructorArgs([$this->sqs, $this->queueName, $this->prefix, '', false, [ @@ -783,9 +792,88 @@ public function testClearDoesNotFlushCacheStore() 'store' => 'database', 'always' => false, 'delete_after_processing' => true, + 'flush_on_clear' => true, ]]) ->getMock(); - $queue->setContainer(m::mock(Container::class)); + $queue->setContainer($container); + $queue->expects($this->once())->method('getQueue')->willReturn($this->queueUrl); + $queue->expects($this->once())->method('size')->willReturn(5); + + $this->sqs->shouldReceive('purgeQueue')->once(); + + $queue->clear($this->queueName); + } + + public function testClearDoesNotFlushOverflowStoreWhenFlushOnClearDisabled() + { + $container = m::mock(Container::class); + $container->shouldNotReceive('make'); + + $queue = $this->getMockBuilder(SqsQueue::class) + ->onlyMethods(['getQueue', 'size']) + ->setConstructorArgs([$this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => true, + 'store' => 'database', + 'always' => false, + 'delete_after_processing' => true, + 'flush_on_clear' => false, + ]]) + ->getMock(); + $queue->setContainer($container); + $queue->expects($this->once())->method('getQueue')->willReturn($this->queueUrl); + $queue->expects($this->once())->method('size')->willReturn(5); + + $this->sqs->shouldReceive('purgeQueue')->once(); + + $queue->clear($this->queueName); + } + + public function testClearDoesNotFlushOverflowStoreWhenOverflowDisabled() + { + $container = m::mock(Container::class); + $container->shouldNotReceive('make'); + + $queue = $this->getMockBuilder(SqsQueue::class) + ->onlyMethods(['getQueue', 'size']) + ->setConstructorArgs([$this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => false, + 'store' => 'database', + 'always' => false, + 'delete_after_processing' => true, + 'flush_on_clear' => true, + ]]) + ->getMock(); + $queue->setContainer($container); + $queue->expects($this->once())->method('getQueue')->willReturn($this->queueUrl); + $queue->expects($this->once())->method('size')->willReturn(5); + + $this->sqs->shouldReceive('purgeQueue')->once(); + + $queue->clear($this->queueName); + } + + public function testClearForwardsConfiguredStoreNameToFactory() + { + $store = m::mock(CacheRepository::class); + $store->shouldReceive('flush')->once(); + + $cache = m::mock(CacheFactory::class); + $cache->shouldReceive('store')->once()->with('redis')->andReturn($store); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->once()->with('cache')->andReturn($cache); + + $queue = $this->getMockBuilder(SqsQueue::class) + ->onlyMethods(['getQueue', 'size']) + ->setConstructorArgs([$this->sqs, $this->queueName, $this->prefix, '', false, [ + 'enabled' => true, + 'store' => 'redis', + 'always' => false, + 'delete_after_processing' => true, + 'flush_on_clear' => true, + ]]) + ->getMock(); + $queue->setContainer($container); $queue->expects($this->once())->method('getQueue')->willReturn($this->queueUrl); $queue->expects($this->once())->method('size')->willReturn(5); From 75b701f956bfa2b948cfce92f6c2c5c6ef7ffe79 Mon Sep 17 00:00:00 2001 From: Jarryd Tilbrook Date: Tue, 19 May 2026 21:16:25 +0800 Subject: [PATCH 397/596] Remove stale PHPStan ignore comments from type tests (#60167) --- types/Support/Helpers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/Support/Helpers.php b/types/Support/Helpers.php index e9be774efbe5..999e55969d03 100644 --- a/types/Support/Helpers.php +++ b/types/Support/Helpers.php @@ -44,7 +44,7 @@ function testThrowIf(float|int $foo, ?DateTime $bar = null): void { rescue(fn () => assertType('never', throw_if(true, Exception::class))); - assertType('false', throw_if(false, Exception::class)); // @phpstan-ignore deadCode.unreachable + assertType('false', throw_if(false, Exception::class)); assertType('false', throw_if(empty($foo))); throw_if(is_float($foo)); assertType('int', $foo); @@ -63,7 +63,7 @@ function testThrowUnless(float|int $foo, ?DateTime $bar = null): void { assertType('true', throw_unless(true, Exception::class)); rescue(fn () => assertType('never', throw_unless(false, Exception::class))); - assertType('true', throw_unless(empty($foo))); // @phpstan-ignore deadCode.unreachable + assertType('true', throw_unless(empty($foo))); throw_unless(is_int($foo)); assertType('int', $foo); throw_unless($foo == false); From b8eea9c48f644bee70f2cb1f675a1668b0321fea Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Tue, 19 May 2026 15:18:16 +0200 Subject: [PATCH 398/596] Add JSON output option to ListFailedCommand and corresponding tests (#60168) --- .../Queue/Console/ListFailedCommand.php | 28 ++++++- tests/Queue/ListFailedCommandTest.php | 75 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 tests/Queue/ListFailedCommandTest.php diff --git a/src/Illuminate/Queue/Console/ListFailedCommand.php b/src/Illuminate/Queue/Console/ListFailedCommand.php index 8aaa2e60d0e4..992749633a1d 100644 --- a/src/Illuminate/Queue/Console/ListFailedCommand.php +++ b/src/Illuminate/Queue/Console/ListFailedCommand.php @@ -15,7 +15,8 @@ class ListFailedCommand extends Command * * @var string */ - protected $name = 'queue:failed'; + protected $signature = 'queue:failed + {--json : Output the failed jobs as JSON}'; /** * The console command description. @@ -38,7 +39,13 @@ class ListFailedCommand extends Command */ public function handle() { - if (count($jobs = $this->getFailedJobs()) === 0) { + $jobs = $this->getFailedJobs(); + + if ($this->option('json')) { + return $this->displayFailedJobsAsJson($jobs); + } + + if (count($jobs) === 0) { return $this->components->info('No failed jobs found.'); } @@ -122,4 +129,21 @@ protected function displayFailedJobs(array $jobs) ), ); } + + /** + * Display the failed jobs as JSON. + * + * @param array $jobs + * @return void + */ + protected function displayFailedJobsAsJson(array $jobs) + { + $this->output->writeln((new Collection($jobs))->values()->map(fn ($job) => [ + 'id' => $job[0], + 'connection' => $job[1], + 'queue' => $job[2], + 'class' => $job[3], + 'failed_at' => $job[4], + ])->toJson()); + } } diff --git a/tests/Queue/ListFailedCommandTest.php b/tests/Queue/ListFailedCommandTest.php new file mode 100644 index 000000000000..e518fc18fee1 --- /dev/null +++ b/tests/Queue/ListFailedCommandTest.php @@ -0,0 +1,75 @@ +runCommandWithFailedJobs([], ['--json' => true]); + + $this->assertJson($output); + $this->assertJsonStringEqualsJsonString('[]', $output); + } + + public function testItDisplaysFailedJobsAsJson() + { + $output = $this->runCommandWithFailedJobs([ + (object) [ + 'id' => 'failed-job-id', + 'connection' => 'redis', + 'queue' => 'default', + 'payload' => json_encode([ + 'job' => 'Illuminate\Queue\CallQueuedHandler@call', + 'data' => [ + 'command' => 'O:32:"Illuminate\Tests\Queue\ExampleJob":0:{}', + ], + ]), + 'exception' => 'Exception stack trace', + 'failed_at' => '2026-05-18 12:00:00', + ], + ], ['--json' => true]); + + $this->assertJson($output); + $this->assertJsonStringEqualsJsonString(json_encode([ + [ + 'id' => 'failed-job-id', + 'connection' => 'redis', + 'queue' => 'default', + 'class' => 'Illuminate\Tests\Queue\ExampleJob', + 'failed_at' => '2026-05-18 12:00:00', + ], + ]), $output); + } + + protected function runCommandWithFailedJobs(array $failedJobs, array $arguments = []): string + { + $container = new Application; + $container->instance('queue.failer', $failer = m::mock()); + + $failer->shouldReceive('all')->once()->andReturn($failedJobs); + + $command = new ListFailedCommand; + $command->setLaravel($container); + + $output = new BufferedOutput; + + $command->run(new ArrayInput($arguments), $output); + + return $output->fetch(); + } +} From 979601b173d3b17dd0b6029780b373906f7eeb53 Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Tue, 19 May 2026 15:20:43 +0200 Subject: [PATCH 399/596] [13.x] Add schema foreign key existence helper (#60169) * Add schema foreign key existence helper * Update Builder.php * Update Schema.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Schema/Builder.php | 18 ++++++++ src/Illuminate/Support/Facades/Schema.php | 3 +- .../DatabaseSchemaBuilderIntegrationTest.php | 45 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 2a15b13417b9..2fae172a9405 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -465,6 +465,24 @@ public function hasIndex($table, $index, $type = null) return false; } + /** + * Determine if the table has a given foreign key. + * + * @param string $table + * @param array|string $foreignKey + * @return bool + */ + public function hasForeignKey($table, $foreignKey) + { + foreach ($this->getForeignKeys($table) as $value) { + if ($value['name'] === $foreignKey || $value['columns'] === $foreignKey) { + return true; + } + } + + return false; + } + /** * Get the foreign keys for a given table. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 5c617687bb3a..436542d97b1c 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -28,7 +28,8 @@ * @method static array getColumns(string $table) * @method static array getIndexes(string $table) * @method static array getIndexListing(string $table) - * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) + * @method static bool hasIndex(string $table, array|string $index, string|null $type = null) + * @method static bool hasForeignKey(string $table, array|string $foreignKey) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) diff --git a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php index d09b6ca56ab8..28d17cdd43f9 100644 --- a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php @@ -5,6 +5,7 @@ use Illuminate\Container\Container; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; use Illuminate\Support\Facades\Facade; use PHPUnit\Framework\TestCase; @@ -87,6 +88,37 @@ public function testHasColumnAndIndexWithPrefixIndexEnabled() $this->assertTrue($this->schemaBuilder()->hasIndex('table1', 'example_table1_name_index')); } + public function testHasForeignKeyWithSQLiteForeignKeys() + { + $this->schemaBuilder()->create('users', function (Blueprint $table) { + $table->id(); + }); + + $this->schemaBuilder()->create('posts', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id'); + $table->foreign('user_id', 'posts_user_id_foreign')->references('id')->on('users'); + }); + + $this->assertTrue($this->schemaBuilder()->hasForeignKey('posts', ['user_id'])); + $this->assertFalse($this->schemaBuilder()->hasForeignKey('posts', ['missing_id'])); + } + + public function testHasForeignKeyCanMatchForeignKeyNamesAndColumns() + { + $builder = new ForeignKeySchemaBuilderStub([ + [ + 'name' => 'posts_user_id_foreign', + 'columns' => ['user_id'], + ], + ]); + + $this->assertTrue($builder->hasForeignKey('posts', 'posts_user_id_foreign')); + $this->assertTrue($builder->hasForeignKey('posts', ['user_id'])); + $this->assertFalse($builder->hasForeignKey('posts', 'posts_missing_foreign')); + $this->assertFalse($builder->hasForeignKey('posts', ['missing_id'])); + } + public function testDropColumnWithTablePrefix() { $this->db::connection()->setTablePrefix('test_'); @@ -115,3 +147,16 @@ private function schemaBuilder() return $this->db::connection()->getSchemaBuilder(); } } + +class ForeignKeySchemaBuilderStub extends Builder +{ + public function __construct(protected array $foreignKeys) + { + // + } + + public function getForeignKeys($table) + { + return $this->foreignKeys; + } +} From fa071b5dc6b7b8653624a6f99768f5e56ddf7b49 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 13:21:19 +0000 Subject: [PATCH 400/596] Update facade docblocks --- src/Illuminate/Support/Facades/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 436542d97b1c..c143296e8361 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -28,7 +28,7 @@ * @method static array getColumns(string $table) * @method static array getIndexes(string $table) * @method static array getIndexListing(string $table) - * @method static bool hasIndex(string $table, array|string $index, string|null $type = null) + * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static bool hasForeignKey(string $table, array|string $foreignKey) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) From ca48fcecd81544111fd6723f1200ffbdf7054e7c Mon Sep 17 00:00:00 2001 From: Jarryd Tilbrook Date: Tue, 19 May 2026 21:34:36 +0800 Subject: [PATCH 401/596] Output cloud request ID in logs for Cloud applications (#60166) This is a backport #60156 to 12.x --- src/Illuminate/Foundation/Cloud.php | 3 +- .../Foundation/LaravelCloudJsonFormatter.php | 30 ++++++ .../LaravelCloudJsonFormatterTest.php | 101 ++++++++++++++++++ 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Foundation/LaravelCloudJsonFormatter.php create mode 100644 tests/Foundation/LaravelCloudJsonFormatterTest.php diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 5cae0e761622..029f55c4cd94 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -10,7 +10,6 @@ use Illuminate\Foundation\Cloud\FailedJobProvider; use Illuminate\Foundation\Cloud\QueueConnector; use Illuminate\Queue\Connectors\SqsConnector; -use Monolog\Formatter\JsonFormatter; use Monolog\Handler\SocketHandler; use PDO; @@ -172,7 +171,7 @@ public static function configureCloudLogging(Application $app): void 'driver' => 'monolog', 'level' => $_ENV['LOG_LEVEL'] ?? $_SERVER['LOG_LEVEL'] ?? 'debug', 'handler' => SocketHandler::class, - 'formatter' => JsonFormatter::class, + 'formatter' => LaravelCloudJsonFormatter::class, 'formatter_with' => [ 'includeStacktraces' => true, ], diff --git a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php new file mode 100644 index 000000000000..942714e1eeef --- /dev/null +++ b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php @@ -0,0 +1,30 @@ +bound('request')) { + $requestId = $app->make('request')->header('X-Request-ID'); + + if ($requestId !== null) { + $normalized['cloud_request_id'] = $requestId; + } + } + + return $normalized; + } +} diff --git a/tests/Foundation/LaravelCloudJsonFormatterTest.php b/tests/Foundation/LaravelCloudJsonFormatterTest.php new file mode 100644 index 000000000000..b9ebcc222b7b --- /dev/null +++ b/tests/Foundation/LaravelCloudJsonFormatterTest.php @@ -0,0 +1,101 @@ +headers->set('X-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); + $app->instance('request', $request); + + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($this->createRecord()); + $decoded = json_decode($formatted, true); + + $this->assertEquals('550e8400-e29b-41d4-a716-446655440000', $decoded['cloud_request_id']); + } + + public function test_does_not_add_field_when_no_request_bound() + { + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($this->createRecord()); + $decoded = json_decode($formatted, true); + + $this->assertArrayNotHasKey('cloud_request_id', $decoded); + } + + public function test_does_not_add_field_when_no_header_present() + { + $app = Container::getInstance(); + $request = Request::create('/'); + $app->instance('request', $request); + + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($this->createRecord()); + $decoded = json_decode($formatted, true); + + $this->assertArrayNotHasKey('cloud_request_id', $decoded); + } + + public function test_preserves_existing_log_fields() + { + $app = Container::getInstance(); + $request = Request::create('/'); + $request->headers->set('X-Request-ID', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'); + $app->instance('request', $request); + + $record = new LogRecord( + message: 'Test message', + level: Level::Warning, + channel: 'my-channel', + datetime: new \DateTimeImmutable('2024-01-15 10:30:00'), + extra: ['extra_field' => 'extra_value'], + context: ['context_field' => 'context_value'], + ); + + $formatter = new LaravelCloudJsonFormatter; + $formatted = $formatter->format($record); + $decoded = json_decode($formatted, true); + + $this->assertEquals('Test message', $decoded['message']); + $this->assertEquals('WARNING', $decoded['level_name']); + $this->assertEquals('my-channel', $decoded['channel']); + $this->assertEquals('extra_value', $decoded['extra']['extra_field']); + $this->assertEquals('context_value', $decoded['context']['context_field']); + $this->assertEquals('6ba7b810-9dad-11d1-80b4-00c04fd430c8', $decoded['cloud_request_id']); + } +} From 29e5b70109f4e2eb61de719f4f2a664acea65774 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 19 May 2026 08:54:05 -0500 Subject: [PATCH 402/596] Stop when empty for (#60176) * initial pass * move method --- src/Illuminate/Queue/Console/WorkCommand.php | 2 + src/Illuminate/Queue/Worker.php | 35 +++++++++--- src/Illuminate/Queue/WorkerOptions.php | 10 ++++ src/Illuminate/Queue/WorkerStopReason.php | 1 + tests/Queue/QueueWorkerTest.php | 60 ++++++++++++++++++++ 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index 648984c50c15..8155e6b6ceee 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -37,6 +37,7 @@ class WorkCommand extends Command {--daemon : Run the worker in daemon mode (Deprecated)} {--once : Only process the next job on the queue} {--stop-when-empty : Stop when the queue is empty} + {--stop-when-empty-for=0 : Stop when no jobs have been processed for the given number of seconds} {--delay=0 : The number of seconds to delay failed jobs (Deprecated)} {--backoff=0 : The number of seconds to wait before retrying a job that encountered an uncaught exception} {--max-jobs=0 : The number of jobs to process before stopping} @@ -169,6 +170,7 @@ protected function gatherWorkerOptions() $this->option('max-jobs'), $this->option('max-time'), $this->option('rest'), + $this->option('stop-when-empty-for'), ); } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 7ecb843e887a..d92f2b4ee885 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -192,7 +192,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $lastRestart = $this->getTimestampOfLastQueueRestart(); - [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + [$startTime, $jobsProcessed] = [$this->currentTime(), 0]; + + $lastJobProcessedAt = $startTime; $this->raiseWorkerStartingEvent($connectionName, $queue, $options); @@ -201,7 +203,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) // if it is we will just pause this worker for a given amount of time and // make sure we do not need to kill this worker process off completely. if (! $this->daemonShouldRun($options, $connectionName, $queue)) { - [$status, $reason] = $this->pauseWorker($options, $lastRestart); + [$status, $reason] = $this->pauseWorker( + $options, $lastRestart, $startTime, $jobsProcessed, $lastJobProcessedAt + ); if (! is_null($status)) { return $this->stop($status, $options, $reason); @@ -233,6 +237,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->runJob($job, $connectionName, $options); + $lastJobProcessedAt = $this->currentTime(); + if ($options->rest > 0) { $this->sleep($options->rest); } @@ -250,7 +256,7 @@ public function daemon($connectionName, $queue, WorkerOptions $options) // the queue should restart based on other indications. If so, we'll stop // this worker and let whatever is "monitoring" it restart the process. [$status, $reason] = $this->stopIfNecessary( - $options, $lastRestart, $startTime, $jobsProcessed, $job + $options, $lastRestart, $startTime, $jobsProcessed, $job, $lastJobProcessedAt ); if (! is_null($status)) { @@ -340,13 +346,16 @@ protected function daemonShouldRun(WorkerOptions $options, $connectionName, $que * * @param \Illuminate\Queue\WorkerOptions $options * @param int $lastRestart + * @param int|float $startTime + * @param int $jobsProcessed + * @param int|float|null $lastJobProcessedAt * @return array|null */ - protected function pauseWorker(WorkerOptions $options, $lastRestart) + protected function pauseWorker(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $lastJobProcessedAt = null) { $this->sleep($options->sleep > 0 ? $options->sleep : 1); - return $this->stopIfNecessary($options, $lastRestart); + return $this->stopIfNecessary($options, $lastRestart, $startTime, $jobsProcessed, null, $lastJobProcessedAt); } /** @@ -357,9 +366,10 @@ protected function pauseWorker(WorkerOptions $options, $lastRestart) * @param int $startTime * @param int $jobsProcessed * @param mixed $job + * @param int|float|null $lastJobProcessedAt * @return array|null */ - protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $job = null) + protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $job = null, $lastJobProcessedAt = null) { return match (true) { $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], @@ -367,7 +377,8 @@ protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startT $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], $options->stopWhenEmpty && is_null($job) => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmpty], - $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], + $options->stopWhenEmptyFor && is_null($job) && $this->currentTime() - ($lastJobProcessedAt ?? $startTime) >= $options->stopWhenEmptyFor => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmptyFor], + $options->maxTime && $this->currentTime() - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], $options->maxJobs && $jobsProcessed >= $options->maxJobs => [static::EXIT_SUCCESS, WorkerStopReason::MaxJobsExceeded], default => null }; @@ -1045,4 +1056,14 @@ public function setManager(QueueManager $manager) { $this->manager = $manager; } + + /** + * Get the current high-resolution timestamp. + * + * @return float + */ + protected function currentTime() + { + return hrtime(true) / 1e9; + } } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 036168b39e70..a9c2bd6ca70e 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -67,6 +67,13 @@ class WorkerOptions */ public $stopWhenEmpty; + /** + * The number of seconds to wait for a job before stopping. + * + * @var int + */ + public $stopWhenEmptyFor; + /** * The maximum number of jobs to run. * @@ -95,6 +102,7 @@ class WorkerOptions * @param int $maxJobs * @param int $maxTime * @param int $rest + * @param int $stopWhenEmptyFor */ public function __construct( $name = 'default', @@ -108,6 +116,7 @@ public function __construct( $maxJobs = 0, $maxTime = 0, $rest = 0, + $stopWhenEmptyFor = 0, ) { $this->name = $name; $this->backoff = $backoff; @@ -118,6 +127,7 @@ public function __construct( $this->timeout = $timeout; $this->maxTries = $maxTries; $this->stopWhenEmpty = $stopWhenEmpty; + $this->stopWhenEmptyFor = $stopWhenEmptyFor; $this->maxJobs = $maxJobs; $this->maxTime = $maxTime; } diff --git a/src/Illuminate/Queue/WorkerStopReason.php b/src/Illuminate/Queue/WorkerStopReason.php index 8591e94743bc..52d74112923a 100644 --- a/src/Illuminate/Queue/WorkerStopReason.php +++ b/src/Illuminate/Queue/WorkerStopReason.php @@ -10,6 +10,7 @@ enum WorkerStopReason: string case MaxMemoryExceeded = 'memory'; case MaxTimeExceeded = 'max_time'; case QueueEmpty = 'empty'; + case QueueEmptyFor = 'empty_for'; case ReceivedRestartSignal = 'restart_signal'; case TimedOut = 'timed_out'; } diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index 30d0b9cfa832..e41bedd908cf 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -99,6 +99,56 @@ public function testWorkerCanWorkUntilQueueIsEmpty() $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessed::class))->twice(); } + public function testWorkerStopsWhenQueueIsEmptyForConfiguredSeconds() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmptyFor = 5; + + $worker = $this->getWorker('default', ['queue' => []]); + $worker->currentTime = 0; + + $status = $worker->daemon('default', 'queue', $workerOptions); + + $this->assertSame(0, $status); + + $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerIdle::class))->twice(); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::QueueEmptyFor; + })); + } + + public function testWorkerResetsQueueEmptyTimerAfterProcessingJob() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmptyFor = 5; + + $worker = $this->getWorker('default', ['queue' => [ + $job = new WorkerFakeJob(function () use (&$worker) { + $worker->currentTime = 10; + }), + ]]); + $worker->currentTime = 0; + + $status = $worker->daemon('default', 'queue', $workerOptions); + + $this->assertTrue($job->fired); + $this->assertSame(0, $status); + $this->assertSame(16, $worker->currentTime); + + $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerIdle::class))->twice(); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::QueueEmptyFor; + })); + } + public function testWorkerStopsWhenMemoryExceeded() { $workerOptions = new WorkerOptions; @@ -593,10 +643,20 @@ class InsomniacWorker extends Worker { public $sleptFor; public $stopOnMemoryExceeded = false; + public $currentTime; public function sleep($seconds) { $this->sleptFor = $seconds; + + if (! is_null($this->currentTime)) { + $this->currentTime += $seconds; + } + } + + protected function currentTime() + { + return $this->currentTime ?? parent::currentTime(); } public function notifyJobOfSignal(int $signal): void From f6a1295441655b9edc5913115aa39c0b2aee2e1b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 19 May 2026 08:58:15 -0500 Subject: [PATCH 403/596] stop-when-empty-for --- src/Illuminate/Queue/Console/WorkCommand.php | 2 + src/Illuminate/Queue/Worker.php | 35 +++++++++--- src/Illuminate/Queue/WorkerOptions.php | 10 ++++ src/Illuminate/Queue/WorkerStopReason.php | 1 + tests/Queue/QueueWorkerTest.php | 56 ++++++++++++++++++++ 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index 04ecf2f9d6ca..a8d9013f53e6 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -37,6 +37,7 @@ class WorkCommand extends Command {--daemon : Run the worker in daemon mode (Deprecated)} {--once : Only process the next job on the queue} {--stop-when-empty : Stop when the queue is empty} + {--stop-when-empty-for=0 : Stop when no jobs have been processed for the given number of seconds} {--delay=0 : The number of seconds to delay failed jobs (Deprecated)} {--backoff=0 : The number of seconds to wait before retrying a job that encountered an uncaught exception} {--max-jobs=0 : The number of jobs to process before stopping} @@ -169,6 +170,7 @@ protected function gatherWorkerOptions() $this->option('max-jobs'), $this->option('max-time'), $this->option('rest'), + $this->option('stop-when-empty-for'), ); } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 946b311258ef..282e9c9669e6 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -166,7 +166,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $lastRestart = $this->getTimestampOfLastQueueRestart(); - [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + [$startTime, $jobsProcessed] = [$this->currentTime(), 0]; + + $lastJobProcessedAt = $startTime; $this->raiseWorkerStartingEvent($connectionName, $queue, $options); @@ -175,7 +177,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) // if it is we will just pause this worker for a given amount of time and // make sure we do not need to kill this worker process off completely. if (! $this->daemonShouldRun($options, $connectionName, $queue)) { - [$status, $reason] = $this->pauseWorker($options, $lastRestart); + [$status, $reason] = $this->pauseWorker( + $options, $lastRestart, $startTime, $jobsProcessed, $lastJobProcessedAt + ); if (! is_null($status)) { return $this->stop($status, $options, $reason); @@ -207,6 +211,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->runJob($job, $connectionName, $options); + $lastJobProcessedAt = $this->currentTime(); + if ($options->rest > 0) { $this->sleep($options->rest); } @@ -222,7 +228,7 @@ public function daemon($connectionName, $queue, WorkerOptions $options) // the queue should restart based on other indications. If so, we'll stop // this worker and let whatever is "monitoring" it restart the process. [$status, $reason] = $this->stopIfNecessary( - $options, $lastRestart, $startTime, $jobsProcessed, $job + $options, $lastRestart, $startTime, $jobsProcessed, $job, $lastJobProcessedAt ); if (! is_null($status)) { @@ -312,13 +318,16 @@ protected function daemonShouldRun(WorkerOptions $options, $connectionName, $que * * @param \Illuminate\Queue\WorkerOptions $options * @param int $lastRestart + * @param int|float $startTime + * @param int $jobsProcessed + * @param int|float|null $lastJobProcessedAt * @return array|null */ - protected function pauseWorker(WorkerOptions $options, $lastRestart) + protected function pauseWorker(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $lastJobProcessedAt = null) { $this->sleep($options->sleep > 0 ? $options->sleep : 1); - return $this->stopIfNecessary($options, $lastRestart); + return $this->stopIfNecessary($options, $lastRestart, $startTime, $jobsProcessed, null, $lastJobProcessedAt); } /** @@ -329,9 +338,10 @@ protected function pauseWorker(WorkerOptions $options, $lastRestart) * @param int $startTime * @param int $jobsProcessed * @param mixed $job + * @param int|float|null $lastJobProcessedAt * @return array|null */ - protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $job = null) + protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startTime = 0, $jobsProcessed = 0, $job = null, $lastJobProcessedAt = null) { return match (true) { $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], @@ -339,7 +349,8 @@ protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $startT $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], $options->stopWhenEmpty && is_null($job) => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmpty], - $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], + $options->stopWhenEmptyFor && is_null($job) && $this->currentTime() - ($lastJobProcessedAt ?? $startTime) >= $options->stopWhenEmptyFor => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmptyFor], + $options->maxTime && $this->currentTime() - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], $options->maxJobs && $jobsProcessed >= $options->maxJobs => [static::EXIT_SUCCESS, WorkerStopReason::MaxJobsExceeded], default => null }; @@ -960,4 +971,14 @@ public function setManager(QueueManager $manager) { $this->manager = $manager; } + + /** + * Get the current high-resolution timestamp. + * + * @return float + */ + protected function currentTime() + { + return hrtime(true) / 1e9; + } } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 036168b39e70..a9c2bd6ca70e 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -67,6 +67,13 @@ class WorkerOptions */ public $stopWhenEmpty; + /** + * The number of seconds to wait for a job before stopping. + * + * @var int + */ + public $stopWhenEmptyFor; + /** * The maximum number of jobs to run. * @@ -95,6 +102,7 @@ class WorkerOptions * @param int $maxJobs * @param int $maxTime * @param int $rest + * @param int $stopWhenEmptyFor */ public function __construct( $name = 'default', @@ -108,6 +116,7 @@ public function __construct( $maxJobs = 0, $maxTime = 0, $rest = 0, + $stopWhenEmptyFor = 0, ) { $this->name = $name; $this->backoff = $backoff; @@ -118,6 +127,7 @@ public function __construct( $this->timeout = $timeout; $this->maxTries = $maxTries; $this->stopWhenEmpty = $stopWhenEmpty; + $this->stopWhenEmptyFor = $stopWhenEmptyFor; $this->maxJobs = $maxJobs; $this->maxTime = $maxTime; } diff --git a/src/Illuminate/Queue/WorkerStopReason.php b/src/Illuminate/Queue/WorkerStopReason.php index 8591e94743bc..52d74112923a 100644 --- a/src/Illuminate/Queue/WorkerStopReason.php +++ b/src/Illuminate/Queue/WorkerStopReason.php @@ -10,6 +10,7 @@ enum WorkerStopReason: string case MaxMemoryExceeded = 'memory'; case MaxTimeExceeded = 'max_time'; case QueueEmpty = 'empty'; + case QueueEmptyFor = 'empty_for'; case ReceivedRestartSignal = 'restart_signal'; case TimedOut = 'timed_out'; } diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index 9d500927847f..05cd6fffa700 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -96,6 +96,52 @@ public function testWorkerCanWorkUntilQueueIsEmpty() $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessed::class))->twice(); } + public function testWorkerStopsWhenQueueIsEmptyForConfiguredSeconds() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmptyFor = 5; + + $worker = $this->getWorker('default', ['queue' => []]); + $worker->currentTime = 0; + + $status = $worker->daemon('default', 'queue', $workerOptions); + + $this->assertSame(0, $status); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::QueueEmptyFor; + }))->once(); + } + + public function testWorkerResetsQueueEmptyTimerAfterProcessingJob() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmptyFor = 5; + + $worker = $this->getWorker('default', ['queue' => [ + $job = new WorkerFakeJob(function () use (&$worker) { + $worker->currentTime = 10; + }), + ]]); + $worker->currentTime = 0; + + $status = $worker->daemon('default', 'queue', $workerOptions); + + $this->assertTrue($job->fired); + $this->assertSame(0, $status); + $this->assertSame(16, $worker->currentTime); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::QueueEmptyFor; + }))->once(); + } + public function testWorkerStopsWhenMemoryExceeded() { $workerOptions = new WorkerOptions; @@ -527,10 +573,20 @@ class InsomniacWorker extends Worker { public $sleptFor; public $stopOnMemoryExceeded = false; + public $currentTime; public function sleep($seconds) { $this->sleptFor = $seconds; + + if (! is_null($this->currentTime)) { + $this->currentTime += $seconds; + } + } + + protected function currentTime() + { + return $this->currentTime ?? parent::currentTime(); } public function stop($status = 0, $options = null, $reason = null) From f05ef246c22eac49c7c7e9b2815449873ccd8a22 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 19 May 2026 16:10:53 +0200 Subject: [PATCH 404/596] Add initial value type to return of reduce and reduceWithKeys (#60178) --- .../Collections/Traits/EnumeratesValues.php | 4 +-- types/Support/Collection.php | 13 +++---- types/Support/LazyCollection.php | 36 +++++++++++++++++-- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index fd20a2cc90ad..8315ff89165a 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -844,7 +844,7 @@ public function pipeThrough($callbacks) * * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback * @param TReduceInitial $initial - * @return TReduceReturnType + * @return TReduceInitial|TReduceReturnType */ public function reduce(callable $callback, $initial = null) { @@ -892,7 +892,7 @@ class_basename(static::class), gettype($result) * * @param callable(TReduceWithKeysInitial|TReduceWithKeysReturnType, TValue, TKey): TReduceWithKeysReturnType $callback * @param TReduceWithKeysInitial $initial - * @return TReduceWithKeysReturnType + * @return TReduceWithKeysInitial|TReduceWithKeysReturnType */ public function reduceWithKeys(callable $callback, $initial = null) { diff --git a/types/Support/Collection.php b/types/Support/Collection.php index 86f8a7e20e0e..34eb6e2cb9db 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -706,21 +706,21 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection::make([1])->random(2)); assertType('string', $collection::make(['string'])->random()); -assertType('1', $collection +assertType('1|null', $collection ->reduce(function ($null, $user) { assertType('User', $user); assertType('1|null', $null); return 1; })); -assertType('1', $collection +assertType('0|1', $collection ->reduce(function ($int, $user) { assertType('User', $user); assertType('0|1', $int); return 1; }, 0)); -assertType('1', $collection +assertType('0|1', $collection ->reduce(function ($int, $user, $key) { assertType('User', $user); assertType('0|1', $int); @@ -729,21 +729,21 @@ function ($collection, $count) { return 1; }, 0)); -assertType('1', $collection +assertType('1|null', $collection ->reduceWithKeys(function ($null, $user) { assertType('User', $user); assertType('1|null', $null); return 1; })); -assertType('1', $collection +assertType('0|1', $collection ->reduceWithKeys(function ($int, $user) { assertType('User', $user); assertType('0|1', $int); return 1; }, 0)); -assertType('1', $collection +assertType('0|1', $collection ->reduceWithKeys(function ($int, $user, $key) { assertType('User', $user); assertType('0|1', $int); @@ -751,6 +751,7 @@ function ($collection, $count) { return 1; }, 0)); +assertType("'bar'|'foo'", $collection::make([])->reduce(static fn (): string => 'foo', 'bar')); assertType('Illuminate\Support\Collection', $collection::make([1])->replace([1])); assertType('Illuminate\Support\Collection', $collection->replace([new User])); diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index d5b382a95edb..d04be9410a8e 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -597,20 +597,52 @@ public function toArray(): array assertType('Illuminate\Support\LazyCollection|int', $collection::make([1])->random(2)); assertType('Illuminate\Support\LazyCollection|string', $collection::make(['string'])->random()); -assertType('1', $collection +assertType('1|null', $collection ->reduce(function ($null, $user) { assertType('User', $user); assertType('1|null', $null); return 1; })); -assertType('1', $collection +assertType('0|1', $collection ->reduce(function ($int, $user) { assertType('User', $user); assertType('0|1', $int); return 1; }, 0)); +assertType('0|1', $collection + ->reduce(function ($int, $user, $key) { + assertType('User', $user); + assertType('0|1', $int); + assertType('int', $key); + + return 1; + }, 0)); + +assertType('1|null', $collection + ->reduceWithKeys(function ($null, $user) { + assertType('User', $user); + assertType('1|null', $null); + + return 1; + })); +assertType('0|1', $collection + ->reduceWithKeys(function ($int, $user) { + assertType('User', $user); + assertType('0|1', $int); + + return 1; + }, 0)); +assertType('0|1', $collection + ->reduceWithKeys(function ($int, $user, $key) { + assertType('User', $user); + assertType('0|1', $int); + assertType('int', $key); + + return 1; + }, 0)); +assertType("'bar'|'foo'", $collection::make([])->reduce(static fn (): string => 'foo', 'bar')); assertType('Illuminate\Support\LazyCollection', $collection::make([1])->replace([1])); assertType('Illuminate\Support\LazyCollection', $collection->replace([new User])); From ea8c5c509a5f716b98bedd546fb9cb834ec3dc2d Mon Sep 17 00:00:00 2001 From: RP SOHAG <66528080+rpsohag@users.noreply.github.com> Date: Tue, 19 May 2026 22:59:44 +0600 Subject: [PATCH 405/596] Fix incorrect type hint in EncodedHtmlString::convert() docblock (#60186) --- src/Illuminate/Support/EncodedHtmlString.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/EncodedHtmlString.php b/src/Illuminate/Support/EncodedHtmlString.php index 36b29cc33ecf..57237fd8536f 100644 --- a/src/Illuminate/Support/EncodedHtmlString.php +++ b/src/Illuminate/Support/EncodedHtmlString.php @@ -39,7 +39,7 @@ public function __construct($html = '', protected bool $doubleEncode = true) * @internal * * @param string|null $value - * @param int $withQuote + * @param bool $withQuote * @param bool $doubleEncode * @return string */ From e36708519b893d78c2fede3d26ccac23f400368d Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 18:11:38 +0000 Subject: [PATCH 406/596] Update version to v13.10.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 9ba83aec2310..cc2295e68676 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 = '13.9.0'; + const VERSION = '13.10.0'; /** * The base path for the Laravel installation. From 0717d034e249142b07664a34d8e53dc70b5f9780 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Tue, 19 May 2026 19:12:16 +0100 Subject: [PATCH 407/596] [13.x] Dedicated Cloud Queue (#60180) --- src/Illuminate/Foundation/Cloud.php | 29 ++-- .../Foundation/Cloud/FailedJobProvider.php | 2 +- src/Illuminate/Foundation/Cloud/Queue.php | 31 ++--- .../Foundation/Cloud/QueueConnector.php | 2 +- tests/Foundation/Cloud/QueueTest.php | 131 +++++++----------- 5 files changed, 75 insertions(+), 120 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 0712eeb6fc15..bdb7d0dbc8ee 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -133,15 +133,22 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): */ public static function configureManagedQueues(Application $app): void { - if (! Cloud::managedQueuesAreActive()) { + if (! isset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'])) { return; } - $app['config']->set('queue.connections.sqs.credentials', 'ecs'); + $config = json_decode($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'], associative: true, flags: JSON_THROW_ON_ERROR); - if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { - $app['config']->set('queue.connections.sqs.region', $_SERVER['LARAVEL_CLOUD_REGION']); - } + $config['connection']['after_commit'] ??= env('CLOUD_QUEUE_AFTER_COMMIT', false); + + $config['connection']['overflow'] ??= [ + 'enabled' => env('CLOUD_QUEUE_OVERFLOW_ENABLED', false), + 'store' => env('CLOUD_QUEUE_OVERFLOW_STORE'), + 'always' => env('CLOUD_QUEUE_OVERFLOW_ALWAYS', false), + 'delete_after_processing' => env('CLOUD_QUEUE_OVERFLOW_DELETE_AFTER_PROCESSING', true), + ]; + + $app['config']->set('queue.connections.cloud', $config); } /** @@ -149,14 +156,14 @@ public static function configureManagedQueues(Application $app): void */ public static function bootManagedQueues(Application $app): void { - if (! Cloud::managedQueuesAreActive()) { + if (($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? '0') !== '1') { return; } $app->singleton(Events::class, fn () => new Events(Cloud::socket())); $app->bind(QueueConnector::class, fn ($app) => new QueueConnector(new SqsConnector, $app)); - $app['queue']->addConnector('sqs', $app->factory(QueueConnector::class)); + $app['queue']->addConnector('cloud', $app->factory(QueueConnector::class)); $failer = $app['queue.failer']; unset($app['queue.failer']); @@ -199,12 +206,4 @@ protected static function socket(): string $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? 'unix:///tmp/cloud-init.sock'; } - - /** - * Determine if managed queues are active. - */ - protected static function managedQueuesAreActive(): bool - { - return ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? null) === '1'; - } } diff --git a/src/Illuminate/Foundation/Cloud/FailedJobProvider.php b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php index 9c0dd8dad795..aa4133da95ea 100644 --- a/src/Illuminate/Foundation/Cloud/FailedJobProvider.php +++ b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php @@ -51,7 +51,7 @@ public function __construct( */ public function log($connection, $queue, $payload, $exception) { - if ($connection !== 'sqs') { + if ($connection !== 'cloud') { return $this->failer->log(...func_get_args()); } diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php index fe3c5c611924..dd9b987c85d3 100644 --- a/src/Illuminate/Foundation/Cloud/Queue.php +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -33,20 +33,6 @@ class Queue implements QueueContract, ClearableQueue */ protected $processingJobStartedAt = null; - /** - * The queue prefix. - * - * @var string - */ - protected $prefix; - - /** - * The queue suffix. - * - * @var string - */ - protected $suffix; - /** * Create a new Queue instance. */ @@ -55,11 +41,7 @@ public function __construct( protected Events $events, protected array $config, ) { - $this->prefix = array_key_exists('prefix', $config) && is_string($config['prefix']) - ? $config['prefix'].'/' - : ''; - - $this->suffix = $config['suffix'] ?? ''; + // } /** @@ -259,7 +241,9 @@ public function setConnectionName($name) */ public function setConfig($config) { - $this->queue->setConfig(...func_get_args()); + $this->config = $config; + + $this->queue->setConfig($config['connection']); return $this; } @@ -377,9 +361,12 @@ protected function startProcessingJob($queue, $job) */ protected function normalizeQueue($queue) { + $prefix = $this->config['connection']['prefix'] ?? null; + $suffix = $this->config['connection']['suffix'] ?? null; + return Str::of($this->queue->getQueue($queue)) - ->chopStart($this->prefix) - ->chopEnd($this->suffix) + ->when($prefix, fn ($str) => $str->chopStart($prefix.'/')) + ->when($suffix, fn ($str) => $str->chopEnd($suffix)) ->toString(); } diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index 23926b1174ac..efe058b6c1fc 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -32,7 +32,7 @@ public function __construct( public function connect(array $config): Queue { $queue = new Queue( - $this->connector->connect($config), + $this->connector->connect($config['connection']), $this->app[Events::class], $config, ); diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 68bc3dce5695..112aecb31ebf 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -20,16 +20,14 @@ use Illuminate\Queue\SqsQueue; use Illuminate\Queue\Worker; use Illuminate\Queue\WorkerStopReason; -use Illuminate\Support\Arr; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Illuminate\Support\Testing\Fakes\QueueFake; +use InvalidArgumentException; use Mockery\MockInterface; -use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; use Ramsey\Uuid\Uuid; @@ -51,21 +49,29 @@ protected function setUp(): void { Worker::$restartable = true; Worker::$pausable = true; - $_SERVER['LARAVEL_CLOUD'] = $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD'] = '1'; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'] = json_encode([ + 'driver' => 'cloud', + 'connection' => [ + 'driver' => 'sqs', + 'region' => 'us-east-2', + 'prefix' => 'https://sqs.us-east-2.amazonaws.com/1234567', + 'suffix' => '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', + 'queue' => 'default', + ], + ]); parent::setUp(); - $this->app['config']->set([ - 'queue.connections.sqs.prefix' => 'https://sqs.us-east-2.amazonaws.com/1234567', - 'queue.connections.sqs.suffix' => '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', - ]); + $this->app['config']->set('queue.connections.cloud', json_decode($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'], true)); } protected function tearDown(): void { parent::tearDown(); - unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); Worker::$restartable = true; Worker::$pausable = true; } @@ -79,7 +85,7 @@ public function testItDisablesQueueRestartPollingForManagedQueues() Cloud::bootManagedQueues($this->app); $this->assertTrue(Worker::$restartable); - $this->app['queue']->connection('sqs'); + $this->app['queue']->connection('cloud'); $this->assertFalse(Worker::$restartable); } finally { $_SERVER['argv'] = $argv; @@ -95,65 +101,42 @@ public function testItDisablesQueuePausePollingForManagedQueues() Cloud::bootManagedQueues($this->app); $this->assertTrue(Worker::$pausable); - $this->app['queue']->connection('sqs'); + $this->app['queue']->connection('cloud'); $this->assertFalse(Worker::$pausable); } finally { $_SERVER['argv'] = $argv; } } - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function testItConfiguresManagedQueueCredentials() + public function testItConfiguresCloudConnectionFromManagedQueuesConfig() { - Cloud::configureManagedQueues($this->app); + $this->app['config']->set('queue.connections.cloud', null); - $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function testItDoesNotConfigureManagedQueuesWhenNotEnabled() - { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); Cloud::configureManagedQueues($this->app); - $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function testItConfiguresManagedQueueRegion() - { - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); - } - } - - public function testItSetSqsCredentialsToEcs() - { - $this->assertSame(null, Config::get('queue.connections.sqs.credentials')); - - Cloud::configureManagedQueues($this->app); + $expected = json_decode($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'], true); + $expected['connection']['after_commit'] = false; + $expected['connection']['overflow'] = [ + 'enabled' => false, + 'store' => null, + 'always' => false, + 'delete_after_processing' => true, + ]; - $this->assertSame('ecs', Config::get('queue.connections.sqs.credentials')); + $this->assertSame( + $expected, + $this->app['config']->get('queue.connections.cloud'), + ); } - public function testItSetsTheSqsRegion() + public function testItDoesNotConfigureManagedQueuesWhenNotEnabled() { - $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); + $this->app['config']->set('queue.connections.cloud', null); Cloud::configureManagedQueues($this->app); - $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); - $_SERVER['LARAVEL_CLOUD_REGION'] = 'eu-central-1'; - Cloud::configureManagedQueues($this->app); - - $this->assertSame('eu-central-1', Config::get('queue.connections.sqs.region')); + $this->assertNull($this->app['config']->get('queue.connections.cloud')); } public function testItBindsQueueConnectorAndNewsUpSqsConnector() @@ -168,7 +151,7 @@ public function testItBindsCloudQueue() { Cloud::bootManagedQueues($this->app); - $this->assertInstanceOf(Queue::class, $this->app['queue']->connection('sqs')); + $this->assertInstanceOf(Queue::class, $this->app['queue']->connection('cloud')); } public function testItBindsCloudEventsAsSingleton() @@ -186,13 +169,15 @@ public function testItBindsTheQueueFailer() $this->assertInstanceOf(FailedJobProvider::class, $this->app['queue.failer']); } - public function testItDoesNotBindCloudQueueWhenManagedQueuesIsInactive() + public function testItDoesNotRegisterCloudConnectorWhenManagedQueuesIsInactive() { unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); Cloud::bootManagedQueues($this->app); - $this->assertInstanceOf(SqsQueue::class, $this->app['queue']->connection('sqs')); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No connector for [cloud]'); + $this->app['queue']->connection('cloud'); } public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() @@ -319,7 +304,7 @@ public function testItEmitsFailedJobEvents() $queue->pop(); $jobFake->fail(); Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]); - $failedJobProvider->log('sqs', 'default', ['payload' => 'here'], new RuntimeException('Whoops!')); + $failedJobProvider->log('cloud', 'default', ['payload' => 'here'], new RuntimeException('Whoops!')); Str::createUuidsNormally(); $queue->pop(); @@ -661,7 +646,7 @@ public function testItRespectsDispatchAfterTransaction() Cloud::configureManagedQueues($this->app); Cloud::bootManagedQueues($this->app); $eventsFake = $this->fakeEvents(); - $this->app['config']->set('queue.connections.sqs.after_commit', true); + $this->app['config']->set('queue.connections.cloud.connection.after_commit', true); [$queue, $client] = $this->mockedQueue(); $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); @@ -786,7 +771,7 @@ public function testFindGetsUrlAndDecryptsResponse() $failer = $this->fakeFailer(); $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); - $payload = ['id' => 'test-job-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{"job":"App\\\\Jobs\\\\TestJob"}']; + $payload = ['id' => 'test-job-id', 'connection' => 'cloud', 'queue' => 'default', 'payload' => '{"job":"App\\\\Jobs\\\\TestJob"}']; $encrypted = Crypt::encryptString(json_encode($payload)); Http::fake([ @@ -797,7 +782,7 @@ public function testFindGetsUrlAndDecryptsResponse() $this->assertIsObject($result); $this->assertSame('test-job-id', $result->id); - $this->assertSame('sqs', $result->connection); + $this->assertSame('cloud', $result->connection); $this->assertSame('default', $result->queue); $this->assertSame('{"job":"App\\\\Jobs\\\\TestJob"}', $result->payload); Http::assertSent(fn ($request) => $request->url() === 'https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); @@ -864,7 +849,7 @@ public function testForgetEmitsEventAfterFind() $failer = $this->fakeFailer(); $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); - $payload = ['id' => 'forget-test-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{}']; + $payload = ['id' => 'forget-test-id', 'connection' => 'cloud', 'queue' => 'default', 'payload' => '{}']; $encrypted = Crypt::encryptString(json_encode($payload)); Http::fake([ @@ -913,22 +898,6 @@ public function testItUsesConfigValuesToNormalizeQueueName() $this->assertSame('my-queue', $eventsFake->emitted[0]['queue']); } - public function testItHandlesMissingPrefixAndSuffixConfig() - { - Cloud::configureManagedQueues($this->app); - Cloud::bootManagedQueues($this->app); - $eventsFake = $this->fakeEvents(); - $this->app['config']->set('queue.connections.sqs', Arr::except($this->app['config']->get('queue.connections.sqs'), ['prefix', 'suffix'])); - [$queue, $client] = $this->mockedQueue(); - $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); - - unset($_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX']); - - $queue->push(new FakeJob, queue: 'https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'); - - $this->assertSame('https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', $eventsFake->emitted[0]['queue']); - } - /** * @return array{Queue, MockInterface} */ @@ -956,9 +925,9 @@ public function connect($config) } }, $this->app)); - $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + $this->app['queue']->addConnector('cloud', $this->app->factory(QueueConnector::class)); - return [$this->app['queue']->connection('sqs'), $client]; + return [$this->app['queue']->connection('cloud'), $client]; } private function fakeEvents() @@ -995,7 +964,7 @@ public function getQueue($queue) { $queue ??= 'default'; - return config('queue.connections.sqs.prefix').'/'.$queue.config('queue.connections.sqs.suffix'); + return config('queue.connections.cloud.connection.prefix').'/'.$queue.config('queue.connections.cloud.connection.suffix'); } public function setConfig(array $config) @@ -1022,9 +991,9 @@ public function connect($config) } }, $this->app)); - $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + $this->app['queue']->addConnector('cloud', $this->app->factory(QueueConnector::class)); - return [$this->app['queue']->connection('sqs'), $fakeQueue]; + return [$this->app['queue']->connection('cloud'), $fakeQueue]; } private function fakeFailer() From 39d1ad80ffcf99baf63eb16bbece29c0fc1f266f Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Tue, 19 May 2026 19:12:51 +0100 Subject: [PATCH 408/596] [12.x] Dedicated Cloud Queue (#60181) --- src/Illuminate/Foundation/Cloud.php | 29 ++-- .../Foundation/Cloud/FailedJobProvider.php | 2 +- src/Illuminate/Foundation/Cloud/Queue.php | 31 ++--- .../Foundation/Cloud/QueueConnector.php | 2 +- tests/Foundation/Cloud/QueueTest.php | 131 +++++++----------- 5 files changed, 75 insertions(+), 120 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 029f55c4cd94..e4499da86e6b 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -125,15 +125,22 @@ public static function ensureMigrationsUseUnpooledConnection(Application $app): */ public static function configureManagedQueues(Application $app): void { - if (! Cloud::managedQueuesAreActive()) { + if (! isset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'])) { return; } - $app['config']->set('queue.connections.sqs.credentials', 'ecs'); + $config = json_decode($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'], associative: true, flags: JSON_THROW_ON_ERROR); - if (isset($_SERVER['LARAVEL_CLOUD_REGION'])) { - $app['config']->set('queue.connections.sqs.region', $_SERVER['LARAVEL_CLOUD_REGION']); - } + $config['connection']['after_commit'] ??= env('CLOUD_QUEUE_AFTER_COMMIT', false); + + $config['connection']['overflow'] ??= [ + 'enabled' => env('CLOUD_QUEUE_OVERFLOW_ENABLED', false), + 'store' => env('CLOUD_QUEUE_OVERFLOW_STORE'), + 'always' => env('CLOUD_QUEUE_OVERFLOW_ALWAYS', false), + 'delete_after_processing' => env('CLOUD_QUEUE_OVERFLOW_DELETE_AFTER_PROCESSING', true), + ]; + + $app['config']->set('queue.connections.cloud', $config); } /** @@ -141,14 +148,14 @@ public static function configureManagedQueues(Application $app): void */ public static function bootManagedQueues(Application $app): void { - if (! Cloud::managedQueuesAreActive()) { + if (($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? '0') !== '1') { return; } $app->singleton(Events::class, fn () => new Events(Cloud::socket())); $app->bind(QueueConnector::class, fn ($app) => new QueueConnector(new SqsConnector, $app)); - $app['queue']->addConnector('sqs', $app->factory(QueueConnector::class)); + $app['queue']->addConnector('cloud', $app->factory(QueueConnector::class)); $failer = $app['queue.failer']; unset($app['queue.failer']); @@ -191,12 +198,4 @@ protected static function socket(): string $_SERVER['LARAVEL_CLOUD_LOG_SOCKET'] ?? 'unix:///tmp/cloud-init.sock'; } - - /** - * Determine if managed queues are active. - */ - protected static function managedQueuesAreActive(): bool - { - return ($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? null) === '1'; - } } diff --git a/src/Illuminate/Foundation/Cloud/FailedJobProvider.php b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php index 9c0dd8dad795..aa4133da95ea 100644 --- a/src/Illuminate/Foundation/Cloud/FailedJobProvider.php +++ b/src/Illuminate/Foundation/Cloud/FailedJobProvider.php @@ -51,7 +51,7 @@ public function __construct( */ public function log($connection, $queue, $payload, $exception) { - if ($connection !== 'sqs') { + if ($connection !== 'cloud') { return $this->failer->log(...func_get_args()); } diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php index fe3c5c611924..dd9b987c85d3 100644 --- a/src/Illuminate/Foundation/Cloud/Queue.php +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -33,20 +33,6 @@ class Queue implements QueueContract, ClearableQueue */ protected $processingJobStartedAt = null; - /** - * The queue prefix. - * - * @var string - */ - protected $prefix; - - /** - * The queue suffix. - * - * @var string - */ - protected $suffix; - /** * Create a new Queue instance. */ @@ -55,11 +41,7 @@ public function __construct( protected Events $events, protected array $config, ) { - $this->prefix = array_key_exists('prefix', $config) && is_string($config['prefix']) - ? $config['prefix'].'/' - : ''; - - $this->suffix = $config['suffix'] ?? ''; + // } /** @@ -259,7 +241,9 @@ public function setConnectionName($name) */ public function setConfig($config) { - $this->queue->setConfig(...func_get_args()); + $this->config = $config; + + $this->queue->setConfig($config['connection']); return $this; } @@ -377,9 +361,12 @@ protected function startProcessingJob($queue, $job) */ protected function normalizeQueue($queue) { + $prefix = $this->config['connection']['prefix'] ?? null; + $suffix = $this->config['connection']['suffix'] ?? null; + return Str::of($this->queue->getQueue($queue)) - ->chopStart($this->prefix) - ->chopEnd($this->suffix) + ->when($prefix, fn ($str) => $str->chopStart($prefix.'/')) + ->when($suffix, fn ($str) => $str->chopEnd($suffix)) ->toString(); } diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index 23926b1174ac..efe058b6c1fc 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -32,7 +32,7 @@ public function __construct( public function connect(array $config): Queue { $queue = new Queue( - $this->connector->connect($config), + $this->connector->connect($config['connection']), $this->app[Events::class], $config, ); diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 68bc3dce5695..112aecb31ebf 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -20,16 +20,14 @@ use Illuminate\Queue\SqsQueue; use Illuminate\Queue\Worker; use Illuminate\Queue\WorkerStopReason; -use Illuminate\Support\Arr; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Illuminate\Support\Testing\Fakes\QueueFake; +use InvalidArgumentException; use Mockery\MockInterface; -use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; use Ramsey\Uuid\Uuid; @@ -51,21 +49,29 @@ protected function setUp(): void { Worker::$restartable = true; Worker::$pausable = true; - $_SERVER['LARAVEL_CLOUD'] = $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD'] = '1'; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; + $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'] = json_encode([ + 'driver' => 'cloud', + 'connection' => [ + 'driver' => 'sqs', + 'region' => 'us-east-2', + 'prefix' => 'https://sqs.us-east-2.amazonaws.com/1234567', + 'suffix' => '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', + 'queue' => 'default', + ], + ]); parent::setUp(); - $this->app['config']->set([ - 'queue.connections.sqs.prefix' => 'https://sqs.us-east-2.amazonaws.com/1234567', - 'queue.connections.sqs.suffix' => '-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', - ]); + $this->app['config']->set('queue.connections.cloud', json_decode($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'], true)); } protected function tearDown(): void { parent::tearDown(); - unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); Worker::$restartable = true; Worker::$pausable = true; } @@ -79,7 +85,7 @@ public function testItDisablesQueueRestartPollingForManagedQueues() Cloud::bootManagedQueues($this->app); $this->assertTrue(Worker::$restartable); - $this->app['queue']->connection('sqs'); + $this->app['queue']->connection('cloud'); $this->assertFalse(Worker::$restartable); } finally { $_SERVER['argv'] = $argv; @@ -95,65 +101,42 @@ public function testItDisablesQueuePausePollingForManagedQueues() Cloud::bootManagedQueues($this->app); $this->assertTrue(Worker::$pausable); - $this->app['queue']->connection('sqs'); + $this->app['queue']->connection('cloud'); $this->assertFalse(Worker::$pausable); } finally { $_SERVER['argv'] = $argv; } } - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function testItConfiguresManagedQueueCredentials() + public function testItConfiguresCloudConnectionFromManagedQueuesConfig() { - Cloud::configureManagedQueues($this->app); + $this->app['config']->set('queue.connections.cloud', null); - $this->assertEquals('ecs', $this->app['config']->get('queue.connections.sqs.credentials')); - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function testItDoesNotConfigureManagedQueuesWhenNotEnabled() - { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); Cloud::configureManagedQueues($this->app); - $this->assertNull($this->app['config']->get('queue.connections.sqs.credentials')); - } - - #[WithConfig('queue.connections.sqs', ['driver' => 'sqs', 'region' => 'us-east-1', 'queue' => 'default'])] - public function testItConfiguresManagedQueueRegion() - { - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; - $_SERVER['LARAVEL_CLOUD_REGION'] = 'us-west-2'; - - try { - Cloud::configureManagedQueues($this->app); - - $this->assertEquals('us-west-2', $this->app['config']->get('queue.connections.sqs.region')); - } finally { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_REGION']); - } - } - - public function testItSetSqsCredentialsToEcs() - { - $this->assertSame(null, Config::get('queue.connections.sqs.credentials')); - - Cloud::configureManagedQueues($this->app); + $expected = json_decode($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'], true); + $expected['connection']['after_commit'] = false; + $expected['connection']['overflow'] = [ + 'enabled' => false, + 'store' => null, + 'always' => false, + 'delete_after_processing' => true, + ]; - $this->assertSame('ecs', Config::get('queue.connections.sqs.credentials')); + $this->assertSame( + $expected, + $this->app['config']->get('queue.connections.cloud'), + ); } - public function testItSetsTheSqsRegion() + public function testItDoesNotConfigureManagedQueuesWhenNotEnabled() { - $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); + unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); + $this->app['config']->set('queue.connections.cloud', null); Cloud::configureManagedQueues($this->app); - $this->assertSame('us-east-1', Config::get('queue.connections.sqs.region')); - $_SERVER['LARAVEL_CLOUD_REGION'] = 'eu-central-1'; - Cloud::configureManagedQueues($this->app); - - $this->assertSame('eu-central-1', Config::get('queue.connections.sqs.region')); + $this->assertNull($this->app['config']->get('queue.connections.cloud')); } public function testItBindsQueueConnectorAndNewsUpSqsConnector() @@ -168,7 +151,7 @@ public function testItBindsCloudQueue() { Cloud::bootManagedQueues($this->app); - $this->assertInstanceOf(Queue::class, $this->app['queue']->connection('sqs')); + $this->assertInstanceOf(Queue::class, $this->app['queue']->connection('cloud')); } public function testItBindsCloudEventsAsSingleton() @@ -186,13 +169,15 @@ public function testItBindsTheQueueFailer() $this->assertInstanceOf(FailedJobProvider::class, $this->app['queue.failer']); } - public function testItDoesNotBindCloudQueueWhenManagedQueuesIsInactive() + public function testItDoesNotRegisterCloudConnectorWhenManagedQueuesIsInactive() { unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); Cloud::bootManagedQueues($this->app); - $this->assertInstanceOf(SqsQueue::class, $this->app['queue']->connection('sqs')); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No connector for [cloud]'); + $this->app['queue']->connection('cloud'); } public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() @@ -319,7 +304,7 @@ public function testItEmitsFailedJobEvents() $queue->pop(); $jobFake->fail(); Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]); - $failedJobProvider->log('sqs', 'default', ['payload' => 'here'], new RuntimeException('Whoops!')); + $failedJobProvider->log('cloud', 'default', ['payload' => 'here'], new RuntimeException('Whoops!')); Str::createUuidsNormally(); $queue->pop(); @@ -661,7 +646,7 @@ public function testItRespectsDispatchAfterTransaction() Cloud::configureManagedQueues($this->app); Cloud::bootManagedQueues($this->app); $eventsFake = $this->fakeEvents(); - $this->app['config']->set('queue.connections.sqs.after_commit', true); + $this->app['config']->set('queue.connections.cloud.connection.after_commit', true); [$queue, $client] = $this->mockedQueue(); $client->shouldReceive('sendMessage')->times(7)->andReturn(new Result()); @@ -786,7 +771,7 @@ public function testFindGetsUrlAndDecryptsResponse() $failer = $this->fakeFailer(); $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); - $payload = ['id' => 'test-job-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{"job":"App\\\\Jobs\\\\TestJob"}']; + $payload = ['id' => 'test-job-id', 'connection' => 'cloud', 'queue' => 'default', 'payload' => '{"job":"App\\\\Jobs\\\\TestJob"}']; $encrypted = Crypt::encryptString(json_encode($payload)); Http::fake([ @@ -797,7 +782,7 @@ public function testFindGetsUrlAndDecryptsResponse() $this->assertIsObject($result); $this->assertSame('test-job-id', $result->id); - $this->assertSame('sqs', $result->connection); + $this->assertSame('cloud', $result->connection); $this->assertSame('default', $result->queue); $this->assertSame('{"job":"App\\\\Jobs\\\\TestJob"}', $result->payload); Http::assertSent(fn ($request) => $request->url() === 'https://cloud.laravel.com/api/jobs/test-job-id?signature=abc'); @@ -864,7 +849,7 @@ public function testForgetEmitsEventAfterFind() $failer = $this->fakeFailer(); $provider = new FailedJobProvider($failer, $eventsFake, $this->app['encrypter']); - $payload = ['id' => 'forget-test-id', 'connection' => 'sqs', 'queue' => 'default', 'payload' => '{}']; + $payload = ['id' => 'forget-test-id', 'connection' => 'cloud', 'queue' => 'default', 'payload' => '{}']; $encrypted = Crypt::encryptString(json_encode($payload)); Http::fake([ @@ -913,22 +898,6 @@ public function testItUsesConfigValuesToNormalizeQueueName() $this->assertSame('my-queue', $eventsFake->emitted[0]['queue']); } - public function testItHandlesMissingPrefixAndSuffixConfig() - { - Cloud::configureManagedQueues($this->app); - Cloud::bootManagedQueues($this->app); - $eventsFake = $this->fakeEvents(); - $this->app['config']->set('queue.connections.sqs', Arr::except($this->app['config']->get('queue.connections.sqs'), ['prefix', 'suffix'])); - [$queue, $client] = $this->mockedQueue(); - $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); - - unset($_SERVER['SQS_PREFIX'], $_SERVER['SQS_SUFFIX']); - - $queue->push(new FakeJob, queue: 'https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f'); - - $this->assertSame('https://sqs.us-east-2.amazonaws.com/1234567/my-queue-env-8280cf2c-2081-47e8-a1f1-9cdfcba8618f', $eventsFake->emitted[0]['queue']); - } - /** * @return array{Queue, MockInterface} */ @@ -956,9 +925,9 @@ public function connect($config) } }, $this->app)); - $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + $this->app['queue']->addConnector('cloud', $this->app->factory(QueueConnector::class)); - return [$this->app['queue']->connection('sqs'), $client]; + return [$this->app['queue']->connection('cloud'), $client]; } private function fakeEvents() @@ -995,7 +964,7 @@ public function getQueue($queue) { $queue ??= 'default'; - return config('queue.connections.sqs.prefix').'/'.$queue.config('queue.connections.sqs.suffix'); + return config('queue.connections.cloud.connection.prefix').'/'.$queue.config('queue.connections.cloud.connection.suffix'); } public function setConfig(array $config) @@ -1022,9 +991,9 @@ public function connect($config) } }, $this->app)); - $this->app['queue']->addConnector('sqs', $this->app->factory(QueueConnector::class)); + $this->app['queue']->addConnector('cloud', $this->app->factory(QueueConnector::class)); - return [$this->app['queue']->connection('sqs'), $fakeQueue]; + return [$this->app['queue']->connection('cloud'), $fakeQueue]; } private function fakeFailer() From 4ddf0168d3c756b2b66ce6f47fd146f9876be074 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 18:13:27 +0000 Subject: [PATCH 409/596] Update CHANGELOG --- CHANGELOG.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2d2701c5bc..0a7d2a7d9879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,42 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.9.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.10.0...13.x) + +## [v13.10.0](https://github.com/laravel/framework/compare/v13.9.0...v13.10.0) - 2026-05-19 + +* [13.x] Improve queue metric tests by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60124 +* [13.x] Add tests for SeeInHtml constraint covering unicode whitespace by [@scabarcas17](https://github.com/scabarcas17) in https://github.com/laravel/framework/pull/60128 +* [13.x] Fix starts_with/ends_with rules rejecting numeric values by [@aydinfatih](https://github.com/aydinfatih) in https://github.com/laravel/framework/pull/60120 +* Fix typo in docblock for listManagementOptions method in SesV2Transport by [@daliendev](https://github.com/daliendev) in https://github.com/laravel/framework/pull/60115 +* [13.x] Fix typo in mergeAttributeFromCachedCasts() PHPDoc comment by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/60112 +* [13.x] Optimize Worker queue pause check by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60109 +* [13.x] Fix typo in preg_replace_array() PHPDoc comment by [@mosabbirrakib](https://github.com/mosabbirrakib) in https://github.com/laravel/framework/pull/60111 +* Add storage store by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/60131 +* Fix typo in Builder::getRelation() comment by [@rpsohag](https://github.com/rpsohag) in https://github.com/laravel/framework/pull/60130 +* URL Encode Paths by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/60137 +* [13.x] Add WorkerIdle event by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60134 +* [13.x] Replace [@return](https://github.com/return) with [@var](https://github.com/var) on docblocks for properties in tests by [@scabarcas17](https://github.com/scabarcas17) in https://github.com/laravel/framework/pull/60132 +* [13.x] Pass WorkerOptions to Pausing/Resuming/Interrupted by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60135 +* [13.x] Switch force check in Worker by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60145 +* [13.x] Skip delimiter filesystem tests on Windows by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60143 +* Delimit aggregate alias by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/60140 +* [13.x] Allow lifecycle and output callbacks on Schedule::group() by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60133 +* [13.x] Allow passing scheduled `Event` in callbacks by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60144 +* Validate against line breaks in emails by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/60151 +* [13.x] Add `assertPushedOnce()` by [@weshooper](https://github.com/weshooper) in https://github.com/laravel/framework/pull/60150 +* [13.x] Fix callable usage in `Event@callEventCallback()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60148 +* [13.x] Fix numeric property names being cast to integers in JsonSchema requi… by [@irabbi360](https://github.com/irabbi360) in https://github.com/laravel/framework/pull/60149 +* [13.x] Pass worker options to Looping by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60153 +* [13.x] Support enum queue names in QueueFake by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/60161 +* [13.x] Remove obsolete [@phpstan-ignore](https://github.com/phpstan-ignore) clauses by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/60164 +* [13.x] Consistent test OS attributes by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/60162 +* Output cloud request ID in logs by [@jradtilbrook](https://github.com/jradtilbrook) in https://github.com/laravel/framework/pull/60156 +* [13.x] Optionally flush the SQS overflow store on queue:clear by [@Orrison](https://github.com/Orrison) in https://github.com/laravel/framework/pull/60138 +* [13.x] Add JSON output option to ListFailedCommand and corresponding tests by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/60168 +* [13.x] Add schema foreign key existence helper by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/60169 +* Stop when empty for by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/60176 +* [13.x] Add initial value type to return of `reduce` and `reduceWithKeys` by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/60178 +* [13.x] Fix incorrect type hint in EncodedHtmlString::convert() docblock by [@rpsohag](https://github.com/rpsohag) in https://github.com/laravel/framework/pull/60186 ## [v13.9.0](https://github.com/laravel/framework/compare/v13.8.0...v13.9.0) - 2026-05-13 From d3d814f9d67270aa5320544948f8e496d551a594 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 18:16:21 +0000 Subject: [PATCH 410/596] Update version to v13.11.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index cc2295e68676..f3f4a0d2774c 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 = '13.10.0'; + const VERSION = '13.11.0'; /** * The base path for the Laravel installation. From e190491d9df96945e331c3be80e565db2146d9aa Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 18:19:15 +0000 Subject: [PATCH 411/596] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7d2a7d9879..d82e59bf7271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.10.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.11.0...13.x) + +## [v13.11.0](https://github.com/laravel/framework/compare/v13.10.0...v13.11.0) - 2026-05-19 + +* [13.x] Dedicated Cloud Queue by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60180 ## [v13.10.0](https://github.com/laravel/framework/compare/v13.9.0...v13.10.0) - 2026-05-19 From b39ddd3fea8c93ccd3ec10c2472e9ea5167494f2 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 18:21:44 +0000 Subject: [PATCH 412/596] Update version to v12.60.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 3fdb843cca62..f839a25c35ce 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.59.0'; + const VERSION = '12.60.0'; /** * The base path for the Laravel installation. From 4d0c4f77ae95f46e336d872d087167be24fc9611 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 18:23:19 +0000 Subject: [PATCH 413/596] Update CHANGELOG --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e716e6c215a8..62ad0ed3bbcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.59.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.60.0...12.x) + +## [v12.60.0](https://github.com/laravel/framework/compare/v12.59.0...v12.60.0) - 2026-05-19 + +* [12.x] Fix Number::fileSize() handling of negative byte values by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60147 +* [12.x] Remove stale PHPStan ignore comments from type tests by [@jradtilbrook](https://github.com/jradtilbrook) in https://github.com/laravel/framework/pull/60167 +* [12.x] Output cloud request ID in logs by [@jradtilbrook](https://github.com/jradtilbrook) in https://github.com/laravel/framework/pull/60166 +* [12.x] Dedicated Cloud Queue by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60181 ## [v12.59.0](https://github.com/laravel/framework/compare/v12.58.0...v12.59.0) - 2026-05-14 From a7c6f00b4b37a4158b214300aa8c020acd3639c6 Mon Sep 17 00:00:00 2001 From: wangchenxudev <284582661+wangchenxudev@users.noreply.github.com> Date: Wed, 20 May 2026 02:39:39 +0800 Subject: [PATCH 414/596] Normalize Windows env variable comparison in ServeCommand (#60183) * Fix Windows serve environment passthrough matching * Normalize Windows env variable comparison in ServeCommand * Use first-class callable for environment variable normalization * Update ServeCommand.php --------- Co-authored-by: Taylor Otwell --- .../Foundation/Console/ServeCommand.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index 670cc896596d..7033275e2434 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -185,7 +185,7 @@ protected function startProcess($hasEnvironment) return [$key => $value]; } - return in_array($key, static::$passthroughVariables) ? [$key => $value] : [$key => false]; + return $this->shouldPassThroughEnvironmentVariable($key) ? [$key => $value] : [$key => false]; })->merge(['PHP_CLI_SERVER_WORKERS' => $this->phpServerWorkers])->all()); $this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) { @@ -283,6 +283,21 @@ protected function canTryAnotherPort() ($this->input->getOption('tries') > $this->portOffset); } + /** + * Determine if the environment variable should be passed to the PHP server process. + * + * @param string $key + * @return bool + */ + protected function shouldPassThroughEnvironmentVariable($key) + { + if (PHP_OS_FAMILY === 'Windows') { + return in_array(strtoupper($key), array_map(strtoupper(...), static::$passthroughVariables), true); + } + + return in_array($key, static::$passthroughVariables, true); + } + /** * Returns a "callable" to handle the process output. * From 499d5e74f8db97df0755a65e67db75d222920fe9 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Tue, 19 May 2026 21:22:01 +0100 Subject: [PATCH 415/596] Rename X-Request-ID header to Cloud-Request-ID (#60188) --- src/Illuminate/Foundation/LaravelCloudJsonFormatter.php | 2 +- tests/Foundation/LaravelCloudJsonFormatterTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php index 942714e1eeef..0501655e1566 100644 --- a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php +++ b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php @@ -18,7 +18,7 @@ protected function normalizeRecord(LogRecord $record): array $app = Container::getInstance(); if ($app->bound('request')) { - $requestId = $app->make('request')->header('X-Request-ID'); + $requestId = $app->make('request')->header('Cloud-Request-ID'); if ($requestId !== null) { $normalized['cloud_request_id'] = $requestId; diff --git a/tests/Foundation/LaravelCloudJsonFormatterTest.php b/tests/Foundation/LaravelCloudJsonFormatterTest.php index b9ebcc222b7b..c590f72ea6b6 100644 --- a/tests/Foundation/LaravelCloudJsonFormatterTest.php +++ b/tests/Foundation/LaravelCloudJsonFormatterTest.php @@ -39,7 +39,7 @@ public function test_adds_cloud_request_id_as_top_level_key() { $app = Container::getInstance(); $request = Request::create('/'); - $request->headers->set('X-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); + $request->headers->set('Cloud-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); $app->instance('request', $request); $formatter = new LaravelCloudJsonFormatter; @@ -75,7 +75,7 @@ public function test_preserves_existing_log_fields() { $app = Container::getInstance(); $request = Request::create('/'); - $request->headers->set('X-Request-ID', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'); + $request->headers->set('Cloud-Request-ID', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'); $app->instance('request', $request); $record = new LogRecord( From a3fa8215972aa8f030772831927a877aadda5ce1 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Tue, 19 May 2026 21:22:11 +0100 Subject: [PATCH 416/596] Rename X-Request-ID header to Cloud-Request-ID (#60189) --- src/Illuminate/Foundation/LaravelCloudJsonFormatter.php | 2 +- tests/Foundation/LaravelCloudJsonFormatterTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php index 942714e1eeef..0501655e1566 100644 --- a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php +++ b/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php @@ -18,7 +18,7 @@ protected function normalizeRecord(LogRecord $record): array $app = Container::getInstance(); if ($app->bound('request')) { - $requestId = $app->make('request')->header('X-Request-ID'); + $requestId = $app->make('request')->header('Cloud-Request-ID'); if ($requestId !== null) { $normalized['cloud_request_id'] = $requestId; diff --git a/tests/Foundation/LaravelCloudJsonFormatterTest.php b/tests/Foundation/LaravelCloudJsonFormatterTest.php index b9ebcc222b7b..c590f72ea6b6 100644 --- a/tests/Foundation/LaravelCloudJsonFormatterTest.php +++ b/tests/Foundation/LaravelCloudJsonFormatterTest.php @@ -39,7 +39,7 @@ public function test_adds_cloud_request_id_as_top_level_key() { $app = Container::getInstance(); $request = Request::create('/'); - $request->headers->set('X-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); + $request->headers->set('Cloud-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); $app->instance('request', $request); $formatter = new LaravelCloudJsonFormatter; @@ -75,7 +75,7 @@ public function test_preserves_existing_log_fields() { $app = Container::getInstance(); $request = Request::create('/'); - $request->headers->set('X-Request-ID', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'); + $request->headers->set('Cloud-Request-ID', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'); $app->instance('request', $request); $record = new LogRecord( From 6b70133ea3552afc37307ffb85b9efa48dc187d1 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 20:24:39 +0000 Subject: [PATCH 417/596] Update version to v13.11.1 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index f3f4a0d2774c..ec16b31b447c 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 = '13.11.0'; + const VERSION = '13.11.1'; /** * The base path for the Laravel installation. From 943edef4a258da8464fef5eefd0e3688c2ab675e Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 20:26:27 +0000 Subject: [PATCH 418/596] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82e59bf7271..aeb1aefe5683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.11.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.11.1...13.x) + +## [v13.11.1](https://github.com/laravel/framework/compare/v13.11.0...v13.11.1) - 2026-05-19 + +* Normalize Windows env variable comparison in ServeCommand by [@wangchenxudev](https://github.com/wangchenxudev) in https://github.com/laravel/framework/pull/60183 +* [13.x] Rename X-Request-ID header to Cloud-Request-ID by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60188 ## [v13.11.0](https://github.com/laravel/framework/compare/v13.10.0...v13.11.0) - 2026-05-19 From 7ca345438e0a191a8382771bd97ebaab2052b807 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 20:43:52 +0000 Subject: [PATCH 419/596] Update version to v12.60.1 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index f839a25c35ce..9e8a716af998 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.60.0'; + const VERSION = '12.60.1'; /** * The base path for the Laravel installation. From 78b8934d5926bc3f1baf2ee1eb3f3e587027c96a Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 19 May 2026 20:45:33 +0000 Subject: [PATCH 420/596] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ad0ed3bbcb..b3e567e33f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.60.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.60.1...12.x) + +## [v12.60.1](https://github.com/laravel/framework/compare/v12.60.0...v12.60.1) - 2026-05-19 + +* [12.x] Rename X-Request-ID header to Cloud-Request-ID by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60189 ## [v12.60.0](https://github.com/laravel/framework/compare/v12.59.0...v12.60.0) - 2026-05-19 From e8043a0a2c00b9decbc041ac3b7d37079c70ad64 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Tue, 19 May 2026 17:32:52 -0400 Subject: [PATCH 421/596] fix grouping (#60190) --- .../Scheduling/PendingEventAttributes.php | 2 +- .../Console/Scheduling/Schedule.php | 4 +- .../Console/Scheduling/ScheduleGroupTest.php | 55 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php index 25f43555f6e9..74a61859ac06 100644 --- a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php +++ b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php @@ -14,7 +14,7 @@ class PendingEventAttributes * * @var array */ - protected const DEFERRED_EVENT_METHODS = [ + public const DEFERRED_EVENT_METHODS = [ 'before', 'after', 'then', diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 14f262bbb4c0..4ce9126f244a 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -502,7 +502,9 @@ public function __call($method, $parameters) return $this->macroCall($method, $parameters); } - if (method_exists(PendingEventAttributes::class, $method) || Event::hasMacro($method)) { + if (method_exists(PendingEventAttributes::class, $method) + || in_array($method, PendingEventAttributes::DEFERRED_EVENT_METHODS, true) + || Event::hasMacro($method)) { $this->attributes ??= $this->groupStack ? clone array_last($this->groupStack) : new PendingEventAttributes($this); return $this->attributes->$method(...$parameters); diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index 14cdfe393742..440a80d2146a 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -468,4 +468,59 @@ public function testNestedGroupInheritsLifecycleCallbacks() $events[1]->finish(app(), 1); $this->assertSame(['outer', 'outer', 'inner'], $calls); } + + public function testGroupCanStartWithLifecycleCallbackWithoutFrequency() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->before(function () use (&$calls) { + $calls[] = 'before'; + }) + ->onSuccess(function () use (&$calls) { + $calls[] = 'success'; + }) + ->onFailure(function () use (&$calls) { + $calls[] = 'failure'; + }) + ->group(function ($schedule) { + $schedule->command('inspire')->daily(); + $schedule->command('inspire')->weekly(); + }); + + $events = $schedule->events(); + $this->assertCount(2, $events); + $this->assertSame('0 0 * * *', $events[0]->expression); + $this->assertSame('0 0 * * 0', $events[1]->expression); + + $events[0]->callBeforeCallbacks(app()); + $events[0]->finish(app(), 0); + $events[1]->callBeforeCallbacks(app()); + $events[1]->finish(app(), 1); + + $this->assertSame(['before', 'success', 'before', 'failure'], $calls); + } + + public function testGroupCanStartWithOutputCallbackWithoutFrequency() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->onFailureWithOutput(function (Event $event, \Illuminate\Support\Stringable $output) use (&$calls) { + $calls[] = 'failure:'.$output; + }) + ->group(function ($schedule) { + $schedule->command('inspire')->daily(); + }); + + $events = $schedule->events(); + $this->assertCount(1, $events); + $this->assertSame('0 0 * * *', $events[0]->expression); + + $events[0]->finish(app(), 1); + + $this->assertCount(1, $calls); + } } From ff0dda12cd93f6d33353c54928c80f13e6b5eec2 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Wed, 20 May 2026 12:43:46 +0100 Subject: [PATCH 422/596] [13.x] Boot managed queues before service providers boot (#60198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Boot managed queues before service providers boot The `cloud` queue connector was registered on the `bootstrapperBootstrapped: BootProviders` event, meaning it fired after every service provider's `boot()` had already run. Application providers whose boot logic touches the queue (e.g. `Queue::createPayloadUsing(...)`, which proxies through `QueueManager::__call` and resolves the default connection) blow up under `QUEUE_CONNECTION=cloud` with "The [cloud] queue connection has not been configured", because the connector isn't registered yet. Move `bootManagedQueues()` to `bootstrapperBootstrapping: BootProviders` so it runs after `RegisterProviders` (queue/queue.failer are bound) but before any user provider's `boot()`. Also refactor the early-return guard. The previous check on `$_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']` overlapped with the env-driven config that `configureManagedQueues()` already populates. Derive intent from observable state instead: only register the cloud connector if `queue.connections.cloud.driver === 'cloud'`. The outer `laravel_cloud()` gate in `Application::registerLaravelCloudServices()` already prevents these hooks from firing off-Cloud. * Drop unused LARAVEL_CLOUD_MANAGED_QUEUES env from test setup No production code reads this env var anymore — the previous commit replaced its only consumer (the guard in `bootManagedQueues`) with a check on `queue.connections.cloud.driver`. The setUp/tearDown lines in `tests/Foundation/Cloud/QueueTest.php` are the only remaining references; remove them. `LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG` is a different env var and is still consumed by `configureManagedQueues()` — left in place. --- src/Illuminate/Foundation/Cloud.php | 12 +++++++----- tests/Foundation/Cloud/QueueTest.php | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index bdb7d0dbc8ee..7f9b98b5d5d8 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -20,7 +20,12 @@ class Cloud */ public static function bootstrapperBootstrapping(Application $app, string $bootstrapper): void { - // + (match ($bootstrapper) { + BootProviders::class => function () use ($app) { + static::bootManagedQueues($app); + }, + default => fn () => true, + })(); } /** @@ -38,9 +43,6 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); }, - BootProviders::class => function () use ($app) { - static::bootManagedQueues($app); - }, default => fn () => true, })(); } @@ -156,7 +158,7 @@ public static function configureManagedQueues(Application $app): void */ public static function bootManagedQueues(Application $app): void { - if (($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? '0') !== '1') { + if ($app['config']->get('queue.connections.cloud.driver') !== 'cloud') { return; } diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 112aecb31ebf..d1273b2aa372 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -50,7 +50,6 @@ protected function setUp(): void Worker::$restartable = true; Worker::$pausable = true; $_SERVER['LARAVEL_CLOUD'] = '1'; - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'] = json_encode([ 'driver' => 'cloud', 'connection' => [ @@ -71,7 +70,7 @@ protected function tearDown(): void { parent::tearDown(); - unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); Worker::$restartable = true; Worker::$pausable = true; } @@ -169,17 +168,28 @@ public function testItBindsTheQueueFailer() $this->assertInstanceOf(FailedJobProvider::class, $this->app['queue.failer']); } - public function testItDoesNotRegisterCloudConnectorWhenManagedQueuesIsInactive() + public function testItDoesNotRegisterCloudConnectorWhenCloudQueueConnectionIsNotConfigured() { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + $this->app['config']->set('queue.connections.cloud', null); Cloud::bootManagedQueues($this->app); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No connector for [cloud]'); + $this->expectExceptionMessage('The [cloud] queue connection has not been configured.'); $this->app['queue']->connection('cloud'); } + public function testItDoesNotRegisterCloudConnectorWhenCloudQueueConnectionDriverIsNotCloud() + { + $this->app['config']->set('queue.connections.cloud.driver', 'sqs'); + $originalFailer = $this->app['queue.failer']; + + Cloud::bootManagedQueues($this->app); + + $this->assertFalse($this->app->bound(Events::class)); + $this->assertSame($originalFailer, $this->app['queue.failer']); + } + public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() { $eventsFake = $this->fakeEvents(); From 0132e692b413b3fde246287aaba9cdaf7c36a493 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Wed, 20 May 2026 12:44:19 +0100 Subject: [PATCH 423/596] [12.x] Boot managed queues before service providers boot (#60199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Boot managed queues before service providers boot The `cloud` queue connector was registered on the `bootstrapperBootstrapped: BootProviders` event, meaning it fired after every service provider's `boot()` had already run. Application providers whose boot logic touches the queue (e.g. `Queue::createPayloadUsing(...)`, which proxies through `QueueManager::__call` and resolves the default connection) blow up under `QUEUE_CONNECTION=cloud` with "The [cloud] queue connection has not been configured", because the connector isn't registered yet. Move `bootManagedQueues()` to `bootstrapperBootstrapping: BootProviders` so it runs after `RegisterProviders` (queue/queue.failer are bound) but before any user provider's `boot()`. Also refactor the early-return guard. The previous check on `$_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']` overlapped with the env-driven config that `configureManagedQueues()` already populates. Derive intent from observable state instead: only register the cloud connector if `queue.connections.cloud.driver === 'cloud'`. The outer `laravel_cloud()` gate in `Application::registerLaravelCloudServices()` already prevents these hooks from firing off-Cloud. * Drop unused LARAVEL_CLOUD_MANAGED_QUEUES env from test setup No production code reads this env var anymore — the previous commit replaced its only consumer (the guard in `bootManagedQueues`) with a check on `queue.connections.cloud.driver`. The setUp/tearDown lines in `tests/Foundation/Cloud/QueueTest.php` are the only remaining references; remove them. `LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG` is a different env var and is still consumed by `configureManagedQueues()` — left in place. --- src/Illuminate/Foundation/Cloud.php | 12 +++++++----- tests/Foundation/Cloud/QueueTest.php | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index e4499da86e6b..c9a668b10d41 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -20,7 +20,12 @@ class Cloud */ public static function bootstrapperBootstrapping(Application $app, string $bootstrapper): void { - // + (match ($bootstrapper) { + BootProviders::class => function () use ($app) { + static::bootManagedQueues($app); + }, + default => fn () => true, + })(); } /** @@ -38,9 +43,6 @@ public static function bootstrapperBootstrapped(Application $app, string $bootst HandleExceptions::class => function () use ($app) { static::configureCloudLogging($app); }, - BootProviders::class => function () use ($app) { - static::bootManagedQueues($app); - }, default => fn () => true, })(); } @@ -148,7 +150,7 @@ public static function configureManagedQueues(Application $app): void */ public static function bootManagedQueues(Application $app): void { - if (($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] ?? '0') !== '1') { + if ($app['config']->get('queue.connections.cloud.driver') !== 'cloud') { return; } diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 112aecb31ebf..d1273b2aa372 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -50,7 +50,6 @@ protected function setUp(): void Worker::$restartable = true; Worker::$pausable = true; $_SERVER['LARAVEL_CLOUD'] = '1'; - $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'] = '1'; $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG'] = json_encode([ 'driver' => 'cloud', 'connection' => [ @@ -71,7 +70,7 @@ protected function tearDown(): void { parent::tearDown(); - unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); + unset($_SERVER['LARAVEL_CLOUD'], $_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES_CONFIG']); Worker::$restartable = true; Worker::$pausable = true; } @@ -169,17 +168,28 @@ public function testItBindsTheQueueFailer() $this->assertInstanceOf(FailedJobProvider::class, $this->app['queue.failer']); } - public function testItDoesNotRegisterCloudConnectorWhenManagedQueuesIsInactive() + public function testItDoesNotRegisterCloudConnectorWhenCloudQueueConnectionIsNotConfigured() { - unset($_SERVER['LARAVEL_CLOUD_MANAGED_QUEUES']); + $this->app['config']->set('queue.connections.cloud', null); Cloud::bootManagedQueues($this->app); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No connector for [cloud]'); + $this->expectExceptionMessage('The [cloud] queue connection has not been configured.'); $this->app['queue']->connection('cloud'); } + public function testItDoesNotRegisterCloudConnectorWhenCloudQueueConnectionDriverIsNotCloud() + { + $this->app['config']->set('queue.connections.cloud.driver', 'sqs'); + $originalFailer = $this->app['queue.failer']; + + Cloud::bootManagedQueues($this->app); + + $this->assertFalse($this->app->bound(Events::class)); + $this->assertSame($originalFailer, $this->app['queue.failer']); + } + public function testItDoesNotEmitEventsWhilePoppingWhenNoJobsAreProcessingAndNoJobsAreAvailableToPop() { $eventsFake = $this->fakeEvents(); From 4148042bf6ee01edd05408f1f66d91b231f85c25 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 20 May 2026 11:46:02 +0000 Subject: [PATCH 424/596] Update version to v13.11.2 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index ec16b31b447c..60aa0281f914 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 = '13.11.1'; + const VERSION = '13.11.2'; /** * The base path for the Laravel installation. From 54e3bc2b00c0addda6b205b3c74e86824e8142bf Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 20 May 2026 11:47:44 +0000 Subject: [PATCH 425/596] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb1aefe5683..77e4421bd931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.11.1...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.11.2...13.x) + +## [v13.11.2](https://github.com/laravel/framework/compare/v13.11.1...v13.11.2) - 2026-05-20 + +* [13.x] Fix lifecycle deferred event methods by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60190 +* [13.x] Boot managed queues before service providers boot by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60198 ## [v13.11.1](https://github.com/laravel/framework/compare/v13.11.0...v13.11.1) - 2026-05-19 From b8b55ce32175cc00f834a56eeb6316f18ed6ea39 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 20 May 2026 11:48:19 +0000 Subject: [PATCH 426/596] Update version to v12.60.2 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 9e8a716af998..5953f99211cb 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.60.1'; + const VERSION = '12.60.2'; /** * The base path for the Laravel installation. From e53dddd0b316f21d6dc0938d16652dc7ddbc1510 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 20 May 2026 11:49:56 +0000 Subject: [PATCH 427/596] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e567e33f51..b24daedd95cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.60.1...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.60.2...12.x) + +## [v12.60.2](https://github.com/laravel/framework/compare/v12.60.1...v12.60.2) - 2026-05-20 + +* [12.x] Boot managed queues before service providers boot by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60199 ## [v12.60.1](https://github.com/laravel/framework/compare/v12.60.0...v12.60.1) - 2026-05-19 From 77de82bc31006f9e3b4344b3d0a54b9d7b446e49 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Wed, 20 May 2026 13:15:36 +0100 Subject: [PATCH 428/596] Accept Symfony's new control-characters exception message in mailer test (#60202) `symfony/mime` 7.4.12 added an early control-character guard in `Address::__construct` that throws `InvalidArgumentException('Email address contains control characters.')` before the older `RfcComplianceException('Email addresses may not contain line break characters.')` ever fires. `testMailerRejectsSymfonyAddressesContainingLineBreaks` constructs `new Address("\"foo\r\nBcc: ...\"@example.com")` to trigger the rejection, so it now hits the new message instead of the old one. The composer constraint is `^7.4.0`, so both messages are reachable depending on which patch version a user has installed. Switch the test to a try/catch+assertContains pattern so it accepts either Symfony exception message as a valid outcome. The exception class (InvalidArgumentException) and the throw-required guarantee (via `$this->fail()`) are still pinned. This pattern is already used elsewhere in the test suite (e.g. tests/Cache/CacheRepositoryTest.php, tests/Auth/AuthHandlesAuthorizationTest.php). --- tests/Mail/MailMailerTest.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index 1f0eae2e2d10..a71d2926918c 100755 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -228,12 +228,18 @@ public function testMailerRejectsSymfonyAddressesContainingLineBreaks(): void $view->shouldReceive('render')->once()->andReturn('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Email addresses may not contain line break characters.'); - - $mailer->send('foo', ['data'], function (Message $message) { - $message->to(new Address("\"foo\r\nBcc: victim@example.com\"@example.com"))->from('hello@laravel.com'); - }); + try { + $mailer->send('foo', ['data'], function (Message $message) { + $message->to(new Address("\"foo\r\nBcc: victim@example.com\"@example.com"))->from('hello@laravel.com'); + }); + + $this->fail('Expected InvalidArgumentException was not thrown.'); + } catch (InvalidArgumentException $e) { + $this->assertContains($e->getMessage(), [ + 'Email address contains control characters.', + 'Email addresses may not contain line break characters.', + ]); + } } public function testGlobalFromIsRespectedOnAllMessages(): void From 073df149fbbb79ee690ceb0dd908f3bcb47df2a2 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 20 May 2026 13:16:20 +0100 Subject: [PATCH 429/596] Update Worker.php (#60201) test --- src/Illuminate/Queue/Worker.php | 9 ++++++++- tests/Queue/QueueWorkerTest.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index d92f2b4ee885..0ff2cb3b1ff5 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -139,6 +139,13 @@ class Worker */ public static $reportJobExceptions = true; + /** + * Indicates if the worker should stop when a lost connection is detected. + * + * @var bool + */ + public static $stopOnLostConnection = true; + /** * Indicates if the worker should check for the restart signal in the cache. * @@ -509,7 +516,7 @@ protected function runJob($job, $connectionName, WorkerOptions $options) */ protected function stopWorkerIfLostConnection($e) { - if ($this->causedByLostConnection($e)) { + if (static::$stopOnLostConnection && $this->causedByLostConnection($e)) { $this->lostConnection = true; } } diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index e41bedd908cf..3cf7e7f1170d 100755 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -558,6 +558,35 @@ public function testWorkerStopsWithLostConnectionReason() })); } + public function testWorkerDoesNotStopOnLostConnectionWhenDisabled() + { + $workerOptions = new WorkerOptions(); + $workerOptions->stopWhenEmpty = true; + + $worker = $this->getWorker('default', ['queue' => [ + $job = new WorkerFakeJob(function () { + throw new RuntimeException('server has gone away'); + }), + ]]); + + Worker::$stopOnLostConnection = false; + + try { + $worker->daemon('default', 'queue', $workerOptions); + } finally { + Worker::$stopOnLostConnection = true; + } + + $this->assertTrue($job->fired); + + $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { + return $event instanceof WorkerStopping + && $event->status === 0 + && $event->workerOptions === $workerOptions + && $event->reason === WorkerStopReason::QueueEmpty; + })); + } + public function testJobReleasedEvent() { $e = new RuntimeException; From c16fe0ca6fafaa08ba5ce4337c29d65e1627149a Mon Sep 17 00:00:00 2001 From: "Kay W." Date: Wed, 20 May 2026 20:16:52 +0800 Subject: [PATCH 430/596] [13.x] Resolve scheduled event callback parameter by type rather than name (#60197) * Resolve scheduled event callback parameter by type rather than name * Add regression test for non-Event-typed callback parameter injection --- src/Illuminate/Console/Scheduling/Event.php | 10 ++--- tests/Console/Scheduling/EventTest.php | 42 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 0dad9e51cbab..6463fb9575ae 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -791,13 +791,13 @@ protected function eventParametersForCallback(Closure $callback) { $parameters = $this->closureParameterTypes($callback); - $eventParameterType = Arr::get($parameters, 'event'); - - if ($eventParameterType === null || ! is_a($this, $eventParameterType)) { - return []; + foreach ($parameters as $name => $type) { + if ($type !== null && is_a($this, $type)) { + return [$name => $this]; + } } - return ['event' => $this]; + return []; } /** diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index b7e9f31ff19e..1ef62be6fd26 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -6,6 +6,7 @@ use Illuminate\Console\Scheduling\EventMutex; use Illuminate\Container\Container; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use Mockery as m; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; @@ -145,6 +146,47 @@ public function testFilterCallbacksCanReceiveEvent() $this->assertSame($event, $rejectEvent); } + public function testEventCallbackResolvesByTypeRegardlessOfParameterName() + { + $container = new Container; + $beforeEvent = null; + $filterEvent = null; + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->before(function (Event $scheduledEvent) use (&$beforeEvent) { + $beforeEvent = $scheduledEvent; + }); + + $event->when(function (Event $scheduledEvent) use (&$filterEvent) { + $filterEvent = $scheduledEvent; + + return true; + }); + + $event->callBeforeCallbacks($container); + $this->assertTrue($event->filtersPass($container)); + + $this->assertSame($event, $beforeEvent); + $this->assertSame($event, $filterEvent); + } + + public function testEventCallbackDoesNotInjectIntoUnrelatedTypedParameters() + { + $container = new Container; + $stringValue = null; + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $container->instance(Stringable::class, Str::of('injected-string')); + + $event->before(function (Stringable $value) use (&$stringValue) { + $stringValue = (string) $value; + }); + + $event->callBeforeCallbacks($container); + + $this->assertSame('injected-string', $stringValue); + } + public function testFilterCallbacksMayBeInvokableObjects() { $container = new Container; From 5ac671bed5ad7a7161b9838e900ce0156b670e81 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 20 May 2026 13:17:43 +0100 Subject: [PATCH 431/596] 13.x default clear() queue param to null (#60192) --- src/Illuminate/Queue/DatabaseQueue.php | 4 ++-- src/Illuminate/Queue/RedisQueue.php | 4 ++-- src/Illuminate/Queue/SqsQueue.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index ce56a58de0eb..4d341418175d 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -567,10 +567,10 @@ public function deleteAndRelease($queue, $job, $delay) /** * Delete all of the jobs from the queue. * - * @param string $queue + * @param string|null $queue * @return int */ - public function clear($queue) + public function clear($queue = null) { return $this->database->table($this->table) ->where('queue', $this->getQueue($queue)) diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index bb5777560c07..a464840c3272 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -511,10 +511,10 @@ public function deleteAndRelease($queue, $job, $delay) /** * Delete all of the jobs from the queue. * - * @param string $queue + * @param string|null $queue * @return int */ - public function clear($queue) + public function clear($queue = null) { $queue = $this->getQueueRedisKey($queue); diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index 975016bd1429..8aa7fe24f574 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -442,10 +442,10 @@ public function pop($queue = null) /** * Delete all of the jobs from the queue. * - * @param string $queue + * @param string|null $queue * @return int */ - public function clear($queue) + public function clear($queue = null) { return tap($this->size($queue), function () use ($queue) { $this->sqs->purgeQueue([ From cfc931c0894426a2a01c5822a2c36801259d0aaf Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 20 May 2026 13:18:07 +0100 Subject: [PATCH 432/596] fix path seperator being encoded (#60194) betterr tests just the one for now. simples clear test --- src/Illuminate/Filesystem/LocalFilesystemAdapter.php | 2 +- tests/Integration/Filesystem/ReceiveFileTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index 5ddb2f9c0ab0..a191db5c0de7 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -115,7 +115,7 @@ public function temporaryUploadUrl($path, $expiration, array $options = []) 'url' => $url->to($url->temporarySignedRoute( 'storage.'.$this->disk.'.upload', $expiration, - ['path' => rawurlencode($path), 'upload' => true], + ['path' => strtr(rawurlencode($path), ['%2F' => '/']), 'upload' => true], absolute: false )), 'headers' => [], diff --git a/tests/Integration/Filesystem/ReceiveFileTest.php b/tests/Integration/Filesystem/ReceiveFileTest.php index 8d7ac005dfe5..07c5a0e0501a 100644 --- a/tests/Integration/Filesystem/ReceiveFileTest.php +++ b/tests/Integration/Filesystem/ReceiveFileTest.php @@ -17,6 +17,7 @@ protected function setUp(): void Storage::delete([ 'receive-file-test.txt', 'receive-file-test.txt?pad=x', + 'nested/folder/receive-file-test.txt', ]); }); @@ -90,6 +91,14 @@ public function testItCanReceiveAFileWithUriDelimitersInThePath() Storage::assertMissing('receive-file-test.txt'); } + #[RequiresOperatingSystem('Linux|Darwin')] + public function testTemporaryUploadUrlPreservesPathSeparatorsInNestedPaths() + { + $result = Storage::temporaryUploadUrl('nested/folder/receive-file-test.txt', Carbon::now()->addMinute()); + + $this->assertStringContainsString('nested/folder/receive-file-test.txt', $result['url']); + } + #[RequiresOperatingSystem('Linux|Darwin')] public function testUriDelimitersInThePathCannotHideAnExpiredUploadUrl() { From 1d358cce7160071a8b5e18995f0ef62aab9e4b76 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Wed, 20 May 2026 13:28:02 +0100 Subject: [PATCH 433/596] Accept Symfony's new control-characters exception message in mailer test (#60203) `symfony/mime` 7.4.12 added an early control-character guard in `Address::__construct` that throws `InvalidArgumentException('Email address contains control characters.')` before the older `RfcComplianceException('Email addresses may not contain line break characters.')` ever fires. `testMailerRejectsSymfonyAddressesContainingLineBreaks` constructs `new Address("\"foo\r\nBcc: ...\"@example.com")` to trigger the rejection, so it now hits the new message instead of the old one. The composer constraint is `^7.4.0`, so both messages are reachable depending on which patch version a user has installed. Switch the test to a try/catch+assertContains pattern so it accepts either Symfony exception message as a valid outcome. The exception class (InvalidArgumentException) and the throw-required guarantee (via `$this->fail()`) are still pinned. This pattern is already used elsewhere in the test suite (e.g. tests/Cache/CacheRepositoryTest.php, tests/Auth/AuthHandlesAuthorizationTest.php). --- tests/Mail/MailMailerTest.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index 1f0eae2e2d10..a71d2926918c 100755 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -228,12 +228,18 @@ public function testMailerRejectsSymfonyAddressesContainingLineBreaks(): void $view->shouldReceive('render')->once()->andReturn('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Email addresses may not contain line break characters.'); - - $mailer->send('foo', ['data'], function (Message $message) { - $message->to(new Address("\"foo\r\nBcc: victim@example.com\"@example.com"))->from('hello@laravel.com'); - }); + try { + $mailer->send('foo', ['data'], function (Message $message) { + $message->to(new Address("\"foo\r\nBcc: victim@example.com\"@example.com"))->from('hello@laravel.com'); + }); + + $this->fail('Expected InvalidArgumentException was not thrown.'); + } catch (InvalidArgumentException $e) { + $this->assertContains($e->getMessage(), [ + 'Email address contains control characters.', + 'Email addresses may not contain line break characters.', + ]); + } } public function testGlobalFromIsRespectedOnAllMessages(): void From 1d3c6c59f78b390c46671ea597024f07043b93c9 Mon Sep 17 00:00:00 2001 From: Claudio Ludovico <921500+ludo237@users.noreply.github.com> Date: Wed, 20 May 2026 15:03:53 +0200 Subject: [PATCH 434/596] feat: add factory to pivot stub (#60204) model.pivot.stub doesn't include the template for factories but php artisan make:model allow the -p and -f flag to coexists --- src/Illuminate/Foundation/Console/stubs/model.pivot.stub | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/stubs/model.pivot.stub b/src/Illuminate/Foundation/Console/stubs/model.pivot.stub index 35a674ad2d0b..6c3bfc487e30 100644 --- a/src/Illuminate/Foundation/Console/stubs/model.pivot.stub +++ b/src/Illuminate/Foundation/Console/stubs/model.pivot.stub @@ -2,9 +2,10 @@ namespace {{ namespace }}; +{{ factoryImport }} use Illuminate\Database\Eloquent\Relations\Pivot; class {{ class }} extends Pivot { - // + {{ factory }} } From b9ee419ca57bc92d3982cc623cdfbae549d39dff Mon Sep 17 00:00:00 2001 From: RP SOHAG <66528080+rpsohag@users.noreply.github.com> Date: Thu, 21 May 2026 04:05:07 +0600 Subject: [PATCH 435/596] Fix incorrect type hint in Optional::offsetUnset() docblock (#60207) --- src/Illuminate/Support/Optional.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Optional.php b/src/Illuminate/Support/Optional.php index aefa943d8a0c..a845fdfe5e24 100644 --- a/src/Illuminate/Support/Optional.php +++ b/src/Illuminate/Support/Optional.php @@ -100,7 +100,7 @@ public function offsetSet($offset, $value): void /** * Unset the item at a given offset. * - * @param string $offset + * @param mixed $offset * @return void */ public function offsetUnset($offset): void From d033a4175f8b803db733a4154e89258654ae9e19 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 21 May 2026 14:14:57 +0100 Subject: [PATCH 436/596] Update ClearCommand.php (#60215) --- src/Illuminate/Queue/Console/ClearCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Console/ClearCommand.php b/src/Illuminate/Queue/Console/ClearCommand.php index 2ed23ffff590..cde9baad417b 100644 --- a/src/Illuminate/Queue/Console/ClearCommand.php +++ b/src/Illuminate/Queue/Console/ClearCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; +use Illuminate\Console\Prohibitable; use Illuminate\Contracts\Queue\ClearableQueue; use Illuminate\Support\Str; use ReflectionClass; @@ -14,7 +15,7 @@ #[AsCommand(name: 'queue:clear')] class ClearCommand extends Command { - use ConfirmableTrait; + use ConfirmableTrait, Prohibitable; /** * The console command name. @@ -37,7 +38,8 @@ class ClearCommand extends Command */ public function handle() { - if (! $this->confirmToProceed()) { + if ($this->isProhibited() || + ! $this->confirmToProceed()) { return 1; } From b96539caff11e62b0f80969062a5aef8f9bbbd22 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 21 May 2026 14:18:48 +0100 Subject: [PATCH 437/596] [13.x] Allow auto discovered listeners to opt out of discovery (#60209) * 13.x-ShouldBeDiscovered-listener import dis tests i changed da name * shorten that up * static --- .../Contracts/Events/ShouldBeDiscovered.php | 11 +++++++++++ .../Foundation/Events/DiscoverEvents.php | 6 ++++++ .../Foundation/DiscoverEventsTest.php | 16 ++++++++++++++++ .../RegisteredListener.php | 19 +++++++++++++++++++ .../SkippedListener.php | 19 +++++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 src/Illuminate/Contracts/Events/ShouldBeDiscovered.php create mode 100644 tests/Integration/Foundation/Fixtures/EventDiscovery/ShouldBeDiscoveredListeners/RegisteredListener.php create mode 100644 tests/Integration/Foundation/Fixtures/EventDiscovery/ShouldBeDiscoveredListeners/SkippedListener.php diff --git a/src/Illuminate/Contracts/Events/ShouldBeDiscovered.php b/src/Illuminate/Contracts/Events/ShouldBeDiscovered.php new file mode 100644 index 000000000000..eb7cea0a7042 --- /dev/null +++ b/src/Illuminate/Contracts/Events/ShouldBeDiscovered.php @@ -0,0 +1,11 @@ +implementsInterface(ShouldBeDiscovered::class) && + $listener->getName()::shouldBeDiscovered() === false) { + continue; + } + foreach ($listener->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if ((! Str::is('handle*', $method->name) && ! Str::is('__invoke', $method->name)) || ! isset($method->getParameters()[0])) { diff --git a/tests/Integration/Foundation/DiscoverEventsTest.php b/tests/Integration/Foundation/DiscoverEventsTest.php index bdf864b1f0eb..2646c81ca2bf 100644 --- a/tests/Integration/Foundation/DiscoverEventsTest.php +++ b/tests/Integration/Foundation/DiscoverEventsTest.php @@ -9,6 +9,8 @@ use Illuminate\Tests\Integration\Foundation\Fixtures\EventDiscovery\Listeners\AbstractListener; use Illuminate\Tests\Integration\Foundation\Fixtures\EventDiscovery\Listeners\Listener; use Illuminate\Tests\Integration\Foundation\Fixtures\EventDiscovery\Listeners\ListenerInterface; +use Illuminate\Tests\Integration\Foundation\Fixtures\EventDiscovery\ShouldBeDiscoveredListeners\RegisteredListener; +use Illuminate\Tests\Integration\Foundation\Fixtures\EventDiscovery\ShouldBeDiscoveredListeners\SkippedListener; use Illuminate\Tests\Integration\Foundation\Fixtures\EventDiscovery\UnionListeners\UnionListener; use Orchestra\Testbench\TestCase; use SplFileInfo; @@ -77,6 +79,20 @@ public function testMultipleDirectoriesCanBeDiscovered(): void ], $events); } + public function testListenersCanOptOutOfDiscovery() + { + class_alias(RegisteredListener::class, 'Tests\Integration\Foundation\Fixtures\EventDiscovery\ShouldBeDiscoveredListeners\RegisteredListener'); + class_alias(SkippedListener::class, 'Tests\Integration\Foundation\Fixtures\EventDiscovery\ShouldBeDiscoveredListeners\SkippedListener'); + + $events = DiscoverEvents::within(__DIR__.'/Fixtures/EventDiscovery/ShouldBeDiscoveredListeners', getcwd()); + + $this->assertEquals([ + EventOne::class => [ + RegisteredListener::class.'@handle', + ], + ], $events); + } + public function testNoExceptionForEmptyDirectories(): void { $events = DiscoverEvents::within([], getcwd()); diff --git a/tests/Integration/Foundation/Fixtures/EventDiscovery/ShouldBeDiscoveredListeners/RegisteredListener.php b/tests/Integration/Foundation/Fixtures/EventDiscovery/ShouldBeDiscoveredListeners/RegisteredListener.php new file mode 100644 index 000000000000..ea8384c87b5b --- /dev/null +++ b/tests/Integration/Foundation/Fixtures/EventDiscovery/ShouldBeDiscoveredListeners/RegisteredListener.php @@ -0,0 +1,19 @@ + Date: Fri, 22 May 2026 12:29:40 +0100 Subject: [PATCH 438/596] 13.x ensure up/down commands report (#60232) --- src/Illuminate/Foundation/Console/DownCommand.php | 2 ++ src/Illuminate/Foundation/Console/UpCommand.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index d9b1dd5faab2..efd3bc70c0de 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -67,6 +67,8 @@ public function handle() $this->components->info('You may bypass maintenance mode via ['.config('app.url')."/{$downFilePayload['secret']}]."); } } catch (Exception $e) { + report($e); + $this->components->error(sprintf( 'Failed to enter maintenance mode: %s.', $e->getMessage(), diff --git a/src/Illuminate/Foundation/Console/UpCommand.php b/src/Illuminate/Foundation/Console/UpCommand.php index c1b0e55c00b9..8bc9b54b29ff 100644 --- a/src/Illuminate/Foundation/Console/UpCommand.php +++ b/src/Illuminate/Foundation/Console/UpCommand.php @@ -48,6 +48,8 @@ public function handle() $this->components->info('Application is now live.'); } catch (Exception $e) { + report($e); + $this->components->error(sprintf( 'Failed to disable maintenance mode: %s.', $e->getMessage(), From e974ce64268629ec8c240785f3e86e002ad7f356 Mon Sep 17 00:00:00 2001 From: "Kay W." Date: Fri, 22 May 2026 19:31:17 +0800 Subject: [PATCH 439/596] Fix path separator encoding in temporaryUrl on local disk (#60230) --- .../Filesystem/LocalFilesystemAdapter.php | 2 +- tests/Integration/Filesystem/ServeFileTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index a191db5c0de7..dcfb30bc6f6c 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -82,7 +82,7 @@ public function temporaryUrl($path, $expiration, array $options = []) return $url->to($url->temporarySignedRoute( 'storage.'.$this->disk, $expiration, - ['path' => rawurlencode($path)], + ['path' => strtr(rawurlencode($path), ['%2F' => '/'])], absolute: false )); } diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php index efc2cb8c3d13..3fa70782254f 100644 --- a/tests/Integration/Filesystem/ServeFileTest.php +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -16,12 +16,14 @@ protected function setUp(): void $this->afterApplicationCreated(function () { Storage::put('serve-file-test.txt', 'Hello World'); Storage::put('serve-file-test.txt?pad=x', 'Hello Question'); + Storage::put('nested/folder/serve-file-test.txt', 'Hello Nested'); }); $this->beforeApplicationDestroyed(function () { Storage::delete([ 'serve-file-test.txt', 'serve-file-test.txt?pad=x', + 'nested/folder/serve-file-test.txt', ]); }); @@ -67,6 +69,18 @@ public function testItCanServeAFileWithUriDelimitersInThePath() $this->assertSame('Hello Question', $response->streamedContent()); } + #[RequiresOperatingSystem('Linux|Darwin')] + public function testTemporaryUrlPreservesPathSeparatorsInNestedPaths() + { + $url = Storage::temporaryUrl('nested/folder/serve-file-test.txt', Carbon::now()->addMinute()); + + $this->assertStringContainsString('nested/folder/serve-file-test.txt', $url); + + $response = $this->get($url); + + $this->assertSame('Hello Nested', $response->streamedContent()); + } + #[RequiresOperatingSystem('Linux|Darwin')] public function testUriDelimitersInThePathCannotHideAnExpiredUrl() { From 4253f5c6e76ed67492d0f8e88b2e37c6f7bccff2 Mon Sep 17 00:00:00 2001 From: Tresor-Kasenda <34010260+Tresor-Kasenda@users.noreply.github.com> Date: Fri, 22 May 2026 13:32:59 +0200 Subject: [PATCH 440/596] Add assertJsonPathsCanonicalizing to TestResponse (#60225) --- src/Illuminate/Testing/TestResponse.php | 14 +++++++++++++ tests/Testing/TestResponseTest.php | 28 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 93b4242c1646..214f5f45095c 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -889,6 +889,20 @@ public function assertJsonPaths(array $paths) return $this; } + /** + * Assert that the given paths in the response contain all of the expected values without looking at the order. + * + * @return $this + */ + public function assertJsonPathsCanonicalizing(array $paths) + { + foreach ($paths as $path => $expected) { + $this->assertJsonPathCanonicalizing($path, $expected); + } + + return $this; + } + /** * Assert that the given path in the response contains all of the expected values without looking at the order. * diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 95e9cc0c37f0..b2c6e3f6e160 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1593,6 +1593,34 @@ public function testAssertJsonPathCanonicalizingCanFail(): void $response->assertJsonPathCanonicalizing('*.foo', ['foo 0', 'foo 2', 'foo 3']); } + public function testAssertJsonPathsCanonicalizing(): void + { + $response = TestResponse::fromBaseResponse(new Response([ + 'data' => [ + ['id' => 10, 'name' => 'Taylor'], + ['id' => 20, 'name' => 'Mohamed'], + ['id' => 30, 'name' => 'Nuno'], + ], + ])); + + $response->assertJsonPathsCanonicalizing([ + 'data.*.id' => [30, 10, 20], + 'data.*.name' => ['Nuno', 'Taylor', 'Mohamed'], + ]); + } + + public function testAssertJsonPathsCanonicalizingCanFail(): void + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that two arrays are equal.'); + + $response->assertJsonPathsCanonicalizing([ + '*.foo' => ['foo 0', 'foo 2', 'foo 3'], + ]); + } + public function testAssertJsonPaths(): void { $response = TestResponse::fromBaseResponse(new Response([ From 88d7f88ed161caf93f39e9920994ebe79e0d182c Mon Sep 17 00:00:00 2001 From: Adam Campbell Date: Fri, 22 May 2026 07:34:33 -0400 Subject: [PATCH 441/596] Add normalize parameter to Str::studly() and Str::pascal() (#60229) Adds an optional `normalize: true` flag that lowercases all-uppercase word segments before conversion, so acronym-style strings like "CBOR" and "FMLS" produce "Cbor" and "Fmls" instead of being left unchanged. Without the flag the behaviour is identical to today, so this is fully backwards-compatible. --- src/Illuminate/Support/Str.php | 16 +++++++++++++--- src/Illuminate/Support/Stringable.php | 10 ++++++---- tests/Support/SupportStrTest.php | 8 ++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 9f31b9b135c8..5d2fd0caca7d 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1709,10 +1709,19 @@ public static function doesntStartWith($haystack, $needles) * Convert a value to studly caps case. * * @param string $value + * @param bool $normalize When true, all-uppercase words (e.g. acronyms) are lowercased before conversion so "CBOR" becomes "Cbor" instead of "CBOR". * @return ($value is '' ? '' : string) */ - public static function studly($value) + public static function studly($value, bool $normalize = false) { + if ($normalize) { + $value = preg_replace_callback( + '/(^|[-_ \s])([A-Z]+)(?=[-_ \s]|$)/u', + fn ($m) => $m[1].static::lower($m[2]), + $value + ); + } + $key = $value; if (isset(static::$studlyCache[$key])) { @@ -1730,11 +1739,12 @@ public static function studly($value) * Convert a value to Pascal case. * * @param string $value + * @param bool $normalize When true, all-uppercase words (e.g. acronyms) are lowercased before conversion so "CBOR" becomes "Cbor" instead of "CBOR". * @return ($value is '' ? '' : string) */ - public static function pascal($value) + public static function pascal($value, bool $normalize = false) { - return static::studly($value); + return static::studly($value, $normalize); } /** diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index fbb7fbda1f36..487ae9899064 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -983,21 +983,23 @@ public function doesntStartWith($needles) /** * Convert a value to studly caps case. * + * @param bool $normalize * @return static */ - public function studly() + public function studly(bool $normalize = false) { - return new static(Str::studly($this->value)); + return new static(Str::studly($this->value, $normalize)); } /** * Convert the string to Pascal case. * + * @param bool $normalize * @return static */ - public function pascal() + public function pascal(bool $normalize = false) { - return new static(Str::pascal($this->value)); + return new static(Str::pascal($this->value, $normalize)); } /** diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index f71919ae56ef..7ba137ee0932 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -1172,6 +1172,14 @@ public function testStudly() $this->assertSame('❤MultiByte☆', Str::studly('❤ multi-byte☆')); $this->assertSame('LaravelRocks!', Str::studly('laravel rocks!')); + + // normalize: true — all-uppercase words (acronyms) are treated as single words + $this->assertSame('Cbor', Str::studly('CBOR', normalize: true)); + $this->assertSame('Fmls', Str::studly('FMLS', normalize: true)); + $this->assertSame('AllCaps', Str::studly('ALL_CAPS', normalize: true)); + $this->assertSame('AllJersey', Str::studly('AllJersey', normalize: true)); + $this->assertSame('AllJersey', Str::studly('all_jersey', normalize: true)); + $this->assertSame('FooBar', Str::studly('foo_bar', normalize: true)); } public function testPascal() From f4ebdf965caec8815a962fb5ce1a6b39061dfceb Mon Sep 17 00:00:00 2001 From: Lucas Cavalheri Date: Fri, 22 May 2026 08:42:21 -0300 Subject: [PATCH 442/596] [13.x] Fix async HTTP retries when using array backoff values (#60214) * fix(http): support array backoff retries in async requests * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Http/Client/PendingRequest.php | 38 +++++++++++++++---- tests/Http/HttpClientTest.php | 34 +++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 4836d71609fe..c7f9e2950e4b 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1240,12 +1240,10 @@ protected function handlePromiseResponse(Response|Throwable $response, $method, return $exception; } - if ($attempt < $this->tries && $shouldRetry) { - $options['delay'] = value( - $this->retryDelay, - $attempt, - $response instanceof Response ? $response->toException() : $response - ); + $exception = $response instanceof Response ? $response->toException() : $response; + + if ($attempt < $this->getMaximumAttempts() && $shouldRetry) { + $options['delay'] = $this->retryDelayInMilliseconds($attempt, $exception); return $this->makePromise($method, $url, $options, $attempt + 1); } @@ -1260,13 +1258,39 @@ protected function handlePromiseResponse(Response|Throwable $response, $method, } } - if ($this->tries > 1 && $this->retryThrow) { + if ($this->getMaximumAttempts() > 1 && $this->retryThrow) { return $response instanceof Response ? $response->toException() : $response; } return $response; } + /** + * Get the maximum number of attempts for the request. + * + * @return int + */ + protected function getMaximumAttempts() + { + return is_array($this->tries) + ? count($this->tries) + 1 + : ($this->tries ?? 1); + } + + /** + * Get the delay in milliseconds before the next retry attempt. + * + * @param int $attempt + * @param mixed $exception + * @return int|float + */ + protected function retryDelayInMilliseconds($attempt, $exception) + { + return is_array($this->tries) + ? $this->tries[$attempt - 1] ?? 0 + : value($this->retryDelay ?? 100, $attempt, $exception); + } + /** * Send a request either synchronously or asynchronously. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index ef2860b5d28c..299229547493 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -2282,6 +2282,40 @@ public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhaustedWi $this->factory->assertSentCount(3); } + public function testAsyncRequestRetriesWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory::response(['error'], 403), + ]); + + $response = $this->factory + ->async() + ->retry([1, 2], throw: false) + ->get('http://foo.com/get') + ->wait(); + + $this->assertTrue($response->failed()); + + $this->factory->assertSentCount(3); + } + + public function testAsyncRequestRetriesWithIntegerTries() + { + $this->factory->fake([ + '*' => $this->factory::response(['error'], 403), + ]); + + $response = $this->factory + ->async() + ->retry(2, 1000, null, false) + ->get('http://foo.com/get') + ->wait(); + + $this->assertTrue($response->failed()); + + $this->factory->assertSentCount(2); + } + public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ From fb71d816c4530e3836ec1a80f48bceb9490a2de7 Mon Sep 17 00:00:00 2001 From: Benjamin Ayles Date: Sat, 23 May 2026 00:42:33 +0930 Subject: [PATCH 443/596] remove compact (#60234) --- src/Illuminate/Cache/DatabaseStore.php | 4 +- src/Illuminate/Cache/FileStore.php | 2 +- src/Illuminate/Cache/StorageStore.php | 2 +- .../Database/Concerns/BuildsQueries.php | 18 ++--- .../Concerns/BuildsWhereDateClauses.php | 2 +- src/Illuminate/Database/Connection.php | 2 +- .../Eloquent/Relations/BelongsToMany.php | 2 +- .../Database/Eloquent/Relations/MorphTo.php | 4 +- .../Database/Migrations/Migrator.php | 2 +- src/Illuminate/Database/Query/Builder.php | 74 +++++++++---------- src/Illuminate/Database/Schema/Blueprint.php | 64 ++++++++-------- src/Illuminate/Database/Schema/Builder.php | 2 +- src/Illuminate/Encryption/Encrypter.php | 2 +- .../Foundation/Http/FormRequest.php | 2 +- src/Illuminate/Log/LogManager.php | 2 +- src/Illuminate/Mail/Mailable.php | 6 +- src/Illuminate/Mail/Mailer.php | 8 +- .../Notifications/Messages/MailMessage.php | 4 +- .../Failed/DatabaseFailedJobProvider.php | 6 +- .../Routing/AbstractRouteCollection.php | 2 +- src/Illuminate/Routing/ResourceRegistrar.php | 4 +- .../Validation/Rules/DatabaseRule.php | 2 +- .../Validation/ValidationException.php | 2 +- 23 files changed, 109 insertions(+), 109 deletions(-) diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 61f100ac4ca1..2222d465ce94 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -222,11 +222,11 @@ public function add($key, $value, $seconds) $expiration = $this->getTime() + $seconds; if (! $this->getConnection() instanceof SqlServerConnection) { - return $this->table()->insertOrIgnore(compact('key', 'value', 'expiration')) > 0; + return $this->table()->insertOrIgnore(['key' => $key, 'value' => $value, 'expiration' => $expiration]) > 0; } try { - return $this->table()->insert(compact('key', 'value', 'expiration')); + return $this->table()->insert(['key' => $key, 'value' => $value, 'expiration' => $expiration]); } catch (QueryException) { // ... } diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index 66407b13b38b..4ac753de0cb0 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -381,7 +381,7 @@ protected function getPayload($key) // operation that may be performed on this cache on a later operation. $time = $expire - $this->currentTime(); - return compact('data', 'time'); + return ['data' => $data, 'time' => $time]; } /** diff --git a/src/Illuminate/Cache/StorageStore.php b/src/Illuminate/Cache/StorageStore.php index 2e46c88a36fe..956121d55985 100644 --- a/src/Illuminate/Cache/StorageStore.php +++ b/src/Illuminate/Cache/StorageStore.php @@ -225,7 +225,7 @@ protected function getPayload($key) $time = $expire - $this->currentTime(); - return compact('data', 'time'); + return ['data' => $data, 'time' => $time]; } /** diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index 541fb9b6e644..0f3295f10a2d 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -549,9 +549,9 @@ protected function getOriginalColumnNameForCursorPagination($builder, string $pa */ protected function paginator($items, $total, $perPage, $currentPage, $options) { - return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact( - 'items', 'total', 'perPage', 'currentPage', 'options' - )); + return Container::getInstance()->makeWith(LengthAwarePaginator::class, [ + 'items' => $items, 'total' => $total, 'perPage' => $perPage, 'currentPage' => $currentPage, 'options' => $options, + ]); } /** @@ -565,9 +565,9 @@ protected function paginator($items, $total, $perPage, $currentPage, $options) */ protected function simplePaginator($items, $perPage, $currentPage, $options) { - return Container::getInstance()->makeWith(Paginator::class, compact( - 'items', 'perPage', 'currentPage', 'options' - )); + return Container::getInstance()->makeWith(Paginator::class, [ + 'items' => $items, 'perPage' => $perPage, 'currentPage' => $currentPage, 'options' => $options, + ]); } /** @@ -581,9 +581,9 @@ protected function simplePaginator($items, $perPage, $currentPage, $options) */ protected function cursorPaginator($items, $perPage, $cursor, $options) { - return Container::getInstance()->makeWith(CursorPaginator::class, compact( - 'items', 'perPage', 'cursor', 'options' - )); + return Container::getInstance()->makeWith(CursorPaginator::class, [ + 'items' => $items, 'perPage' => $perPage, 'cursor' => $cursor, 'options' => $options, + ]); } /** diff --git a/src/Illuminate/Database/Concerns/BuildsWhereDateClauses.php b/src/Illuminate/Database/Concerns/BuildsWhereDateClauses.php index b96c535c6611..74a6d106f19c 100644 --- a/src/Illuminate/Database/Concerns/BuildsWhereDateClauses.php +++ b/src/Illuminate/Database/Concerns/BuildsWhereDateClauses.php @@ -109,7 +109,7 @@ protected function wherePastOrFuture($columns, $operator, $boolean) $value = Carbon::now(); foreach (Arr::wrap($columns) as $column) { - $this->wheres[] = compact('type', 'column', 'boolean', 'operator', 'value'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'boolean' => $boolean, 'operator' => $operator, 'value' => $value]; $this->addBinding($value); } diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index 0b729062f475..7b62c355946a 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -900,7 +900,7 @@ public function logQuery($query, $bindings, $time = null) : $query; if ($this->loggingQueries) { - $this->queryLog[] = compact('query', 'bindings', 'time', 'readWriteType'); + $this->queryLog[] = ['query' => $query, 'bindings' => $bindings, 'time' => $time, 'readWriteType' => $readWriteType]; } } diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index cd36196894ad..88cf8b89c38f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -493,7 +493,7 @@ public function withPivotValue($column, $value = null) throw new InvalidArgumentException('The provided value may not be null.'); } - $this->pivotValues[] = compact('column', 'value'); + $this->pivotValues[] = ['column' => $column, 'value' => $value]; return $this->wherePivot($column, '=', $value); } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index 2ca72e652183..94ae92b5937e 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -448,7 +448,7 @@ public function __call($method, $parameters) $result = parent::__call($method, $parameters); if (in_array($method, ['select', 'selectRaw', 'selectSub', 'addSelect', 'withoutGlobalScopes'])) { - $this->macroBuffer[] = compact('method', 'parameters'); + $this->macroBuffer[] = ['method' => $method, 'parameters' => $parameters]; } return $result; @@ -458,7 +458,7 @@ public function __call($method, $parameters) // we'll assume that we want to call a query macro (e.g. withTrashed) that only // exists on related models. We will just store the call and replay it later. catch (BadMethodCallException) { - $this->macroBuffer[] = compact('method', 'parameters'); + $this->macroBuffer[] = ['method' => $method, 'parameters' => $parameters]; return $this; } diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 0a110d685471..2f89c0ca10cf 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -389,7 +389,7 @@ protected function resetMigrations(array $migrations, array $paths, $pretend = f $migrations = (new Collection($migrations))->map(fn ($m) => (object) ['migration' => $m])->all(); return $this->rollbackMigrations( - $migrations, $paths, compact('pretend') + $migrations, $paths, ['pretend' => $pretend] ); } diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index e8d79787d2cd..9fcf09fd4328 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -931,7 +931,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' if ($column instanceof ConditionExpression) { $type = 'Expression'; - $this->wheres[] = compact('type', 'column', 'boolean'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'boolean' => $boolean]; return $this; } @@ -1016,9 +1016,9 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' // Now that we are working with just a simple query we can put the elements // in our array and add the query binding to our array of bindings that // will be bound to each SQL statements when it is finally executed. - $this->wheres[] = compact( - 'type', 'column', 'operator', 'value', 'boolean' - ); + $this->wheres[] = [ + 'type' => $type, 'column' => $column, 'operator' => $operator, 'value' => $value, 'boolean' => $boolean, + ]; if (! $value instanceof ExpressionContract) { $this->addBinding($this->flattenValue($value), 'where'); @@ -1188,9 +1188,9 @@ public function whereColumn($first, $operator = null, $second = null, $boolean = // once the query is about to be executed and run against the database. $type = 'Column'; - $this->wheres[] = compact( - 'type', 'first', 'operator', 'second', 'boolean' - ); + $this->wheres[] = [ + 'type' => $type, 'first' => $first, 'operator' => $operator, 'second' => $second, 'boolean' => $boolean, + ]; return $this; } @@ -1320,7 +1320,7 @@ public function whereLike($column, $value, $caseSensitive = false, $boolean = 'a { $type = 'Like'; - $this->wheres[] = compact('type', 'column', 'value', 'caseSensitive', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'value' => $value, 'caseSensitive' => $caseSensitive, 'boolean' => $boolean, 'not' => $not]; if (method_exists($this->grammar, 'prepareWhereLikeBinding')) { $value = $this->grammar->prepareWhereLikeBinding($value, $caseSensitive); @@ -1383,7 +1383,7 @@ public function whereNullSafeEquals($column, $value, $boolean = 'and') { $type = 'NullSafeEquals'; - $this->wheres[] = compact('type', 'column', 'value', 'boolean'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'value' => $value, 'boolean' => $boolean]; if (! $value instanceof ExpressionContract) { $this->addBinding($this->flattenValue($value), 'where'); @@ -1437,7 +1437,7 @@ public function whereIn($column, $values, $boolean = 'and', $not = false) $values = $values->toArray(); } - $this->wheres[] = compact('type', 'column', 'values', 'boolean'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'values' => $values, 'boolean' => $boolean]; if (count($values) !== count(Arr::flatten($values, 1))) { throw new InvalidArgumentException('Nested arrays may not be passed to whereIn method.'); @@ -1511,7 +1511,7 @@ public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = fal $value = (int) ($value instanceof BackedEnum ? $value->value : $value); } - $this->wheres[] = compact('type', 'column', 'values', 'boolean'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'values' => $values, 'boolean' => $boolean]; return $this; } @@ -1566,7 +1566,7 @@ public function whereNull($columns, $boolean = 'and', $not = false) $type = $not ? 'NotNull' : 'Null'; foreach (Arr::wrap($columns) as $column) { - $this->wheres[] = compact('type', 'column', 'boolean'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'boolean' => $boolean]; } return $this; @@ -1618,7 +1618,7 @@ public function whereBetween($column, iterable $values, $boolean = 'and', $not = $values = $this->resolveDatePeriodBounds($values); } - $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'values' => $values, 'boolean' => $boolean, 'not' => $not]; $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'where'); @@ -1644,7 +1644,7 @@ public function whereBetweenColumns($column, array $values, $boolean = 'and', $n ->whereBetweenColumns(new Expression('('.$sub.')'), $values, $boolean, $not); } - $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'values' => $values, 'boolean' => $boolean, 'not' => $not]; return $this; } @@ -1730,7 +1730,7 @@ public function whereValueBetween($value, array $columns, $boolean = 'and', $not { $type = 'valueBetween'; - $this->wheres[] = compact('type', 'value', 'columns', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'value' => $value, 'columns' => $columns, 'boolean' => $boolean, 'not' => $not]; $this->addBinding($value, 'where'); @@ -2045,7 +2045,7 @@ public function orWhereYear($column, $operator, $value = null) */ protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') { - $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value'); + $this->wheres[] = ['column' => $column, 'type' => $type, 'boolean' => $boolean, 'operator' => $operator, 'value' => $value]; if (! $value instanceof ExpressionContract) { $this->addBinding($value, 'where'); @@ -2089,7 +2089,7 @@ public function addNestedWhereQuery($query, $boolean = 'and') if (count($query->wheres)) { $type = 'Nested'; - $this->wheres[] = compact('type', 'query', 'boolean'); + $this->wheres[] = ['type' => $type, 'query' => $query, 'boolean' => $boolean]; $this->addBinding($query->getRawBindings()['where'], 'where'); } @@ -2119,9 +2119,9 @@ protected function whereSub($column, $operator, $callback, $boolean) $query = $callback instanceof EloquentBuilder ? $callback->toBase() : $callback; } - $this->wheres[] = compact( - 'type', 'column', 'operator', 'query', 'boolean' - ); + $this->wheres[] = [ + 'type' => $type, 'column' => $column, 'operator' => $operator, 'query' => $query, 'boolean' => $boolean, + ]; $this->addBinding($query->getBindings(), 'where'); @@ -2198,7 +2198,7 @@ public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false) { $type = $not ? 'NotExists' : 'Exists'; - $this->wheres[] = compact('type', 'query', 'boolean'); + $this->wheres[] = ['type' => $type, 'query' => $query, 'boolean' => $boolean]; $this->addBinding($query->getBindings(), 'where'); @@ -2224,7 +2224,7 @@ public function whereRowValues($columns, $operator, $values, $boolean = 'and') $type = 'RowValues'; - $this->wheres[] = compact('type', 'columns', 'operator', 'values', 'boolean'); + $this->wheres[] = ['type' => $type, 'columns' => $columns, 'operator' => $operator, 'values' => $values, 'boolean' => $boolean]; $this->addBinding($this->cleanBindings($values)); @@ -2257,7 +2257,7 @@ public function whereJsonContains($column, $value, $boolean = 'and', $not = fals { $type = 'JsonContains'; - $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'value' => $value, 'boolean' => $boolean, 'not' => $not]; if (! $value instanceof ExpressionContract) { $this->addBinding($this->grammar->prepareBindingForJsonContains($value)); @@ -2316,7 +2316,7 @@ public function whereJsonOverlaps($column, $value, $boolean = 'and', $not = fals { $type = 'JsonOverlaps'; - $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'value' => $value, 'boolean' => $boolean, 'not' => $not]; if (! $value instanceof ExpressionContract) { $this->addBinding($this->grammar->prepareBindingForJsonContains($value)); @@ -2374,7 +2374,7 @@ public function whereJsonContainsKey($column, $boolean = 'and', $not = false) { $type = 'JsonContainsKey'; - $this->wheres[] = compact('type', 'column', 'boolean', 'not'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'boolean' => $boolean, 'not' => $not]; return $this; } @@ -2437,7 +2437,7 @@ public function whereJsonLength($column, $operator, $value = null, $boolean = 'a [$value, $operator] = [$operator, '=']; } - $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); + $this->wheres[] = ['type' => $type, 'column' => $column, 'operator' => $operator, 'value' => $value, 'boolean' => $boolean]; if (! $value instanceof ExpressionContract) { $this->addBinding((int) $this->flattenValue($value)); @@ -2539,7 +2539,7 @@ public function whereFullText($columns, $value, array $options = [], $boolean = $columns = (array) $columns; - $this->wheres[] = compact('type', 'columns', 'value', 'options', 'boolean'); + $this->wheres[] = ['type' => $type, 'columns' => $columns, 'value' => $value, 'options' => $options, 'boolean' => $boolean]; $this->addBinding($value); @@ -2708,7 +2708,7 @@ public function having($column, $operator = null, $value = null, $boolean = 'and if ($column instanceof ConditionExpression) { $type = 'Expression'; - $this->havings[] = compact('type', 'column', 'boolean'); + $this->havings[] = ['type' => $type, 'column' => $column, 'boolean' => $boolean]; return $this; } @@ -2735,7 +2735,7 @@ public function having($column, $operator = null, $value = null, $boolean = 'and $type = 'Bitwise'; } - $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); + $this->havings[] = ['type' => $type, 'column' => $column, 'operator' => $operator, 'value' => $value, 'boolean' => $boolean]; if (! $value instanceof ExpressionContract) { $this->addBinding($this->flattenValue($value), 'having'); @@ -2786,7 +2786,7 @@ public function addNestedHavingQuery($query, $boolean = 'and') if (count($query->havings)) { $type = 'Nested'; - $this->havings[] = compact('type', 'query', 'boolean'); + $this->havings[] = ['type' => $type, 'query' => $query, 'boolean' => $boolean]; $this->addBinding($query->getRawBindings()['having'], 'having'); } @@ -2807,7 +2807,7 @@ public function havingNull($columns, $boolean = 'and', $not = false) $type = $not ? 'NotNull' : 'Null'; foreach (Arr::wrap($columns) as $column) { - $this->havings[] = compact('type', 'column', 'boolean'); + $this->havings[] = ['type' => $type, 'column' => $column, 'boolean' => $boolean]; } return $this; @@ -2863,7 +2863,7 @@ public function havingBetween($column, iterable $values, $boolean = 'and', $not $values = $this->resolveDatePeriodBounds($values); } - $this->havings[] = compact('type', 'column', 'values', 'boolean', 'not'); + $this->havings[] = ['type' => $type, 'column' => $column, 'values' => $values, 'boolean' => $boolean, 'not' => $not]; $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'having'); @@ -2941,7 +2941,7 @@ public function havingRaw($sql, array $bindings = [], $boolean = 'and') { $type = 'Raw'; - $this->havings[] = compact('type', 'sql', 'boolean'); + $this->havings[] = ['type' => $type, 'sql' => $sql, 'boolean' => $boolean]; $this->addBinding($bindings, 'having'); @@ -3114,7 +3114,7 @@ public function orderByRaw($sql, $bindings = []) { $type = 'Raw'; - $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'sql'); + $this->{$this->unions ? 'unionOrders' : 'orders'}[] = ['type' => $type, 'sql' => $sql]; $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order'); @@ -3185,7 +3185,7 @@ public function limit($value) public function groupLimit($value, $column) { if ($value >= 0) { - $this->groupLimit = compact('value', 'column'); + $this->groupLimit = ['value' => $value, 'column' => $column]; } return $this; @@ -3306,7 +3306,7 @@ public function union($query, $all = false) $query($query = $this->newQuery()); } - $this->unions[] = compact('query', 'all'); + $this->unions[] = ['query' => $query, 'all' => $all]; $this->addBinding($query->getBindings(), 'union'); @@ -4110,7 +4110,7 @@ public function numericAggregate($function, $columns = ['*']) */ protected function setAggregate($function, $columns) { - $this->aggregate = compact('function', 'columns'); + $this->aggregate = ['function' => $function, 'columns' => $columns]; if (empty($this->groups)) { $this->orders = null; diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 7f51787dd3e4..8584b29a0704 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -273,7 +273,7 @@ public function addFluentCommands() { foreach ($this->columns as $column) { foreach ($this->grammar->getFluentCommands() as $commandName) { - $this->addCommand($commandName, compact('column')); + $this->addCommand($commandName, ['column' => $column]); } } } @@ -422,7 +422,7 @@ public function dropColumn($columns) { $columns = is_array($columns) ? $columns : func_get_args(); - return $this->addCommand('dropColumn', compact('columns')); + return $this->addCommand('dropColumn', ['columns' => $columns]); } /** @@ -434,7 +434,7 @@ public function dropColumn($columns) */ public function renameColumn($from, $to) { - return $this->addCommand('renameColumn', compact('from', 'to')); + return $this->addCommand('renameColumn', ['from' => $from, 'to' => $to]); } /** @@ -557,7 +557,7 @@ public function dropConstrainedForeignIdFor($model, $column = null) */ public function renameIndex($from, $to) { - return $this->addCommand('renameIndex', compact('from', 'to')); + return $this->addCommand('renameIndex', ['from' => $from, 'to' => $to]); } /** @@ -634,7 +634,7 @@ public function dropMorphs($name, $indexName = null) */ public function rename($to) { - return $this->addCommand('rename', compact('to')); + return $this->addCommand('rename', ['to' => $to]); } /** @@ -832,7 +832,7 @@ public function char($column, $length = null) { $length = ! is_null($length) ? $length : Builder::$defaultStringLength; - return $this->addColumn('char', $column, compact('length')); + return $this->addColumn('char', $column, ['length' => $length]); } /** @@ -846,7 +846,7 @@ public function string($column, $length = null) { $length = $length ?: Builder::$defaultStringLength; - return $this->addColumn('string', $column, compact('length')); + return $this->addColumn('string', $column, ['length' => $length]); } /** @@ -904,7 +904,7 @@ public function longText($column) */ public function integer($column, $autoIncrement = false, $unsigned = false) { - return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned')); + return $this->addColumn('integer', $column, ['autoIncrement' => $autoIncrement, 'unsigned' => $unsigned]); } /** @@ -918,7 +918,7 @@ public function integer($column, $autoIncrement = false, $unsigned = false) */ public function tinyInteger($column, $autoIncrement = false, $unsigned = false) { - return $this->addColumn('tinyInteger', $column, compact('autoIncrement', 'unsigned')); + return $this->addColumn('tinyInteger', $column, ['autoIncrement' => $autoIncrement, 'unsigned' => $unsigned]); } /** @@ -932,7 +932,7 @@ public function tinyInteger($column, $autoIncrement = false, $unsigned = false) */ public function smallInteger($column, $autoIncrement = false, $unsigned = false) { - return $this->addColumn('smallInteger', $column, compact('autoIncrement', 'unsigned')); + return $this->addColumn('smallInteger', $column, ['autoIncrement' => $autoIncrement, 'unsigned' => $unsigned]); } /** @@ -946,7 +946,7 @@ public function smallInteger($column, $autoIncrement = false, $unsigned = false) */ public function mediumInteger($column, $autoIncrement = false, $unsigned = false) { - return $this->addColumn('mediumInteger', $column, compact('autoIncrement', 'unsigned')); + return $this->addColumn('mediumInteger', $column, ['autoIncrement' => $autoIncrement, 'unsigned' => $unsigned]); } /** @@ -960,7 +960,7 @@ public function mediumInteger($column, $autoIncrement = false, $unsigned = false */ public function bigInteger($column, $autoIncrement = false, $unsigned = false) { - return $this->addColumn('bigInteger', $column, compact('autoIncrement', 'unsigned')); + return $this->addColumn('bigInteger', $column, ['autoIncrement' => $autoIncrement, 'unsigned' => $unsigned]); } /** @@ -1098,7 +1098,7 @@ public function foreignUuidFor($model, $column = null) */ public function float($column, $precision = 53) { - return $this->addColumn('float', $column, compact('precision')); + return $this->addColumn('float', $column, ['precision' => $precision]); } /** @@ -1122,7 +1122,7 @@ public function double($column) */ public function decimal($column, $total = 8, $places = 2) { - return $this->addColumn('decimal', $column, compact('total', 'places')); + return $this->addColumn('decimal', $column, ['total' => $total, 'places' => $places]); } /** @@ -1147,7 +1147,7 @@ public function enum($column, array $allowed) { $allowed = array_map(fn ($value) => enum_value($value), $allowed); - return $this->addColumn('enum', $column, compact('allowed')); + return $this->addColumn('enum', $column, ['allowed' => $allowed]); } /** @@ -1159,7 +1159,7 @@ public function enum($column, array $allowed) */ public function set($column, array $allowed) { - return $this->addColumn('set', $column, compact('allowed')); + return $this->addColumn('set', $column, ['allowed' => $allowed]); } /** @@ -1206,7 +1206,7 @@ public function dateTime($column, $precision = null) { $precision ??= $this->defaultTimePrecision(); - return $this->addColumn('dateTime', $column, compact('precision')); + return $this->addColumn('dateTime', $column, ['precision' => $precision]); } /** @@ -1220,7 +1220,7 @@ public function dateTimeTz($column, $precision = null) { $precision ??= $this->defaultTimePrecision(); - return $this->addColumn('dateTimeTz', $column, compact('precision')); + return $this->addColumn('dateTimeTz', $column, ['precision' => $precision]); } /** @@ -1234,7 +1234,7 @@ public function time($column, $precision = null) { $precision ??= $this->defaultTimePrecision(); - return $this->addColumn('time', $column, compact('precision')); + return $this->addColumn('time', $column, ['precision' => $precision]); } /** @@ -1248,7 +1248,7 @@ public function timeTz($column, $precision = null) { $precision ??= $this->defaultTimePrecision(); - return $this->addColumn('timeTz', $column, compact('precision')); + return $this->addColumn('timeTz', $column, ['precision' => $precision]); } /** @@ -1262,7 +1262,7 @@ public function timestamp($column, $precision = null) { $precision ??= $this->defaultTimePrecision(); - return $this->addColumn('timestamp', $column, compact('precision')); + return $this->addColumn('timestamp', $column, ['precision' => $precision]); } /** @@ -1276,7 +1276,7 @@ public function timestampTz($column, $precision = null) { $precision ??= $this->defaultTimePrecision(); - return $this->addColumn('timestampTz', $column, compact('precision')); + return $this->addColumn('timestampTz', $column, ['precision' => $precision]); } /** @@ -1404,7 +1404,7 @@ public function year($column) */ public function binary($column, $length = null, $fixed = false) { - return $this->addColumn('binary', $column, compact('length', 'fixed')); + return $this->addColumn('binary', $column, ['length' => $length, 'fixed' => $fixed]); } /** @@ -1492,7 +1492,7 @@ public function macAddress($column = 'mac_address') */ public function geometry($column, $subtype = null, $srid = 0) { - return $this->addColumn('geometry', $column, compact('subtype', 'srid')); + return $this->addColumn('geometry', $column, ['subtype' => $subtype, 'srid' => $srid]); } /** @@ -1505,7 +1505,7 @@ public function geometry($column, $subtype = null, $srid = 0) */ public function geography($column, $subtype = null, $srid = 4326) { - return $this->addColumn('geography', $column, compact('subtype', 'srid')); + return $this->addColumn('geography', $column, ['subtype' => $subtype, 'srid' => $srid]); } /** @@ -1517,7 +1517,7 @@ public function geography($column, $subtype = null, $srid = 4326) */ public function computed($column, $expression) { - return $this->addColumn('computed', $column, compact('expression')); + return $this->addColumn('computed', $column, ['expression' => $expression]); } /** @@ -1529,7 +1529,7 @@ public function computed($column, $expression) */ public function vector($column, $dimensions = null) { - $options = $dimensions ? compact('dimensions') : []; + $options = $dimensions ? ['dimensions' => $dimensions] : []; return $this->addColumn('vector', $column, $options); } @@ -1722,7 +1722,7 @@ public function rememberToken() */ public function rawColumn($column, $definition) { - return $this->addColumn('raw', $column, compact('definition')); + return $this->addColumn('raw', $column, ['definition' => $definition]); } /** @@ -1733,7 +1733,7 @@ public function rawColumn($column, $definition) */ public function comment($comment) { - return $this->addCommand('tableComment', compact('comment')); + return $this->addCommand('tableComment', ['comment' => $comment]); } /** @@ -1756,7 +1756,7 @@ protected function indexCommand($type, $columns, $index, $algorithm = null, $ope $index = $index ?: $this->createIndexName($type, $columns); return $this->addCommand( - $type, compact('index', 'columns', 'algorithm', 'operatorClass') + $type, ['index' => $index, 'columns' => $columns, 'algorithm' => $algorithm, 'operatorClass' => $operatorClass] ); } @@ -1815,7 +1815,7 @@ protected function createIndexName($type, array $columns) public function addColumn($type, $name, array $parameters = []) { return $this->addColumnDefinition(new ColumnDefinition( - array_merge(compact('type', 'name'), $parameters) + array_merge(['type' => $type, 'name' => $name], $parameters) )); } @@ -1900,7 +1900,7 @@ protected function addCommand($name, array $parameters = []) */ protected function createCommand($name, array $parameters = []) { - return new Fluent(array_merge(compact('name'), $parameters)); + return new Fluent(array_merge(['name' => $name], $parameters)); } /** diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 2fae172a9405..1cd6289dfd46 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -723,7 +723,7 @@ protected function createBlueprint($table, ?Closure $callback = null) return call_user_func($this->resolver, $connection, $table, $callback); } - return Container::getInstance()->make(Blueprint::class, compact('connection', 'table', 'callback')); + return Container::getInstance()->make(Blueprint::class, ['connection' => $connection, 'table' => $table, 'callback' => $callback]); } /** diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 339ab5a8eb02..4c7d3b92ec8b 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -121,7 +121,7 @@ public function encrypt(#[\SensitiveParameter] $value, $serialize = true) ? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt... : $this->hash($iv, $value, $this->key); - $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES); + $json = json_encode(['iv' => $iv, 'value' => $value, 'mac' => $mac, 'tag' => $tag], JSON_UNESCAPED_SLASHES); if (json_last_error() !== JSON_ERROR_NONE) { throw new EncryptException('Could not encrypt the data.'); diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index bad072c3f9a9..60a833a4af59 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -102,7 +102,7 @@ protected function getValidatorInstance() $factory = $this->container->make(ValidationFactory::class); if (method_exists($this, 'validator')) { - $validator = $this->container->call($this->validator(...), compact('factory')); + $validator = $this->container->call($this->validator(...), ['factory' => $factory]); } else { $validator = $this->createDefaultValidator($factory); } diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 059a55e2c150..8fa7e6b45329 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -103,7 +103,7 @@ public function build(array $config) public function stack(array $channels, $channel = null) { return (new Logger( - $this->createStackDriver(compact('channels', 'channel')), + $this->createStackDriver(['channels' => $channels, 'channel' => $channel]), $this->app['events'] ))->withContext($this->sharedContext); } diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index d3c5048ddd77..d890c58dac02 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -811,7 +811,7 @@ protected function normalizeRecipient($recipient) if (is_array($recipient)) { if (array_values($recipient) === $recipient) { return (object) array_map(function ($email) { - return compact('email'); + return ['email' => $email]; }, $recipient); } @@ -1001,7 +1001,7 @@ public function attach($file, array $options = []) } $this->attachments = (new Collection($this->attachments)) - ->push(compact('file', 'options')) + ->push(['file' => $file, 'options' => $options]) ->unique('file') ->all(); @@ -1166,7 +1166,7 @@ public function hasAttachmentFromStorageDisk($disk, $path, $name = null, array $ public function attachData($data, $name, array $options = []) { $this->rawAttachments = (new Collection($this->rawAttachments)) - ->push(compact('data', 'name', 'options')) + ->push(['data' => $data, 'name' => $name, 'options' => $options]) ->unique(fn ($file) => $file['name'].$file['data']) ->all(); diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 75e02b74f504..6d67a7e1a88d 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -113,7 +113,7 @@ public function __construct(string $name, Factory $views, TransportInterface $tr */ public function alwaysFrom($address, $name = null) { - $this->from = compact('address', 'name'); + $this->from = ['address' => $address, 'name' => $name]; } /** @@ -125,7 +125,7 @@ public function alwaysFrom($address, $name = null) */ public function alwaysReplyTo($address, $name = null) { - $this->replyTo = compact('address', 'name'); + $this->replyTo = ['address' => $address, 'name' => $name]; } /** @@ -136,7 +136,7 @@ public function alwaysReplyTo($address, $name = null) */ public function alwaysReturnPath($address) { - $this->returnPath = compact('address'); + $this->returnPath = ['address' => $address]; } /** @@ -148,7 +148,7 @@ public function alwaysReturnPath($address) */ public function alwaysTo($address, $name = null) { - $this->to = compact('address', 'name'); + $this->to = ['address' => $address, 'name' => $name]; } /** diff --git a/src/Illuminate/Notifications/Messages/MailMessage.php b/src/Illuminate/Notifications/Messages/MailMessage.php index f65622863d76..52a801c9524b 100644 --- a/src/Illuminate/Notifications/Messages/MailMessage.php +++ b/src/Illuminate/Notifications/Messages/MailMessage.php @@ -273,7 +273,7 @@ public function attach($file, array $options = []) return $file->attachTo($this); } - $this->attachments[] = compact('file', 'options'); + $this->attachments[] = ['file' => $file, 'options' => $options]; return $this; } @@ -307,7 +307,7 @@ public function attachMany($files) */ public function attachData($data, $name, array $options = []) { - $this->rawAttachments[] = compact('data', 'name', 'options'); + $this->rawAttachments[] = ['data' => $data, 'name' => $name, 'options' => $options]; return $this; } diff --git a/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php b/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php index d7f3f6b35c98..ec16cbae4485 100644 --- a/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php +++ b/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php @@ -58,9 +58,9 @@ public function log($connection, $queue, $payload, $exception) $exception = (string) mb_convert_encoding($exception, 'UTF-8'); - return $this->getTable()->insertGetId(compact( - 'connection', 'queue', 'payload', 'exception', 'failed_at' - )); + return $this->getTable()->insertGetId([ + 'connection' => $connection, 'queue' => $queue, 'payload' => $payload, 'exception' => $exception, 'failed_at' => $failed_at, + ]); } /** diff --git a/src/Illuminate/Routing/AbstractRouteCollection.php b/src/Illuminate/Routing/AbstractRouteCollection.php index 6f7c73c61ee2..5a943a7436dd 100644 --- a/src/Illuminate/Routing/AbstractRouteCollection.php +++ b/src/Illuminate/Routing/AbstractRouteCollection.php @@ -187,7 +187,7 @@ public function compile() ]; } - return compact('compiled', 'attributes'); + return ['compiled' => $compiled, 'attributes' => $attributes]; } /** diff --git a/src/Illuminate/Routing/ResourceRegistrar.php b/src/Illuminate/Routing/ResourceRegistrar.php index 82ccc1620b83..07ff5412832c 100644 --- a/src/Illuminate/Routing/ResourceRegistrar.php +++ b/src/Illuminate/Routing/ResourceRegistrar.php @@ -217,7 +217,7 @@ protected function prefixedResource($name, $controller, array $options) $me->resource($name, $controller, $options); }; - return $this->router->group(compact('prefix'), $callback); + return $this->router->group(['prefix' => $prefix], $callback); } /** @@ -239,7 +239,7 @@ protected function prefixedSingleton($name, $controller, array $options) $me->singleton($name, $controller, $options); }; - return $this->router->group(compact('prefix'), $callback); + return $this->router->group(['prefix' => $prefix], $callback); } /** diff --git a/src/Illuminate/Validation/Rules/DatabaseRule.php b/src/Illuminate/Validation/Rules/DatabaseRule.php index bc879ee0ee18..ba723afd889e 100644 --- a/src/Illuminate/Validation/Rules/DatabaseRule.php +++ b/src/Illuminate/Validation/Rules/DatabaseRule.php @@ -102,7 +102,7 @@ public function where($column, $value = null) $value = enum_value($value); - $this->wheres[] = compact('column', 'value'); + $this->wheres[] = ['column' => $column, 'value' => $value]; return $this; } diff --git a/src/Illuminate/Validation/ValidationException.php b/src/Illuminate/Validation/ValidationException.php index ea46eb5ad95c..25ccc28768f3 100644 --- a/src/Illuminate/Validation/ValidationException.php +++ b/src/Illuminate/Validation/ValidationException.php @@ -95,7 +95,7 @@ protected static function summarize($validator) if ($count = count($messages)) { $pluralized = $count === 1 ? 'error' : 'errors'; - $message .= ' '.$validator->getTranslator()->choice("(and :count more $pluralized)", $count, compact('count')); + $message .= ' '.$validator->getTranslator()->choice("(and :count more $pluralized)", $count, ['count' => $count]); } return $message; From d16659821e26c36f1d603b4a9bfa651925dbc1c5 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 22 May 2026 16:13:03 +0100 Subject: [PATCH 444/596] [13.x] Add prohibited to KeyGenerateCommand (#60224) * Update KeyGenerateCommand.php move about Revert "move about" This reverts commit 3708f3c852d1447f7bb0d08ee02b4ddca275f2b1. Revert "Update KeyGenerateCommand.php" This reverts commit 5f832f2b99b333b2c255c02b6ee2096e36ddd6a3. make it super non b/c * human * cs --- src/Illuminate/Foundation/Console/KeyGenerateCommand.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/KeyGenerateCommand.php b/src/Illuminate/Foundation/Console/KeyGenerateCommand.php index 467fd2acda33..74f5362e26d5 100644 --- a/src/Illuminate/Foundation/Console/KeyGenerateCommand.php +++ b/src/Illuminate/Foundation/Console/KeyGenerateCommand.php @@ -4,13 +4,14 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; +use Illuminate\Console\Prohibitable; use Illuminate\Encryption\Encrypter; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'key:generate')] class KeyGenerateCommand extends Command { - use ConfirmableTrait; + use ConfirmableTrait, Prohibitable; /** * The name and signature of the console command. @@ -35,6 +36,10 @@ class KeyGenerateCommand extends Command */ public function handle() { + if ($this->isProhibited()) { + return; + } + $key = $this->generateRandomKey(); if ($this->option('show')) { From 012ea921cffba83dfdd465a6f2aec513e0d1cada Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Sat, 23 May 2026 18:36:34 -0500 Subject: [PATCH 445/596] remove last `compact()` call (#60235) this is a follow up to #60234, and removes the final use of `compact()` in the framework --- tests/Auth/AuthPasswordBrokerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Auth/AuthPasswordBrokerTest.php b/tests/Auth/AuthPasswordBrokerTest.php index 244dd2435f60..e2ba9fee2ff3 100755 --- a/tests/Auth/AuthPasswordBrokerTest.php +++ b/tests/Auth/AuthPasswordBrokerTest.php @@ -95,7 +95,7 @@ public function testResetRemovesRecordOnReminderTableAndCallsCallback() $broker->shouldReceive('validateReset')->once()->andReturn($user = m::mock(CanResetPassword::class)); $mocks['tokens']->shouldReceive('delete')->once()->with($user); $callback = function ($user, $password) { - $_SERVER['__password.reset.test'] = compact('user', 'password'); + $_SERVER['__password.reset.test'] = ['user' => $user, 'password' => $password]; return 'foo'; }; From 85cde9ce68a8818f91443264416ad2547389eb2e Mon Sep 17 00:00:00 2001 From: Lucas Cavalheri Date: Sat, 23 May 2026 20:37:51 -0300 Subject: [PATCH 446/596] allow fluent flags to be unset (#60239) --- src/Illuminate/JsonSchema/Types/ArrayType.php | 4 +-- src/Illuminate/JsonSchema/Types/Type.php | 8 ++--- tests/JsonSchema/ArrayTypeTest.php | 9 ++++++ tests/JsonSchema/TypeTest.php | 29 +++++++++++++++++++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/JsonSchema/Types/ArrayType.php b/src/Illuminate/JsonSchema/Types/ArrayType.php index 7059cc264a84..65f49c00eb9a 100644 --- a/src/Illuminate/JsonSchema/Types/ArrayType.php +++ b/src/Illuminate/JsonSchema/Types/ArrayType.php @@ -59,9 +59,7 @@ public function items(Type $type): static */ public function unique(bool $unique = true): static { - if ($unique) { - $this->uniqueItems = true; - } + $this->uniqueItems = $unique ?: null; return $this; } diff --git a/src/Illuminate/JsonSchema/Types/Type.php b/src/Illuminate/JsonSchema/Types/Type.php index 4443342dd5de..021e271c969e 100644 --- a/src/Illuminate/JsonSchema/Types/Type.php +++ b/src/Illuminate/JsonSchema/Types/Type.php @@ -46,9 +46,7 @@ abstract class Type extends JsonSchema */ public function required(bool $required = true): static { - if ($required) { - $this->required = true; - } + $this->required = $required ?: null; return $this; } @@ -58,9 +56,7 @@ public function required(bool $required = true): static */ public function nullable(bool $nullable = true): static { - if ($nullable) { - $this->nullable = true; - } + $this->nullable = $nullable ?: null; return $this; } diff --git a/tests/JsonSchema/ArrayTypeTest.php b/tests/JsonSchema/ArrayTypeTest.php index c7b1b4601cf5..3473e6205618 100644 --- a/tests/JsonSchema/ArrayTypeTest.php +++ b/tests/JsonSchema/ArrayTypeTest.php @@ -67,6 +67,15 @@ public function test_it_may_set_unique_items(): void ], $type->toArray()); } + public function test_it_may_unset_unique_items(): void + { + $type = JsonSchema::array()->unique()->unique(false); + + $this->assertEquals([ + 'type' => 'array', + ], $type->toArray()); + } + public function test_it_may_combine_unique_items_with_min_and_max(): void { $type = JsonSchema::array()->min(1)->max(5)->unique(); diff --git a/tests/JsonSchema/TypeTest.php b/tests/JsonSchema/TypeTest.php index 5797cd2fce30..7e897b8f9844 100644 --- a/tests/JsonSchema/TypeTest.php +++ b/tests/JsonSchema/TypeTest.php @@ -107,6 +107,35 @@ public function test_types_in_object_schema(): void $this->assertInstanceOf(JsonSchema::class, $schema); } + public function test_required_may_be_unset(): void + { + $schema = JsonSchema::object([ + 'name' => JsonSchema::string()->required()->required(false), + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + ], + ], $schema->toArray()); + + $this->assertValidOnJsonSchema($schema, (object) []); + } + + public function test_nullable_may_be_unset(): void + { + $schema = JsonSchema::string()->nullable()->nullable(false); + + $this->assertEquals([ + 'type' => 'string', + ], $schema->toArray()); + + $this->assertNotValidOnJsonSchema($schema, null); + } + public function test_throws_with_invalid_enum_string(): void { $this->expectException(InvalidArgumentException::class); From 3e7df8b7ed92de7439b37204db7073e98fe487ab Mon Sep 17 00:00:00 2001 From: Devon Garbalosa <58236685+DGarbs51@users.noreply.github.com> Date: Sat, 23 May 2026 19:55:45 -0400 Subject: [PATCH 447/596] [13.x] battle harden when scheme is present in the config (#60237) * battle harden when scheme is present in the config * additional checks for validation of host * apply similar logic to predis connection for consistency --- .../Redis/Connectors/PhpRedisConnector.php | 20 ++++- .../Redis/Connectors/PredisConnector.php | 42 +++++++-- tests/Redis/PhpRedisConnectorTest.php | 73 ++++++++++++++++ tests/Redis/PredisConnectorTest.php | 86 +++++++++++++++++++ 4 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 tests/Redis/PredisConnectorTest.php diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 52da4c019511..071e8212f580 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -267,11 +267,27 @@ protected function createRedisClusterInstance(array $servers, array $options) */ protected function formatHost(array $options) { + $host = $options['host'] ?? null; + + if (! is_string($host) || $host === '') { + throw new InvalidArgumentException('Redis host must be a non-empty string.'); + } + + $hostScheme = parse_url($host, PHP_URL_SCHEME); + if (isset($options['scheme'])) { - return Str::start($options['host'], "{$options['scheme']}://"); + if (is_string($hostScheme)) { + if (strcasecmp($hostScheme, $options['scheme']) !== 0) { + throw new InvalidArgumentException('The scheme configured in the Redis host option must match the scheme option.'); + } + + return $host; + } + + return Str::start($host, "{$options['scheme']}://"); } - return $options['host']; + return $host; } /** diff --git a/src/Illuminate/Redis/Connectors/PredisConnector.php b/src/Illuminate/Redis/Connectors/PredisConnector.php index 50fc39462ce0..c805d1f17fe6 100644 --- a/src/Illuminate/Redis/Connectors/PredisConnector.php +++ b/src/Illuminate/Redis/Connectors/PredisConnector.php @@ -7,6 +7,7 @@ use Illuminate\Redis\Connections\PredisConnection; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use InvalidArgumentException; use Predis\Client; class PredisConnector implements Connector @@ -28,10 +29,7 @@ public function connect(array $config, array $options) $formattedOptions['prefix'] = $config['prefix']; } - if (isset($config['host']) && str_starts_with($config['host'], 'tls://')) { - $config['scheme'] = 'tls'; - $config['host'] = Str::after($config['host'], 'tls://'); - } + $config = $this->formatHost($config); return new PredisConnection(new Client($config, $formattedOptions)); } @@ -52,8 +50,42 @@ public function connectToCluster(array $config, array $clusterOptions, array $op $clusterSpecificOptions['prefix'] = $config['prefix']; } - return new PredisClusterConnection(new Client(array_values($config), array_merge( + $servers = array_map(function ($server) { + return is_array($server) ? $this->formatHost($server) : $server; + }, array_values($config)); + + return new PredisClusterConnection(new Client($servers, array_merge( $options, $clusterOptions, $clusterSpecificOptions ))); } + + /** + * Format the host using the scheme if available. + * + * @param array $config + * @return array + */ + protected function formatHost(array $config) + { + $host = $config['host'] ?? null; + + if (! is_string($host) || $host === '') { + return $config; + } + + $hostScheme = parse_url($host, PHP_URL_SCHEME); + + if (! is_string($hostScheme)) { + return $config; + } + + if (isset($config['scheme']) && strcasecmp($hostScheme, $config['scheme']) !== 0) { + throw new InvalidArgumentException('The scheme configured in the Redis host option must match the scheme option.'); + } + + $config['scheme'] = $config['scheme'] ?? $hostScheme; + $config['host'] = Str::after($host, "{$hostScheme}://"); + + return $config; + } } diff --git a/tests/Redis/PhpRedisConnectorTest.php b/tests/Redis/PhpRedisConnectorTest.php index bddcaf014bbd..57dd79b66bbf 100644 --- a/tests/Redis/PhpRedisConnectorTest.php +++ b/tests/Redis/PhpRedisConnectorTest.php @@ -226,6 +226,74 @@ public function testParseBackoffAlgorithmThrowsForInvalidName() $connector->testParseBackoffAlgorithm('bogus'); } + + public function testFormatHostPrefixesConfiguredSchemeWhenHostHasNoScheme() + { + $connector = new TestablePhpRedisConnector; + + $this->assertSame('tls://127.0.0.1', $connector->testFormatHost([ + 'host' => '127.0.0.1', + 'scheme' => 'tls', + ])); + } + + public function testFormatHostDoesNotDuplicateMatchingScheme() + { + $connector = new TestablePhpRedisConnector; + + $this->assertSame('tls://127.0.0.1', $connector->testFormatHost([ + 'host' => 'tls://127.0.0.1', + 'scheme' => 'tls', + ])); + } + + public function testFormatHostThrowsOnConflictingScheme() + { + $connector = new TestablePhpRedisConnector; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The scheme configured in the Redis host option must match the scheme option.'); + + $connector->testFormatHost([ + 'host' => 'tcp://127.0.0.1', + 'scheme' => 'tls', + ]); + } + + public function testFormatHostAllowsCaseInsensitiveMatchingScheme() + { + $connector = new TestablePhpRedisConnector; + + $this->assertSame('TLS://127.0.0.1', $connector->testFormatHost([ + 'host' => 'TLS://127.0.0.1', + 'scheme' => 'tls', + ])); + } + + public function testFormatHostThrowsWhenHostIsMissing() + { + $connector = new TestablePhpRedisConnector; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Redis host must be a non-empty string.'); + + $connector->testFormatHost([ + 'scheme' => 'tls', + ]); + } + + public function testFormatHostThrowsWhenHostIsNull() + { + $connector = new TestablePhpRedisConnector; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Redis host must be a non-empty string.'); + + $connector->testFormatHost([ + 'host' => null, + 'scheme' => 'tls', + ]); + } } class TestablePhpRedisConnector extends PhpRedisConnector @@ -249,4 +317,9 @@ public function testParseBackoffAlgorithm(mixed $algorithm): int { return $this->parseBackoffAlgorithm($algorithm); } + + public function testFormatHost(array $options): string + { + return $this->formatHost($options); + } } diff --git a/tests/Redis/PredisConnectorTest.php b/tests/Redis/PredisConnectorTest.php new file mode 100644 index 000000000000..4c1452f4af6a --- /dev/null +++ b/tests/Redis/PredisConnectorTest.php @@ -0,0 +1,86 @@ + 'tls']; + + $this->assertSame($config, $connector->testFormatHost($config)); + } + + public function testFormatHostLeavesConfigUnchangedWhenHostHasNoScheme() + { + $connector = new TestablePredisConnector; + + $config = ['host' => '127.0.0.1', 'scheme' => 'tls']; + + $this->assertSame($config, $connector->testFormatHost($config)); + } + + public function testFormatHostUsesHostSchemeWhenSchemeNotConfigured() + { + $connector = new TestablePredisConnector; + + $this->assertSame([ + 'host' => '127.0.0.1', + 'scheme' => 'tls', + ], $connector->testFormatHost([ + 'host' => 'tls://127.0.0.1', + ])); + } + + public function testFormatHostKeepsExplicitSchemeWhenMatchingHostScheme() + { + $connector = new TestablePredisConnector; + + $this->assertSame([ + 'host' => '127.0.0.1', + 'scheme' => 'tls', + ], $connector->testFormatHost([ + 'host' => 'tls://127.0.0.1', + 'scheme' => 'tls', + ])); + } + + public function testFormatHostAcceptsCaseInsensitiveMatchingScheme() + { + $connector = new TestablePredisConnector; + + $this->assertSame([ + 'host' => '127.0.0.1', + 'scheme' => 'TLS', + ], $connector->testFormatHost([ + 'host' => 'tls://127.0.0.1', + 'scheme' => 'TLS', + ])); + } + + public function testFormatHostThrowsOnConflictingScheme() + { + $connector = new TestablePredisConnector; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The scheme configured in the Redis host option must match the scheme option.'); + + $connector->testFormatHost([ + 'host' => 'tcp://127.0.0.1', + 'scheme' => 'tls', + ]); + } +} + +class TestablePredisConnector extends PredisConnector +{ + public function testFormatHost(array $config): array + { + return $this->formatHost($config); + } +} From d7dcc2680f09317381e3d0b46f20758cfb41ebac Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Sun, 24 May 2026 01:58:38 +0200 Subject: [PATCH 448/596] Rector : Always convert compact() to variables (#60236) Co-authored-by: Lucas Michot --- rector.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rector.php b/rector.php index 9abfdd45c33c..88e4e9f33336 100644 --- a/rector.php +++ b/rector.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector; use Rector\CodeQuality\Rector\FuncCall\SortCallLikeNamedArgsRector; use Rector\CodeQuality\Rector\Identical\StrlenZeroToIdenticalEmptyStringRector; use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; @@ -123,6 +124,7 @@ ]) ->withRules([ ...$testsuiteRules, + CompactToVariablesRector::class, CountArrayToEmptyArrayComparisonRector::class, SortCallLikeNamedArgsRector::class, StrlenZeroToIdenticalEmptyStringRector::class, From d8b5e464c0fdfa221f07aabd0ca3458064617b6b Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sun, 24 May 2026 18:42:26 -0400 Subject: [PATCH 449/596] [13.x] Add attributes to Scheduler (#60255) * scheduler attributes * avoid doubling attributes * stop double replaying events * test lifecycle --- .../Console/Scheduling/ManagesAttributes.php | 20 ++++ .../Scheduling/PendingEventAttributes.php | 4 + .../Console/Scheduling/Schedule.php | 14 ++- tests/Console/Scheduling/ScheduleTest.php | 27 +++++ .../Console/Scheduling/ScheduleGroupTest.php | 103 ++++++++++++++++++ 5 files changed, 162 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/ManagesAttributes.php b/src/Illuminate/Console/Scheduling/ManagesAttributes.php index d541dc0ee899..4d12fb55fb18 100644 --- a/src/Illuminate/Console/Scheduling/ManagesAttributes.php +++ b/src/Illuminate/Console/Scheduling/ManagesAttributes.php @@ -111,6 +111,13 @@ trait ManagesAttributes */ public $description; + /** + * The arbitrary attributes stored with the event. + * + * @var array + */ + public $attributes = []; + /** * Set which user the command should run as. * @@ -260,4 +267,17 @@ public function description($description) return $this; } + + /** + * Set arbitrary attributes to store with the event. + * + * @param array $attributes + * @return $this + */ + public function withAttributes($attributes) + { + $this->attributes = array_merge_recursive($this->attributes, $attributes); + + return $this; + } } diff --git a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php index 74a61859ac06..66996c863ff7 100644 --- a/src/Illuminate/Console/Scheduling/PendingEventAttributes.php +++ b/src/Illuminate/Console/Scheduling/PendingEventAttributes.php @@ -85,6 +85,10 @@ public function mergeAttributes(Event $event): void $event->name($this->description); } + if ($this->attributes !== []) { + $event->attributes = $this->attributes; + } + if ($this->timezone !== null) { $event->timezone($this->timezone); } diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 4ce9126f244a..1956160421d1 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -336,16 +336,18 @@ public function group(Closure $events) */ protected function mergePendingAttributes(Event $event) { - if (! empty($this->groupStack)) { - $group = array_last($this->groupStack); - - $group->mergeAttributes($event); - } - if (isset($this->attributes)) { $this->attributes->mergeAttributes($event); $this->attributes = null; + + return; + } + + if (! empty($this->groupStack)) { + $group = array_last($this->groupStack); + + $group->mergeAttributes($event); } } diff --git a/tests/Console/Scheduling/ScheduleTest.php b/tests/Console/Scheduling/ScheduleTest.php index 1071f543208d..55b90a4197d7 100644 --- a/tests/Console/Scheduling/ScheduleTest.php +++ b/tests/Console/Scheduling/ScheduleTest.php @@ -89,4 +89,31 @@ public function testItCanFilterEventsByEnvironments(): void $this->assertSame([], $filteredEvents[2]->environments); $this->assertSame('0 * * * *', $filteredEvents[2]->expression); } + + public function testItCanAddAttributesToEvents(): void + { + $schedule = new Schedule(); + + $event = $schedule->command('inspire') + ->withAttributes(['team' => 'platform']) + ->withAttributes(['labels' => ['maintenance']]); + + $this->assertSame([ + 'team' => 'platform', + 'labels' => ['maintenance'], + ], $event->attributes); + } + + public function testItCanAddAttributesToPendingEvents(): void + { + $schedule = new Schedule(); + + $schedule->withAttributes(['team' => 'platform'])->command('inspire'); + $schedule->command('queue:work'); + + $events = $schedule->events(); + + $this->assertSame(['team' => 'platform'], $events[0]->attributes); + $this->assertSame([], $events[1]->attributes); + } } diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index 440a80d2146a..e92c7a476322 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -88,6 +88,44 @@ public function testGroupedScheduleCanBeNested() $this->assertSame('Asia/Dhaka', $events[1]->timezone); } + public function testGroupCanApplyAttributesToSchedules() + { + Schedule::withAttributes(['team' => 'platform'])->group(function () { + Schedule::command('inspire'); + }); + + $events = Schedule::events(); + + $this->assertSame(['team' => 'platform'], $events[0]->attributes); + } + + public function testGroupAttributesAreNotDuplicatedOnPendingSchedules() + { + Schedule::withAttributes(['team' => 'platform'])->group(function () { + Schedule::dailyAt('09:00')->command('inspire'); + }); + + $events = Schedule::events(); + + $this->assertSame(['team' => 'platform'], $events[0]->attributes); + $this->assertSame('0 9 * * *', $events[0]->expression); + } + + public function testGroupAttributesAreMergedWithPendingAttributes() + { + Schedule::withAttributes(['team' => 'platform'])->group(function () { + Schedule::withAttributes(['tagName' => 'import-premium-podcasts']) + ->command('audio:import-podcasts --only-premium'); + }); + + $events = Schedule::events(); + + $this->assertSame([ + 'team' => 'platform', + 'tagName' => 'import-premium-podcasts', + ], $events[0]->attributes); + } + #[DataProvider('groupAttributes')] public function testGroupCanApplyAttributeToSchedules(string $property, mixed $value) { @@ -329,6 +367,26 @@ public function testNestedGroupInheritsEventMacros() Event::flushMacros(); } + public function testGroupAppliesEventMacrosOnceToPendingSchedules() + { + Event::macro('sentryMonitor', function () { + $this->sentryMonitored = ($this->sentryMonitored ?? 0) + 1; + + return $this; + }); + + $schedule = new ScheduleClass; + $schedule->daily()->sentryMonitor()->group(function ($schedule) { + $schedule->at('09:00')->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertSame(1, $events[0]->sentryMonitored); + $this->assertSame('0 9 * * *', $events[0]->expression); + + Event::flushMacros(); + } + public function testGroupAppliesOnFailureCallbackToAllEvents() { $calls = []; @@ -418,6 +476,26 @@ public function testGroupAppliesBeforeAndAfterCallbacksToAllEvents() $this->assertSame(['before', 'after', 'then'], $calls); } + public function testGroupAppliesAfterCallbackOnceToPendingSchedules() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->after(function () use (&$calls) { + $calls[] = 'after'; + }) + ->group(function ($schedule) { + $schedule->at('09:00')->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + + $this->assertSame(['after'], $calls); + $this->assertSame('0 9 * * *', $events[0]->expression); + } + public function testGroupCallbacksCombineWithEventLevelCallbacks() { $calls = []; @@ -469,6 +547,31 @@ public function testNestedGroupInheritsLifecycleCallbacks() $this->assertSame(['outer', 'outer', 'inner'], $calls); } + public function testNestedGroupInheritsLifecycleCallbacksOnce() + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->after(function () use (&$calls) { + $calls[] = 'outer'; + }) + ->group(function ($schedule) use (&$calls) { + $schedule + ->after(function () use (&$calls) { + $calls[] = 'inner'; + }) + ->group(function ($schedule) { + $schedule->command('inspire'); + }); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + + $this->assertSame(['outer', 'inner'], $calls); + } + public function testGroupCanStartWithLifecycleCallbackWithoutFrequency() { $calls = []; From 576ebce930b017fb6470d8ae2eebc4b74b603e3e Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sun, 24 May 2026 22:42:51 +0000 Subject: [PATCH 450/596] Update facade docblocks --- src/Illuminate/Support/Facades/Schedule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index 0c0024d02f35..ad0779f31a68 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -33,6 +33,7 @@ * @method static \Illuminate\Console\Scheduling\PendingEventAttributes skip(\Closure|bool $callback) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes name(string $description) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes description(string $description) + * @method static \Illuminate\Console\Scheduling\PendingEventAttributes withAttributes(array $attributes) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes cron(string $expression) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes between(string $startTime, string $endTime) * @method static \Illuminate\Console\Scheduling\PendingEventAttributes unlessBetween(string $startTime, string $endTime) From 39cb3f2fb40604e4f841c8ea78845b8a3fcd64ed Mon Sep 17 00:00:00 2001 From: Youssef Mansour Date: Tue, 26 May 2026 02:02:48 +0300 Subject: [PATCH 451/596] Guard base_path() call in SQLiteConnector for standalone usage (#60260) (#60266) When using illuminate/database as a standalone package (without the full Laravel framework), the base_path() helper function is not available. Guard the call with function_exists() to prevent a fatal error. Co-authored-by: Youssef Mansour --- src/Illuminate/Database/Connectors/SQLiteConnector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Connectors/SQLiteConnector.php b/src/Illuminate/Database/Connectors/SQLiteConnector.php index 858549ec55de..7060669073be 100755 --- a/src/Illuminate/Database/Connectors/SQLiteConnector.php +++ b/src/Illuminate/Database/Connectors/SQLiteConnector.php @@ -51,7 +51,7 @@ protected function parseDatabasePath(string $path): string return $path; } - $path = realpath($path) ?: realpath(base_path($path)); + $path = realpath($path) ?: (function_exists('base_path') ? realpath(base_path($path)) : false); // Here we'll verify that the SQLite database exists before going any further // as the developer probably wants to know if the database exists and this From 71042202434914647bd849b478ff96f43560ab01 Mon Sep 17 00:00:00 2001 From: MD Amdadul Hoque Shakib <92402980+AmdadulShakib@users.noreply.github.com> Date: Tue, 26 May 2026 05:04:25 +0600 Subject: [PATCH 452/596] [13.x] Fix incorrect @return types in Number::spell(), ordinal(), and spellOrdinal() (#60263) * [13.x] Fix @return type for Number::spell() to string|false * [13.x] Fix @return type for Number::ordinal() to string|false * [13.x] Fix @return type for Number::spellOrdinal() to string|false --- src/Illuminate/Support/Number.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 6ea2430a98a0..b66de2bae340 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -96,7 +96,7 @@ public static function parseFloat(string $string, ?string $locale = null): float * @param string|null $locale * @param int|null $after * @param int|null $until - * @return string + * @return string|false */ public static function spell(int|float $number, ?string $locale = null, ?int $after = null, ?int $until = null) { @@ -120,7 +120,7 @@ public static function spell(int|float $number, ?string $locale = null, ?int $af * * @param int|float $number * @param string|null $locale - * @return string + * @return string|false */ public static function ordinal(int|float $number, ?string $locale = null) { @@ -136,7 +136,7 @@ public static function ordinal(int|float $number, ?string $locale = null) * * @param int|float $number * @param string|null $locale - * @return string + * @return string|false */ public static function spellOrdinal(int|float $number, ?string $locale = null) { From 566f2c4d9c3c5bb4bb2622281bc0a1e72ec219fa Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 26 May 2026 07:05:07 +0800 Subject: [PATCH 453/596] Supports using URI-based connection for SQLite using `file:` prefix (#60261) fixes #60260 Reference: php.watch/versions/8.1/pdo-sqlite-file-uri Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Database/Connectors/SQLiteConnector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Connectors/SQLiteConnector.php b/src/Illuminate/Database/Connectors/SQLiteConnector.php index 7060669073be..3b07715de870 100755 --- a/src/Illuminate/Database/Connectors/SQLiteConnector.php +++ b/src/Illuminate/Database/Connectors/SQLiteConnector.php @@ -46,7 +46,8 @@ protected function parseDatabasePath(string $path): string // querying. In-memory databases shall be anonymous (:memory:) or named. if ($path === ':memory:' || str_contains($path, '?mode=memory') || - str_contains($path, '&mode=memory') + str_contains($path, '&mode=memory') || + str_starts_with($path, 'file:') ) { return $path; } From ee6949d1f1752ea415673fd04a13c8f1dd309381 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 26 May 2026 19:17:44 -0400 Subject: [PATCH 454/596] Add Client\Request::uri() (#60282) --- src/Illuminate/Http/Client/Request.php | 11 +++++++++++ tests/Http/HttpClientTest.php | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Illuminate/Http/Client/Request.php b/src/Illuminate/Http/Client/Request.php index 976ace0d3b6f..0c7e3970f96d 100644 --- a/src/Illuminate/Http/Client/Request.php +++ b/src/Illuminate/Http/Client/Request.php @@ -6,6 +6,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Traits\Macroable; +use Illuminate\Support\Uri; use LogicException; class Request implements ArrayAccess @@ -63,6 +64,16 @@ public function url() return (string) $this->request->getUri(); } + /** + * Get the request URI as a URI instance. + * + * @return \Illuminate\Support\Uri + */ + public function uri() + { + return Uri::of($this->url()); + } + /** * Determine if the request has a given header. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 299229547493..43bbdba80a1c 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -38,6 +38,7 @@ use Illuminate\Support\Fluent; use Illuminate\Support\Sleep; use Illuminate\Support\Str; +use Illuminate\Support\Uri; use Illuminate\Support\Stringable; use JsonSerializable; use Mockery as m; @@ -1132,6 +1133,18 @@ public function testGetWithStringQueryParam() }); } + public function testRequestUriMethod() + { + $this->factory->fake(); + + $this->factory->get('http://foo.com/get?foo=bar&page=1'); + + $this->factory->assertSent(function (Request $request) { + return $request->uri() instanceof Uri + && (string) $request->uri() === 'http://foo.com/get?foo=bar&page=1'; + }); + } + public function testGetWithQuery() { $this->factory->fake(); From a67650e0c3f425597b35d2b186d1b490725ef404 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 26 May 2026 23:18:17 +0000 Subject: [PATCH 455/596] Apply fixes from StyleCI --- tests/Http/HttpClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 43bbdba80a1c..df89fc1dbda2 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -38,8 +38,8 @@ use Illuminate\Support\Fluent; use Illuminate\Support\Sleep; use Illuminate\Support\Str; -use Illuminate\Support\Uri; use Illuminate\Support\Stringable; +use Illuminate\Support\Uri; use JsonSerializable; use Mockery as m; use OutOfBoundsException; From 4db3505e397194022521e547cc603c496907c7ee Mon Sep 17 00:00:00 2001 From: Martin Soenen Date: Wed, 27 May 2026 01:18:26 +0200 Subject: [PATCH 456/596] Fix #60278 - View\Factory::flushComponents() doesn't reset $slots / $slotStack (#60283) --- src/Illuminate/View/Concerns/ManagesComponents.php | 2 ++ tests/View/ViewFactoryTest.php | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Illuminate/View/Concerns/ManagesComponents.php b/src/Illuminate/View/Concerns/ManagesComponents.php index c0a529c02203..be88c4edf7cc 100644 --- a/src/Illuminate/View/Concerns/ManagesComponents.php +++ b/src/Illuminate/View/Concerns/ManagesComponents.php @@ -217,5 +217,7 @@ protected function flushComponents() $this->componentStack = []; $this->componentData = []; $this->currentComponentData = []; + $this->slots = []; + $this->slotStack = []; } } diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 326df4900f49..42d2ef26fb0d 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -696,6 +696,20 @@ public function testComponentHandlingUsingHtmlable() $this->assertSame('laravel.com', $contents); } + public function testFlushStateResetsSlots() + { + $factory = $this->getFactory(); + + $factory->slot('title'); + echo 'laravel.com'; + $factory->endSlot(); + + $factory->flushState(); + + $this->assertSame([], (fn () => $this->slots)->call($factory)); + $this->assertSame([], (fn () => $this->slotStack)->call($factory)); + } + public function testTranslation() { $container = new Container; From 48951e99163c7f63e47250e0f9ebe37c06946d7d Mon Sep 17 00:00:00 2001 From: clem Date: Wed, 27 May 2026 01:22:32 +0200 Subject: [PATCH 457/596] [12.x] Fix queue:failed command to show real class name (#60279) --- .../Queue/Console/ListFailedCommand.php | 16 +- tests/Queue/QueueListFailedCommandTest.php | 181 ++++++++++++++++++ 2 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 tests/Queue/QueueListFailedCommandTest.php diff --git a/src/Illuminate/Queue/Console/ListFailedCommand.php b/src/Illuminate/Queue/Console/ListFailedCommand.php index 8aaa2e60d0e4..356ca9be24b3 100644 --- a/src/Illuminate/Queue/Console/ListFailedCommand.php +++ b/src/Illuminate/Queue/Console/ListFailedCommand.php @@ -87,11 +87,21 @@ private function extractJobName($payload) { $payload = json_decode($payload, true); - if ($payload && (! isset($payload['data']['command']))) { + if (! $payload) { + return null; + } + + if (! isset($payload['data']['command'])) { return $payload['job'] ?? null; - } elseif ($payload && isset($payload['data']['command'])) { - return $this->matchJobName($payload); } + + // Prefer the displayName set by Queue::createPayloadArray() so wrapper jobs + // (CallQueuedListener, SendQueuedMailable, etc.) show the underlying class. + if (! empty($payload['displayName']) && is_string($payload['displayName'])) { + return $payload['displayName']; + } + + return $this->matchJobName($payload); } /** diff --git a/tests/Queue/QueueListFailedCommandTest.php b/tests/Queue/QueueListFailedCommandTest.php new file mode 100644 index 000000000000..69959f3be942 --- /dev/null +++ b/tests/Queue/QueueListFailedCommandTest.php @@ -0,0 +1,181 @@ +runListFailedCommandWith([ + $this->fakeFailedJob([ + 'displayName' => 'App\\Listeners\\HandleStripeWebhookHandled', + 'data' => [ + 'commandName' => 'Illuminate\\Events\\CallQueuedListener', + 'command' => 'O:42:"Illuminate\\Events\\CallQueuedListener":3:{s:5:"class";s:43:"App\\Listeners\\HandleStripeWebhookHandled";s:6:"method";s:6:"handle";s:4:"data";a:0:{}}', + ], + ]), + ]); + + $this->assertStringContainsString('App\\Listeners\\HandleStripeWebhookHandled', $output); + $this->assertStringNotContainsString('CallQueuedListener', $output); + } + + public function testRegularQueuedJobStillShowsJobClass() + { + // Regression: the common ShouldQueue path must keep showing the job class. + $output = $this->runListFailedCommandWith([ + $this->fakeFailedJob([ + 'displayName' => 'App\\Jobs\\ProcessPodcast', + 'data' => [ + 'commandName' => 'App\\Jobs\\ProcessPodcast', + 'command' => 'O:25:"App\\Jobs\\ProcessPodcast":0:{}', + ], + ]), + ]); + + $this->assertStringContainsString('App\\Jobs\\ProcessPodcast', $output); + } + + public function testLegacyPayloadWithoutDisplayNameFallsBackToRegex() + { + // Pre-5.6 failed_jobs rows have no displayName. The legacy regex path + // must still extract the first quoted class from data.command. + $output = $this->runListFailedCommandWith([ + $this->fakeFailedJob([ + 'data' => [ + 'commandName' => 'App\\Jobs\\LegacyJob', + 'command' => 'O:18:"App\\Jobs\\LegacyJob":0:{}', + ], + ]), + ]); + + $this->assertStringContainsString('App\\Jobs\\LegacyJob', $output); + } + + public function testEncryptedJobShowsUnderlyingClass() + { + // Encrypted queue payloads store ciphertext in data.command, so the + // legacy regex falls back to CallQueuedHandler@call (the value at + // payload.job). displayName carries the real underlying class. + $output = $this->runListFailedCommandWith([ + $this->fakeFailedJob([ + 'displayName' => 'App\\Jobs\\ProcessOrder', + 'job' => 'Illuminate\\Queue\\CallQueuedHandler@call', + 'data' => [ + 'commandName' => 'Illuminate\\Queue\\CallEncryptedQueuedHandler', + 'command' => 'eyJpdiI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFg9IiwidmFsdWUiOiJjaXBoZXJ0ZXh0LWJsb2Itd2l0aC1uby1jbGFzcy1uYW1lcyIsIm1hYyI6ImZha2UifQ==', + ], + ]), + ]); + + $this->assertStringContainsString('App\\Jobs\\ProcessOrder', $output); + $this->assertStringNotContainsString('CallQueuedHandler', $output); + } + + public function testMalformedPayloadDoesNotThrow() + { + // Malformed JSON in the payload column must not bubble up an exception; + // the row should render with an empty Class cell. + $output = $this->runListFailedCommandWithRawPayload('not-json-at-all'); + + $this->assertStringContainsString('1', $output); + } + + /** + * Build a fake failed_jobs row with an encoded JSON payload. + * + * @param array $payload + * @return array + */ + private function fakeFailedJob(array $payload): array + { + return [ + 'id' => 1, + 'connection' => 'database', + 'queue' => 'default', + 'payload' => json_encode($payload + ['uuid' => 'fake-uuid']), + 'exception' => 'Exception: boom', + 'failed_at' => '2026-01-01 00:00:00', + ]; + } + + /** + * Build a row with a raw (possibly malformed) payload string. + * + * @param string $rawPayload + * @return array + */ + private function rawFailedJob(string $rawPayload): array + { + return [ + 'id' => 1, + 'connection' => 'database', + 'queue' => 'default', + 'payload' => $rawPayload, + 'exception' => 'Exception: boom', + 'failed_at' => '2026-01-01 00:00:00', + ]; + } + + /** + * Run queue:failed with the given failed_jobs rows and return captured output. + * + * @param array $rows + * @return string + */ + private function runListFailedCommandWith(array $rows): string + { + return $this->executeCommand($rows); + } + + /** + * Run queue:failed with a single raw-payload row and return captured output. + * + * @param string $rawPayload + * @return string + */ + private function runListFailedCommandWithRawPayload(string $rawPayload): string + { + return $this->executeCommand([$this->rawFailedJob($rawPayload)]); + } + + /** + * Wire up a stub failer, run the command, and return the buffered output. + * + * @param array $rows + * @return string + */ + private function executeCommand(array $rows): string + { + $container = new Application; + + // The command resolves the failer via the queue.failer container binding. + $failer = m::mock(FailedJobProviderInterface::class); + $failer->shouldReceive('all')->andReturn($rows); + $container->instance('queue.failer', $failer); + + $command = new ListFailedCommand; + $command->setLaravel($container); + + $output = new BufferedOutput; + $command->run(new ArrayInput([]), $output); + + return $output->fetch(); + } +} From 4614124208e59ef97f64b06c06676575f5cbdc22 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 27 May 2026 08:23:16 +0900 Subject: [PATCH 458/596] remove comment --- src/Illuminate/Queue/Console/ListFailedCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Illuminate/Queue/Console/ListFailedCommand.php b/src/Illuminate/Queue/Console/ListFailedCommand.php index 356ca9be24b3..2f617e9e7c64 100644 --- a/src/Illuminate/Queue/Console/ListFailedCommand.php +++ b/src/Illuminate/Queue/Console/ListFailedCommand.php @@ -95,8 +95,6 @@ private function extractJobName($payload) return $payload['job'] ?? null; } - // Prefer the displayName set by Queue::createPayloadArray() so wrapper jobs - // (CallQueuedListener, SendQueuedMailable, etc.) show the underlying class. if (! empty($payload['displayName']) && is_string($payload['displayName'])) { return $payload['displayName']; } From 2627847779c74d48b372ff25b792ab5754c57137 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 27 May 2026 08:24:40 +0900 Subject: [PATCH 459/596] use display name --- src/Illuminate/Queue/Console/ListFailedCommand.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/Console/ListFailedCommand.php b/src/Illuminate/Queue/Console/ListFailedCommand.php index 992749633a1d..a361475c59b0 100644 --- a/src/Illuminate/Queue/Console/ListFailedCommand.php +++ b/src/Illuminate/Queue/Console/ListFailedCommand.php @@ -94,11 +94,19 @@ private function extractJobName($payload) { $payload = json_decode($payload, true); - if ($payload && (! isset($payload['data']['command']))) { + if (! $payload) { + return null; + } + + if (! isset($payload['data']['command'])) { return $payload['job'] ?? null; - } elseif ($payload && isset($payload['data']['command'])) { - return $this->matchJobName($payload); } + + if (! empty($payload['displayName']) && is_string($payload['displayName'])) { + return $payload['displayName']; + } + + return $this->matchJobName($payload); } /** From 66aa65c2ee87fdbf72d0667e565369ded951d51e Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Wed, 27 May 2026 00:26:39 +0100 Subject: [PATCH 460/596] [12.x] Throw ManagedQueueNotFoundException when a managed queue is missing (#60276) * Throw ManagedQueueNotFoundException with queue name for missing managed queues Wraps AWS.SimpleQueueService.NonExistentQueue errors at the SQS client handler stack so every managed queue operation surfaces a clear message including the queue name, instead of the vague AWS error. Co-Authored-By: Claude Opus 4.7 (1M context) * Reword ManagedQueueNotFoundException message Co-Authored-By: Claude Opus 4.7 (1M context) * Use expectException/expectExceptionMessage in managed queue not found test Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../Cloud/ManagedQueueNotFoundException.php | 10 ++++ src/Illuminate/Foundation/Cloud/Queue.php | 2 +- .../Foundation/Cloud/QueueConnector.php | 37 ++++++++++++- tests/Foundation/Cloud/QueueTest.php | 53 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php diff --git a/src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php b/src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php new file mode 100644 index 000000000000..d363db724d93 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php @@ -0,0 +1,10 @@ +config['connection']['prefix'] ?? null; $suffix = $this->config['connection']['suffix'] ?? null; diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index efe058b6c1fc..55a25569b607 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -2,12 +2,17 @@ namespace Illuminate\Foundation\Cloud; +use Aws\CommandInterface; +use Aws\Exception\AwsException; +use Aws\Sqs\SqsClient; use Illuminate\Foundation\Application; use Illuminate\Queue\Connectors\ConnectorInterface; use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\WorkerStopping; +use Illuminate\Queue\SqsQueue; use Illuminate\Queue\Worker; use Illuminate\Queue\WorkerStopReason; +use Psr\Http\Message\RequestInterface; class QueueConnector implements ConnectorInterface { @@ -31,12 +36,18 @@ public function __construct( */ public function connect(array $config): Queue { + $underlying = $this->connector->connect($config['connection']); + $queue = new Queue( - $this->connector->connect($config['connection']), + $underlying, $this->app[Events::class], $config, ); + if ($underlying instanceof SqsQueue) { + $this->registerErrorHandling($underlying->getSqs(), $queue); + } + $this->configureQueue($queue); if (! $this->app->runningConsoleCommand('queue:work')) { @@ -49,6 +60,30 @@ public function connect(array $config): Queue return $queue; } + /** + * Register SQS client middleware that translates "queue does not exist" + * errors into a ManagedQueueNotFoundException with the queue name. + */ + protected function registerErrorHandling(SqsClient $sqs, Queue $queue): void + { + $sqs->getHandlerList()->appendSign(function (callable $handler) use ($queue) { + return function (CommandInterface $command, RequestInterface $request) use ($handler, $queue) { + return $handler($command, $request)->otherwise(function ($reason) use ($command, $queue) { + if ($reason instanceof AwsException && + $reason->getAwsErrorCode() === 'AWS.SimpleQueueService.NonExistentQueue') { + $name = $queue->normalizeQueue($command['QueueUrl'] ?? null); + + throw new ManagedQueueNotFoundException( + "Managed queue [{$name}] does not exist.", 0, $reason, + ); + } + + throw $reason; + }); + }; + }, 'managed-queue-not-found'); + } + /** * Configure the queue. */ diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index d1273b2aa372..8ed19dde47a3 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -2,12 +2,17 @@ namespace Tests\Tests\Foundation; +use Aws\CommandInterface; +use Aws\Exception\AwsException; +use Aws\HandlerList; +use Aws\MockHandler; use Aws\Result; use Aws\Sqs\SqsClient; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Foundation\Cloud; use Illuminate\Foundation\Cloud\Events; use Illuminate\Foundation\Cloud\FailedJobProvider; +use Illuminate\Foundation\Cloud\ManagedQueueNotFoundException; use Illuminate\Foundation\Cloud\Queue; use Illuminate\Foundation\Cloud\QueueConnector; use Illuminate\Foundation\Testing\DatabaseMigrations; @@ -893,6 +898,53 @@ public function testForgetReturnsFalseWithoutPriorFind() $this->assertEmpty($eventsFake->emitted); } + public function testItThrowsManagedQueueNotFoundExceptionWhenQueueDoesNotExist() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $this->fakeEvents(); + + $mock = new MockHandler(); + $mock->append(fn (CommandInterface $cmd) => new AwsException('Queue does not exist.', $cmd, [ + 'code' => 'AWS.SimpleQueueService.NonExistentQueue', + ])); + + $client = new SqsClient([ + 'region' => 'us-east-2', + 'version' => 'latest', + 'handler' => $mock, + 'credentials' => false, + ]); + + $this->app->instance(QueueConnector::class, new QueueConnector(new class($client) implements ConnectorInterface + { + public function __construct(private $client) + { + } + + public function connect($config) + { + return new SqsQueue( + $this->client, + $config['queue'], + $config['prefix'] ?? '', + $config['suffix'] ?? '', + $config['after_commit'] ?? null, + $config['overflow'] ?? [], + ); + } + }, $this->app)); + + $this->app['queue']->addConnector('cloud', $this->app->factory(QueueConnector::class)); + + $queue = $this->app['queue']->connection('cloud'); + + $this->expectException(ManagedQueueNotFoundException::class); + $this->expectExceptionMessage('Managed queue [missing-queue] does not exist.'); + + $queue->push(new FakeJob, queue: 'missing-queue'); + } + public function testItUsesConfigValuesToNormalizeQueueName() { Cloud::configureManagedQueues($this->app); @@ -914,6 +966,7 @@ public function testItUsesConfigValuesToNormalizeQueueName() private function mockedQueue() { $client = $this->mock(SqsClient::class); + $client->shouldReceive('getHandlerList')->andReturn(new HandlerList()); $this->app->instance(QueueConnector::class, new QueueConnector(new class($client) implements ConnectorInterface { From fae546e9a0400a96b9e3717781318fd5fb6e80b1 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Wed, 27 May 2026 00:27:23 +0100 Subject: [PATCH 461/596] [13.x] Throw ManagedQueueNotFoundException when a managed queue is missing (#60275) * Throw ManagedQueueNotFoundException with queue name for missing managed queues Wraps AWS.SimpleQueueService.NonExistentQueue errors at the SQS client handler stack so every managed queue operation surfaces a clear message including the queue name, instead of the vague AWS error. Co-Authored-By: Claude Opus 4.7 (1M context) * Reword ManagedQueueNotFoundException message Co-Authored-By: Claude Opus 4.7 (1M context) * Use expectException/expectExceptionMessage in managed queue not found test Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../Cloud/ManagedQueueNotFoundException.php | 10 ++++ src/Illuminate/Foundation/Cloud/Queue.php | 2 +- .../Foundation/Cloud/QueueConnector.php | 37 ++++++++++++- tests/Foundation/Cloud/QueueTest.php | 53 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php diff --git a/src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php b/src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php new file mode 100644 index 000000000000..d363db724d93 --- /dev/null +++ b/src/Illuminate/Foundation/Cloud/ManagedQueueNotFoundException.php @@ -0,0 +1,10 @@ +config['connection']['prefix'] ?? null; $suffix = $this->config['connection']['suffix'] ?? null; diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index efe058b6c1fc..55a25569b607 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -2,12 +2,17 @@ namespace Illuminate\Foundation\Cloud; +use Aws\CommandInterface; +use Aws\Exception\AwsException; +use Aws\Sqs\SqsClient; use Illuminate\Foundation\Application; use Illuminate\Queue\Connectors\ConnectorInterface; use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\WorkerStopping; +use Illuminate\Queue\SqsQueue; use Illuminate\Queue\Worker; use Illuminate\Queue\WorkerStopReason; +use Psr\Http\Message\RequestInterface; class QueueConnector implements ConnectorInterface { @@ -31,12 +36,18 @@ public function __construct( */ public function connect(array $config): Queue { + $underlying = $this->connector->connect($config['connection']); + $queue = new Queue( - $this->connector->connect($config['connection']), + $underlying, $this->app[Events::class], $config, ); + if ($underlying instanceof SqsQueue) { + $this->registerErrorHandling($underlying->getSqs(), $queue); + } + $this->configureQueue($queue); if (! $this->app->runningConsoleCommand('queue:work')) { @@ -49,6 +60,30 @@ public function connect(array $config): Queue return $queue; } + /** + * Register SQS client middleware that translates "queue does not exist" + * errors into a ManagedQueueNotFoundException with the queue name. + */ + protected function registerErrorHandling(SqsClient $sqs, Queue $queue): void + { + $sqs->getHandlerList()->appendSign(function (callable $handler) use ($queue) { + return function (CommandInterface $command, RequestInterface $request) use ($handler, $queue) { + return $handler($command, $request)->otherwise(function ($reason) use ($command, $queue) { + if ($reason instanceof AwsException && + $reason->getAwsErrorCode() === 'AWS.SimpleQueueService.NonExistentQueue') { + $name = $queue->normalizeQueue($command['QueueUrl'] ?? null); + + throw new ManagedQueueNotFoundException( + "Managed queue [{$name}] does not exist.", 0, $reason, + ); + } + + throw $reason; + }); + }; + }, 'managed-queue-not-found'); + } + /** * Configure the queue. */ diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index d1273b2aa372..8ed19dde47a3 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -2,12 +2,17 @@ namespace Tests\Tests\Foundation; +use Aws\CommandInterface; +use Aws\Exception\AwsException; +use Aws\HandlerList; +use Aws\MockHandler; use Aws\Result; use Aws\Sqs\SqsClient; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Foundation\Cloud; use Illuminate\Foundation\Cloud\Events; use Illuminate\Foundation\Cloud\FailedJobProvider; +use Illuminate\Foundation\Cloud\ManagedQueueNotFoundException; use Illuminate\Foundation\Cloud\Queue; use Illuminate\Foundation\Cloud\QueueConnector; use Illuminate\Foundation\Testing\DatabaseMigrations; @@ -893,6 +898,53 @@ public function testForgetReturnsFalseWithoutPriorFind() $this->assertEmpty($eventsFake->emitted); } + public function testItThrowsManagedQueueNotFoundExceptionWhenQueueDoesNotExist() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $this->fakeEvents(); + + $mock = new MockHandler(); + $mock->append(fn (CommandInterface $cmd) => new AwsException('Queue does not exist.', $cmd, [ + 'code' => 'AWS.SimpleQueueService.NonExistentQueue', + ])); + + $client = new SqsClient([ + 'region' => 'us-east-2', + 'version' => 'latest', + 'handler' => $mock, + 'credentials' => false, + ]); + + $this->app->instance(QueueConnector::class, new QueueConnector(new class($client) implements ConnectorInterface + { + public function __construct(private $client) + { + } + + public function connect($config) + { + return new SqsQueue( + $this->client, + $config['queue'], + $config['prefix'] ?? '', + $config['suffix'] ?? '', + $config['after_commit'] ?? null, + $config['overflow'] ?? [], + ); + } + }, $this->app)); + + $this->app['queue']->addConnector('cloud', $this->app->factory(QueueConnector::class)); + + $queue = $this->app['queue']->connection('cloud'); + + $this->expectException(ManagedQueueNotFoundException::class); + $this->expectExceptionMessage('Managed queue [missing-queue] does not exist.'); + + $queue->push(new FakeJob, queue: 'missing-queue'); + } + public function testItUsesConfigValuesToNormalizeQueueName() { Cloud::configureManagedQueues($this->app); @@ -914,6 +966,7 @@ public function testItUsesConfigValuesToNormalizeQueueName() private function mockedQueue() { $client = $this->mock(SqsClient::class); + $client->shouldReceive('getHandlerList')->andReturn(new HandlerList()); $this->app->instance(QueueConnector::class, new QueueConnector(new class($client) implements ConnectorInterface { From 4e0f82b658706a4cabc1fa143d8c4c922897d300 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 27 May 2026 08:28:21 +0900 Subject: [PATCH 462/596] update comment --- src/Illuminate/Foundation/Cloud/QueueConnector.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index 55a25569b607..2ed8075df251 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -61,8 +61,7 @@ public function connect(array $config): Queue } /** - * Register SQS client middleware that translates "queue does not exist" - * errors into a ManagedQueueNotFoundException with the queue name. + * Register SQS client middleware that translates "queue does not exist" errors into ManagedQueueNotFoundExceptions. */ protected function registerErrorHandling(SqsClient $sqs, Queue $queue): void { From 5d0dd302b1397d8118121a9cf68ef50e2cf985d0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 27 May 2026 08:28:48 +0900 Subject: [PATCH 463/596] update comment --- src/Illuminate/Foundation/Cloud/QueueConnector.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud/QueueConnector.php b/src/Illuminate/Foundation/Cloud/QueueConnector.php index 55a25569b607..2ed8075df251 100644 --- a/src/Illuminate/Foundation/Cloud/QueueConnector.php +++ b/src/Illuminate/Foundation/Cloud/QueueConnector.php @@ -61,8 +61,7 @@ public function connect(array $config): Queue } /** - * Register SQS client middleware that translates "queue does not exist" - * errors into a ManagedQueueNotFoundException with the queue name. + * Register SQS client middleware that translates "queue does not exist" errors into ManagedQueueNotFoundExceptions. */ protected function registerErrorHandling(SqsClient $sqs, Queue $queue): void { From 6ac27a7fcfa728250c9f77921cb8fb955546b591 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 26 May 2026 23:39:26 +0000 Subject: [PATCH 464/596] Update version to v13.12.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 60aa0281f914..f9331abe8d60 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 = '13.11.2'; + const VERSION = '13.12.0'; /** * The base path for the Laravel installation. From dcdad3e8cf36e242443f1b8fff724ce109c2206c Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 26 May 2026 23:41:10 +0000 Subject: [PATCH 465/596] Update CHANGELOG --- CHANGELOG.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e4421bd931..fb54002c8c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,36 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.11.2...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.12.0...13.x) + +## [v13.12.0](https://github.com/laravel/framework/compare/v13.11.2...v13.12.0) - 2026-05-26 + +* [13.x] Accept Symfony's new control-characters exception message in mailer test by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60202 +* [13.x] Ability to opt out of worker restart on lost connection by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60201 +* [13.x] Resolve scheduled event callback parameter by type rather than name by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/60197 +* [13.x] default clear() queue driver param to null by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60192 +* [13.x] Fix path separator being encoded for LocalFilesystemAdapter by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60194 +* [13.x] feat: add factory to pivot stub by [@ludo237](https://github.com/ludo237) in https://github.com/laravel/framework/pull/60204 +* [13.x] Fix incorrect type hint in Optional::offsetUnset() docblock by [@rpsohag](https://github.com/rpsohag) in https://github.com/laravel/framework/pull/60207 +* [13.x] Make ClearCommand prohibitable by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60215 +* [13.x] Allow auto discovered listeners to opt out of discovery by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60209 +* [13.x] Ensure Up/Down commands report exceptions by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60232 +* [13.x] Fix path separator encoding in temporaryUrl on local disk by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/60230 +* [13.x] Add assertJsonPathsCanonicalizing to TestResponse by [@Tresor-Kasenda](https://github.com/Tresor-Kasenda) in https://github.com/laravel/framework/pull/60225 +* [13.x] Add `normalize` parameter to `Str::studly()` and `Str::pascal()` by [@hotmeteor](https://github.com/hotmeteor) in https://github.com/laravel/framework/pull/60229 +* [13.x] Fix async HTTP retries when using array backoff values by [@LucasCavalheri](https://github.com/LucasCavalheri) in https://github.com/laravel/framework/pull/60214 +* [13.x] Replace compact with explicit arrays by [@parkourben99](https://github.com/parkourben99) in https://github.com/laravel/framework/pull/60234 +* [13.x] Add prohibited to KeyGenerateCommand by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60224 +* [13.x] remove last `compact()` call by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/60235 +* [13.x] Allow JsonSchema fluent boolean flags to be unset by [@LucasCavalheri](https://github.com/LucasCavalheri) in https://github.com/laravel/framework/pull/60239 +* [13.x] battle harden when scheme is present in the config by [@DGarbs51](https://github.com/DGarbs51) in https://github.com/laravel/framework/pull/60237 +* [13.x] Rector : Always convert `compact()` to variables by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/60236 +* [13.x] Add attributes to Scheduler by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60255 +* [13.x] Guard base_path() call in SQLiteConnector for standalone usage by [@YoussefMansour9](https://github.com/YoussefMansour9) in https://github.com/laravel/framework/pull/60266 +* [13.x] Fix incorrect [@return](https://github.com/return) types in Number::spell(), ordinal(), and spellOrdinal() by [@AmdadulShakib](https://github.com/AmdadulShakib) in https://github.com/laravel/framework/pull/60263 +* [13.x] Supports using URI-based connection for SQLite using `file:` prefix by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60261 +* [13.x] Add `Client\Request::uri()` by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/60282 +* [13.x] View\Factory::flushComponents() doesn't reset $slots / $slotStack by [@martinsoenen](https://github.com/martinsoenen) in https://github.com/laravel/framework/pull/60283 +* [13.x] Throw ManagedQueueNotFoundException when a managed queue is missing by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60275 ## [v13.11.2](https://github.com/laravel/framework/compare/v13.11.1...v13.11.2) - 2026-05-20 From 1124062a1ca92d290c8bcb9b7f649920fa6816bf Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 26 May 2026 23:41:33 +0000 Subject: [PATCH 466/596] Update version to v12.61.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 5953f99211cb..b99b746f3624 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.60.2'; + const VERSION = '12.61.0'; /** * The base path for the Laravel installation. From 3438371a248fa86dba3f2b95704532e043232353 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 26 May 2026 23:43:04 +0000 Subject: [PATCH 467/596] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24daedd95cc..56cf9cd6ea64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.60.2...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.61.0...12.x) + +## [v12.61.0](https://github.com/laravel/framework/compare/v12.60.2...v12.61.0) - 2026-05-26 + +* [12.x] Accept Symfony's new control-characters exception message in mailer test by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60203 +* [12.x] Fix queue:failed command to show real class name by [@clementmas](https://github.com/clementmas) in https://github.com/laravel/framework/pull/60279 +* [12.x] Throw ManagedQueueNotFoundException when a managed queue is missing by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60276 ## [v12.60.2](https://github.com/laravel/framework/compare/v12.60.1...v12.60.2) - 2026-05-20 From 60a275a08ca4e07970908b47ad6ef632d3da80e3 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 27 May 2026 02:54:40 +0100 Subject: [PATCH 468/596] Update MySqlSchemaState.php (#60284) --- src/Illuminate/Database/Schema/MySqlSchemaState.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index abef3c5cbabe..a053913cd160 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -130,6 +130,7 @@ protected function connectionString(array $versionInfo) $value .= ' --ssl-key="${:LARAVEL_LOAD_SSL_KEY}"'; } + /** @phpstan-ignore classConstant.notFound */ if (($config['options'][Mysql::ATTR_SSL_VERIFY_SERVER_CERT] ?? null) === false) { if (version_compare($versionInfo['version'], '5.7.11', '>=') && ! $versionInfo['isMariaDb']) { $value .= ' --ssl-mode=DISABLED'; From 90403f011c0e032c46bc54d8e516d387fac38cd4 Mon Sep 17 00:00:00 2001 From: Lucas Cavalheri Date: Wed, 27 May 2026 02:47:46 -0300 Subject: [PATCH 469/596] feat: add storage attachment helpers to MailMessage (#60268) --- .../Notifications/Messages/MailMessage.php | 37 +++++++ .../NotificationMailMessageTest.php | 100 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/Illuminate/Notifications/Messages/MailMessage.php b/src/Illuminate/Notifications/Messages/MailMessage.php index 52a801c9524b..edcc2aae08e4 100644 --- a/src/Illuminate/Notifications/Messages/MailMessage.php +++ b/src/Illuminate/Notifications/Messages/MailMessage.php @@ -312,6 +312,43 @@ public function attachData($data, $name, array $options = []) return $this; } + /** + * Attach a file to the message from storage. + * + * @param string $path + * @param string|null $name + * @param array $options + * @return $this + */ + public function attachFromStorage($path, $name = null, array $options = []) + { + return $this->attachFromStorageDisk(null, $path, $name, $options); + } + + /** + * Attach a file to the message from storage. + * + * @param string|null $disk + * @param string $path + * @param string|null $name + * @param array $options + * @return $this + */ + public function attachFromStorageDisk($disk, $path, $name = null, array $options = []) + { + $attachment = Attachment::fromStorageDisk($disk, $path); + + if (! is_null($name)) { + $attachment->as($name); + } + + if (isset($options['mime'])) { + $attachment->withMime($options['mime']); + } + + return $this->attach($attachment); + } + /** * Add a tag header to the message when supported by the underlying transport. * diff --git a/tests/Notifications/NotificationMailMessageTest.php b/tests/Notifications/NotificationMailMessageTest.php index 794a1ad3670d..7135785d1f32 100644 --- a/tests/Notifications/NotificationMailMessageTest.php +++ b/tests/Notifications/NotificationMailMessageTest.php @@ -2,13 +2,29 @@ namespace Illuminate\Tests\Notifications; +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Container\Container; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Illuminate\Contracts\Mail\Attachable; +use Illuminate\Filesystem\Filesystem; +use Illuminate\Filesystem\FilesystemManager; use Illuminate\Mail\Attachment; use Illuminate\Notifications\Messages\MailMessage; use PHPUnit\Framework\TestCase; class NotificationMailMessageTest extends TestCase { + protected ?string $filesystemRoot = null; + + protected function tearDown(): void + { + if ($this->filesystemRoot !== null) { + $this->deleteDirectory($this->filesystemRoot); + $this->filesystemRoot = null; + } + + parent::tearDown(); + } public function testTemplate() { $message = new MailMessage; @@ -303,6 +319,37 @@ public function testUnlessCallbackWithDefault() $this->assertSame([['truthy@example.com', null]], $message->cc); } + public function testItAttachesFilesFromStorage() + { + $this->bootstrapFilesystem(); + + file_put_contents($this->filesystemRoot.'/invoices/1.pdf', 'pdf content'); + + $message = new MailMessage; + $message->attachFromStorage('invoices/1.pdf', 'invoice.pdf'); + + $this->assertCount(1, $message->rawAttachments); + $this->assertSame('invoice.pdf', $message->rawAttachments[0]['name']); + $this->assertSame('pdf content', $message->rawAttachments[0]['data']); + } + + public function testItAttachesFilesFromStorageDisk() + { + $this->bootstrapFilesystem(); + + file_put_contents($this->filesystemRoot.'/reports/report.txt', 'report content'); + + $message = new MailMessage; + $message->attachFromStorageDisk('s3', 'reports/report.txt', 'monthly-report.txt', [ + 'mime' => 'text/plain', + ]); + + $this->assertCount(1, $message->rawAttachments); + $this->assertSame('monthly-report.txt', $message->rawAttachments[0]['name']); + $this->assertSame('report content', $message->rawAttachments[0]['data']); + $this->assertSame('text/plain', $message->rawAttachments[0]['options']['mime']); + } + public function testItAttachesFilesViaAttachableContractFromPath() { $message = new MailMessage; @@ -389,4 +436,57 @@ public function toMailAttachment() ], ], $mailMessage->attachments); } + + protected function bootstrapFilesystem(): void + { + $this->filesystemRoot = sys_get_temp_dir().'/laravel-notification-mail-message-'.uniqid(); + + mkdir($this->filesystemRoot.'/invoices', 0777, true); + mkdir($this->filesystemRoot.'/reports', 0777, true); + + $container = Container::getInstance() ?? new Container; + + Container::setInstance($container); + + $container->instance('config', new ConfigRepository([ + 'filesystems' => [ + 'default' => 'local', + 'disks' => [ + 'local' => [ + 'driver' => 'local', + 'root' => $this->filesystemRoot, + ], + 's3' => [ + 'driver' => 'local', + 'root' => $this->filesystemRoot, + ], + ], + ], + ])); + + $container->instance('files', new Filesystem); + + $container->singleton('filesystem', fn ($container) => new FilesystemManager($container)); + + $container->alias('filesystem', FilesystemFactory::class); + } + + protected function deleteDirectory(string $directory): void + { + if (! is_dir($directory)) { + return; + } + + foreach (scandir($directory) as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $directory.'/'.$item; + + is_dir($path) ? $this->deleteDirectory($path) : unlink($path); + } + + rmdir($directory); + } } From 5a49e57d82534e6233bf4f189d72f2b585946317 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 27 May 2026 05:48:01 +0000 Subject: [PATCH 470/596] Apply fixes from StyleCI --- tests/Notifications/NotificationMailMessageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Notifications/NotificationMailMessageTest.php b/tests/Notifications/NotificationMailMessageTest.php index 7135785d1f32..66e61b49b33c 100644 --- a/tests/Notifications/NotificationMailMessageTest.php +++ b/tests/Notifications/NotificationMailMessageTest.php @@ -25,6 +25,7 @@ protected function tearDown(): void parent::tearDown(); } + public function testTemplate() { $message = new MailMessage; From 734742618b0f1acf8f87589f83b20283d76e0bac Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 27 May 2026 19:23:03 -0400 Subject: [PATCH 471/596] [13.x] Allow Http Client to be used as PSR Client (#60295) * Add failing test * Make `laravel_data` optional * Make `on_stats` optional --- src/Illuminate/Http/Client/Factory.php | 2 +- src/Illuminate/Http/Client/PendingRequest.php | 6 ++--- tests/Http/HttpClientTest.php | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index c99bace1ce53..3beccd5e4577 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -259,7 +259,7 @@ function ($request, $options) use ($callback) { $response = $response($request, $options); } - if ($response instanceof PromiseInterface) { + if ($response instanceof PromiseInterface && ($options['on_stats'] ?? null) instanceof Closure) { $options['on_stats'](new TransferStats( $request->toPsrRequest(), $response->wait(), diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index c7f9e2950e4b..2f76bae74800 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1499,7 +1499,7 @@ public function buildRecorderHandler() return $promise->then(function ($response) use ($request, $options) { $this->factory?->recordRequestResponsePair( (new Request($request)) - ->withData($options['laravel_data']) + ->withData($options['laravel_data'] ?? []) ->setRequestAttributes($this->attributes), $this->newResponse($response) ); @@ -1525,7 +1525,7 @@ public function buildStubHandler() ->map ->__invoke( (new Request($request)) - ->withData($options['laravel_data']) + ->withData($options['laravel_data'] ?? []) ->setRequestAttributes($this->attributes), $options ) @@ -1589,7 +1589,7 @@ public function runBeforeSendingCallbacks($request, array $options) $callbackResult = call_user_func( $callback, (new Request($request)) - ->withData($options['laravel_data']) + ->withData($options['laravel_data'] ?? []) ->setRequestAttributes($this->attributes), $options, $this diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index df89fc1dbda2..4d90f196e88e 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Http; use Exception; +use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException as GuzzleRequestException; use GuzzleHttp\Exception\TooManyRedirectsException; @@ -1957,6 +1958,30 @@ public function testClientCanBeSet() $this->assertSame($client, $request->buildClient()); } + public function testClientCanBeUsedExternally() + { + $this->factory->fake([ + '200.com' => $this->factory::response('hello', 200), + ]); + + $apiClient = new class ($this->factory->buildClient()) + { + public function __construct( + private GuzzleClient $client, + ) { + } + + public function sendGetRequest() + { + return $this->client->sendRequest(new GuzzleRequest('GET', '200.com')); + } + }; + + $response = $apiClient->sendGetRequest(); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('hello', $response->getBody()->getContents()); + } + public function testRequestsCanReplaceOptions() { $request = new PendingRequest($this->factory); From 9dac9339870623ffc455c5bba9a480064623d0b1 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 27 May 2026 23:23:31 +0000 Subject: [PATCH 472/596] Apply fixes from StyleCI --- tests/Http/HttpClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 4d90f196e88e..9271ecb879a6 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -1964,7 +1964,7 @@ public function testClientCanBeUsedExternally() '200.com' => $this->factory::response('hello', 200), ]); - $apiClient = new class ($this->factory->buildClient()) + $apiClient = new class($this->factory->buildClient()) { public function __construct( private GuzzleClient $client, From 2911a668cdcfafa79d58ed77e11a82691a2e318c Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 28 May 2026 00:23:59 +0100 Subject: [PATCH 473/596] Normalize HTTP client header values (#60292) --- src/Illuminate/Http/Client/Factory.php | 43 ++++- src/Illuminate/Http/Client/PendingRequest.php | 137 ++++++++++++++- tests/Http/HttpClientTest.php | 164 ++++++++++++++++++ 3 files changed, 338 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 3beccd5e4577..5d147c6abbfb 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -12,7 +12,9 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use PHPUnit\Framework\Assert as PHPUnit; /** @@ -182,7 +184,46 @@ public static function psr7Response($body = null, $status = 200, $headers = []) $headers['Content-Type'] = 'application/json'; } - return new Psr7Response($status, $headers, $body); + return new Psr7Response($status, static::normalizeResponseHeaders($headers), $body); + } + + /** + * Normalize the given fake response headers. + * + * @param array $headers + * @return array + */ + protected static function normalizeResponseHeaders(array $headers): array + { + foreach ($headers as $name => $value) { + if (is_array($value)) { + if ($value === []) { + $headers[$name] = ''; + + continue; + } + + foreach ($value as $key => $item) { + $value[$key] = match (true) { + is_scalar($item) => (string) $item, + $item instanceof Stringable => $item->toString(), + default => throw new InvalidArgumentException('HTTP fake response header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + }; + } + + $headers[$name] = $value; + + continue; + } + + $headers[$name] = match (true) { + is_scalar($value) => (string) $value, + $value instanceof Stringable => $value->toString(), + default => throw new InvalidArgumentException('HTTP fake response header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + }; + } + + return $headers; } /** diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 2f76bae74800..03257925a52b 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -26,6 +26,7 @@ use Illuminate\Support\Stringable; use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use JsonSerializable; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; @@ -1361,6 +1362,10 @@ protected function parseRequestData($method, $url, array $options) $laravelData = $laravelData->jsonSerialize(); } + if (is_array($laravelData) && $this->bodyFormat === 'multipart') { + return $this->normalizeMultipartOption($laravelData); + } + return is_array($laravelData) ? $laravelData : []; } @@ -1373,16 +1378,138 @@ protected function parseRequestData($method, $url, array $options) protected function normalizeRequestOptions(array $options) { foreach ($options as $key => $value) { - $options[$key] = match (true) { - is_array($value) => $this->normalizeRequestOptions($value), - $value instanceof Stringable => $value->toString(), - default => $value, - }; + if ($key === 'headers' && is_array($value)) { + $options[$key] = $this->normalizeHeaderValues($value); + + continue; + } + + if ($key === 'multipart' && is_array($value)) { + $options[$key] = $this->normalizeMultipartOption($value); + + continue; + } + + $options[$key] = $this->normalizeRequestOptionValue($value); } return $options; } + /** + * Normalize the given header values. + * + * @param array $headers + * @return array + */ + protected function normalizeHeaderValues(array $headers): array + { + foreach ($headers as $name => $value) { + $headers[$name] = $this->normalizeHeaderValue($value); + } + + return $headers; + } + + /** + * Normalize the given header value. + * + * @param mixed $value + * @return string|array + */ + protected function normalizeHeaderValue($value): string|array + { + if (is_array($value)) { + if ($value === []) { + return ''; + } + + foreach ($value as $key => $item) { + $value[$key] = match (true) { + is_scalar($item) => (string) $item, + $item instanceof Stringable => $item->toString(), + default => throw new InvalidArgumentException('HTTP header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + }; + } + + return $value; + } + + return match (true) { + is_scalar($value) => (string) $value, + $value instanceof Stringable => $value->toString(), + default => throw new InvalidArgumentException('HTTP header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + }; + } + + /** + * Normalize the given multipart option. + * + * @param array $multipart + * @return array + */ + protected function normalizeMultipartOption(array $multipart): array + { + foreach ($multipart as $index => $part) { + if (! is_array($part)) { + $multipart[$index] = $this->normalizeRequestOptionValue($part); + + continue; + } + + foreach ($part as $key => $value) { + if ($key === 'headers' && is_array($value)) { + continue; + } + + $part[$key] = $this->normalizeRequestOptionValue($value); + } + + $multipart[$index] = $part; + } + + return $this->normalizeMultipartHeaders($multipart); + } + + /** + * Normalize the given multipart headers. + * + * @param array $multipart + * @return array + */ + protected function normalizeMultipartHeaders(array $multipart): array + { + foreach ($multipart as $index => $part) { + if (is_array($part) && isset($part['headers']) && is_array($part['headers'])) { + foreach ($part['headers'] as $name => $value) { + $multipart[$index]['headers'][$name] = match (true) { + $value === [] => '', + is_scalar($value) => (string) $value, + $value instanceof Stringable => $value->toString(), + default => throw new InvalidArgumentException('Multipart header values must be scalar or Laravel Stringable.'), + }; + } + } + } + + return $multipart; + } + + /** + * Normalize the given request option value. + * + * @param mixed $value + * @return mixed + */ + protected function normalizeRequestOptionValue($value) + { + return match (true) { + is_array($value) => array_map(fn ($item) => $this->normalizeRequestOptionValue($item), $value), + $value instanceof Stringable => $value->toString(), + default => $value, + }; + } + /** * Populate the given response with additional data. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 9271ecb879a6..33f84f98764d 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -41,6 +41,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Stringable; use Illuminate\Support\Uri; +use InvalidArgumentException; use JsonSerializable; use Mockery as m; use OutOfBoundsException; @@ -51,6 +52,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use RuntimeException; +use stdClass; use Symfony\Component\VarDumper\VarDumper; use Throwable; @@ -98,6 +100,32 @@ public function testCreatedRequest() $this->assertFalse($response->created()); } + public function testFakeResponseHeaderValuesAreSerialized() + { + $response = $this->factory::response('OK', 200, [ + 'X-Int' => 123, + 'X-False' => false, + 'X-Empty' => [], + 'X-Laravel-Stringable' => new Stringable('laravel stringable'), + 'X-Multiple' => ['first', 123, true, false], + ])->wait(); + + $this->assertSame(['123'], $response->getHeader('X-Int')); + $this->assertSame([''], $response->getHeader('X-False')); + $this->assertSame([''], $response->getHeader('X-Empty')); + $this->assertSame(['laravel stringable'], $response->getHeader('X-Laravel-Stringable')); + $this->assertSame(['first', '123', '1', ''], $response->getHeader('X-Multiple')); + } + + #[DataProvider('invalidFakeResponseHeaderValuesProvider')] + public function testInvalidFakeResponseHeaderValuesAreRejected($value) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP fake response header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'); + + $this->factory::response('OK', 200, ['X-Test' => $value]); + } + public function testStatusCodeShorthand() { $this->factory->fake([ @@ -676,6 +704,55 @@ public function testCanSendJsonDataWithStringable() }); } + public function testHeaderValuesAreSerialized() + { + $this->factory->fake(); + + $this->factory->withHeaders([ + 'X-Int' => 123, + 'X-Float' => 1.5, + 'X-True' => true, + 'X-False' => false, + 'X-Laravel-Stringable' => new Stringable('laravel stringable'), + 'X-Multiple' => ['first', 123, true, false], + 'X-Empty' => [], + ])->post('http://foo.com/json'); + + $this->factory->assertSent(function (Request $request) { + return $request->hasHeader('X-Int', '123') + && $request->hasHeader('X-Float', '1.5') + && $request->hasHeader('X-True', '1') + && $request->hasHeader('X-False', '') + && $request->hasHeader('X-Laravel-Stringable', 'laravel stringable') + && $request->hasHeader('X-Multiple', ['first', '123', '1', '']) + && $request->hasHeader('X-Empty', ''); + }); + } + + #[DataProvider('invalidHeaderValuesProvider')] + public function testInvalidHeaderValuesAreRejected($value) + { + $this->factory->fake(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'); + + $this->factory->withHeaders(['X-Test' => $value])->post('http://foo.com/json'); + } + + public function testHeaderValuesProvidedThroughOptionsAreSerialized() + { + $this->factory->fake(); + + $this->factory->withOptions([ + 'headers' => ['X-Test' => 123], + ])->post('http://foo.com/json'); + + $this->factory->assertSent(function (Request $request) { + return $request->hasHeader('X-Test', '123'); + }); + } + public function testCanSendFormDataWithStringable() { $this->factory->fake(); @@ -797,6 +874,57 @@ public function testFilesCanBeAttached() }); } + public function testAttachHeaderValuesAreSerialized() + { + $this->factory->fake(); + + $this->factory->attach('file', 'data', 'file.txt', ['X-Part' => 123])->post('http://foo.com/file'); + + $this->factory->assertSent(function (Request $request) { + return $request[0]['headers']['X-Part'] === '123'; + }); + } + + public function testMultipartHeaderValuesAreSerialized() + { + $this->factory->fake(); + + $this->factory->asMultipart()->post('http://foo.com/multipart', [ + [ + 'name' => 'file', + 'contents' => 'data', + 'headers' => [ + 'X-Part' => 123, + 'X-Empty' => [], + 'X-Laravel-Stringable' => new Stringable('laravel stringable'), + ], + ], + ]); + + $this->factory->assertSent(function (Request $request) { + return $request[0]['headers']['X-Part'] === '123' + && $request[0]['headers']['X-Empty'] === '' + && $request[0]['headers']['X-Laravel-Stringable'] === 'laravel stringable'; + }); + } + + #[DataProvider('invalidMultipartHeaderValuesProvider')] + public function testInvalidMultipartHeaderValuesAreRejected($value) + { + $this->factory->fake(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Multipart header values must be scalar or Laravel Stringable.'); + + $this->factory->asMultipart()->post('http://foo.com/multipart', [ + [ + 'name' => 'file', + 'contents' => 'data', + 'headers' => ['X-Part' => $value], + ], + ]); + } + public function testCanSendMultipartDataWithSimplifiedParameters() { $this->factory->fake(); @@ -4420,6 +4548,42 @@ public static function methodsReceivingArrayableDataProvider() ]; } + public static function invalidHeaderValuesProvider() + { + return [ + 'null' => [null], + 'object' => [new stdClass], + 'resource' => [fopen('php://temp', 'r')], + 'array with null' => [['valid', null]], + 'array with object' => [['valid', new stdClass]], + 'array with resource' => [['valid', fopen('php://temp', 'r')]], + 'array with nested array' => [['valid', ['nested']]], + ]; + } + + public static function invalidMultipartHeaderValuesProvider() + { + return [ + 'null' => [null], + 'array' => [['nested']], + 'object' => [new stdClass], + 'resource' => [fopen('php://temp', 'r')], + ]; + } + + public static function invalidFakeResponseHeaderValuesProvider() + { + return [ + 'null' => [null], + 'object' => [new stdClass], + 'resource' => [fopen('php://temp', 'r')], + 'array with null' => [['valid', null]], + 'array with object' => [['valid', new stdClass]], + 'array with resource' => [['valid', fopen('php://temp', 'r')]], + 'array with nested array' => [['valid', ['nested']]], + ]; + } + public function testAfterResponse() { $this->factory->fake([ From 02a6eb6cdf46929d867485400c76800b89f01713 Mon Sep 17 00:00:00 2001 From: Peter Bishop <9081809+PeteBishwhip@users.noreply.github.com> Date: Thu, 28 May 2026 00:26:29 +0100 Subject: [PATCH 474/596] Report MultipleRecordsFoundException from sole() (#60294) --- src/Illuminate/Foundation/Exceptions/Handler.php | 2 -- tests/Foundation/FoundationExceptionsHandlerTest.php | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 811dc10bedc2..fde694da7971 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -17,7 +17,6 @@ use Illuminate\Contracts\Foundation\ExceptionRenderer; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\RecordNotFoundException; use Illuminate\Database\RecordsNotFoundException; use Illuminate\Foundation\Exceptions\Renderer\Renderer; @@ -161,7 +160,6 @@ class Handler implements ExceptionHandlerContract HttpException::class, HttpResponseException::class, ModelNotFoundException::class, - MultipleRecordsFoundException::class, OriginMismatchException::class, RecordNotFoundException::class, RecordsNotFoundException::class, diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index a02a144ff6ee..c031753e194c 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -14,6 +14,7 @@ use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract; use Illuminate\Contracts\Support\Responsable; use Illuminate\Contracts\View\Factory as ViewFactory; +use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\RecordsNotFoundException; use Illuminate\Foundation\Exceptions\Handler; use Illuminate\Foundation\Testing\Concerns\InteractsWithExceptionHandling; @@ -393,6 +394,15 @@ public function testRecordsNotFoundReturns404WithoutReporting() $this->handler->report(new RecordsNotFoundException); } + public function testMultipleRecordsFoundIsReported() + { + $logger = m::mock(LoggerInterface::class); + $this->container->instance(LoggerInterface::class, $logger); + $logger->shouldReceive('error')->withArgs(['2 records were found.', m::hasKey('exception')])->once(); + + $this->handler->report(new MultipleRecordsFoundException(2)); + } + public function testItReturnsSpecificErrorViewIfExists() { $viewFactory = m::mock(ViewFactory::class); From b8f2341e8c7467406d66bb40f7f9ad2ee0aa6925 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 28 May 2026 00:28:12 +0100 Subject: [PATCH 475/596] Preserve empty HTTP attach contents (#60291) --- src/Illuminate/Http/Client/PendingRequest.php | 11 +++++-- tests/Http/HttpClientTest.php | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index f850495c9873..6ee9587fac4e 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -351,12 +351,17 @@ public function attach($name, $contents = '', $filename = null, array $headers = $this->asMultipart(); - $this->pendingFiles[] = array_filter([ + $file = [ 'name' => $name, 'contents' => $contents, 'headers' => $headers, - 'filename' => $filename, - ]); + ]; + + if ($filename !== null) { + $file['filename'] = $filename; + } + + $this->pendingFiles[] = $file; return $this; } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 6e5040d77a43..0d0e339bfb31 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -795,6 +795,35 @@ public function testFilesCanBeAttached() }); } + public function testAttachPreservesEmptyContents() + { + $this->factory->fake(); + + $this->factory->attach('file')->post('http://foo.com/file'); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/file' + && $request->isMultipart() + && $request[0]['name'] === 'file' + && array_key_exists('contents', $request[0]) + && $request[0]['contents'] === ''; + }); + } + + public function testAttachPreservesFalseyStringContentsAndName() + { + $this->factory->fake(); + + $this->factory->attach('0', '0')->post('http://foo.com/file'); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/file' + && $request->isMultipart() + && $request[0]['name'] === '0' + && $request[0]['contents'] === '0'; + }); + } + public function testCanSendMultipartDataWithSimplifiedParameters() { $this->factory->fake(); From b5547f6e74440344a4d38c6105dab7e0cd2078cc Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Wed, 27 May 2026 19:29:09 -0400 Subject: [PATCH 476/596] hint the unit (#60289) Clarify the parameter description for uniqueFor. --- src/Illuminate/Queue/Attributes/UniqueFor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Attributes/UniqueFor.php b/src/Illuminate/Queue/Attributes/UniqueFor.php index 0dfece3cbf29..a1cabcb2b51c 100644 --- a/src/Illuminate/Queue/Attributes/UniqueFor.php +++ b/src/Illuminate/Queue/Attributes/UniqueFor.php @@ -10,7 +10,7 @@ class UniqueFor /** * Create a new attribute instance. * - * @param int $uniqueFor + * @param int $uniqueFor Seconds to consider the queueable unique for. */ public function __construct(public int $uniqueFor) { From 06a9f54e4c176a813ee33491a8d0ea83ab6dbf33 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 28 May 2026 08:36:39 +0900 Subject: [PATCH 477/596] port feature to 13.x --- src/Illuminate/Http/Client/PendingRequest.php | 11 +++++-- tests/Http/HttpClientTest.php | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 03257925a52b..0b5518bb4170 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -352,12 +352,17 @@ public function attach($name, $contents = '', $filename = null, array $headers = $this->asMultipart(); - $this->pendingFiles[] = array_filter([ + $file = [ 'name' => $name, 'contents' => $contents, 'headers' => $headers, - 'filename' => $filename, - ]); + ]; + + if ($filename !== null) { + $file['filename'] = $filename; + } + + $this->pendingFiles[] = $file; return $this; } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 33f84f98764d..b83a81f5941b 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -874,6 +874,35 @@ public function testFilesCanBeAttached() }); } + public function testAttachPreservesEmptyContents() + { + $this->factory->fake(); + + $this->factory->attach('file')->post('http://foo.com/file'); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/file' + && $request->isMultipart() + && $request[0]['name'] === 'file' + && array_key_exists('contents', $request[0]) + && $request[0]['contents'] === ''; + }); + } + + public function testAttachPreservesFalseyStringContentsAndName() + { + $this->factory->fake(); + + $this->factory->attach('0', '0')->post('http://foo.com/file'); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/file' + && $request->isMultipart() + && $request[0]['name'] === '0' + && $request[0]['contents'] === '0'; + }); + } + public function testAttachHeaderValuesAreSerialized() { $this->factory->fake(); From 21264ff79e85388f8586a21ca7e218b5c6f460c1 Mon Sep 17 00:00:00 2001 From: Christos Koumpis <56029580+Button99@users.noreply.github.com> Date: Thu, 28 May 2026 02:42:11 +0300 Subject: [PATCH 478/596] [13.x] Fix inverted ratio comparison operators in image dimension validation (#60290) * fix inverted dimensions min and max ratio validations * more fixes --------- Co-authored-by: Taylor Otwell --- .../Concerns/ValidatesAttributes.php | 8 +++++-- .../ValidationImageFileRuleTest.php | 23 ++++++++++++++----- tests/Validation/ValidationValidatorTest.php | 10 ++++---- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 32bd38dc2d5f..28efc707ce17 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -850,7 +850,9 @@ private function failsMinRatioCheck($parameters, $width, $height) [1, 1], array_filter(sscanf($parameters['min_ratio'], '%f/%d')) ); - return ($width / $height) > ($minNumerator / $minDenominator); + $precision = 1 / (max(($width + $height) / 2, $height) + 1); + + return ($minNumerator / $minDenominator) - ($width / $height) > $precision; } /** @@ -871,7 +873,9 @@ private function failsMaxRatioCheck($parameters, $width, $height) [1, 1], array_filter(sscanf($parameters['max_ratio'], '%f/%d')) ); - return ($width / $height) < ($maxNumerator / $maxDenominator); + $precision = 1 / (max(($width + $height) / 2, $height) + 1); + + return ($width / $height) - ($maxNumerator / $maxDenominator) > $precision; } /** diff --git a/tests/Validation/ValidationImageFileRuleTest.php b/tests/Validation/ValidationImageFileRuleTest.php index 957478e6d12a..3f2ac90505b4 100644 --- a/tests/Validation/ValidationImageFileRuleTest.php +++ b/tests/Validation/ValidationImageFileRuleTest.php @@ -60,42 +60,53 @@ public function testDimensionWithTheRatioMethod() public function testDimensionWithTheMinRatioMethod() { - $this->fails( + $this->passes( File::image()->dimensions(Rule::dimensions()->minRatio(1 / 2)), UploadedFile::fake()->image('foo.png', 100, 100), - ['validation.dimensions'], ); $this->passes( + File::image()->dimensions(Rule::dimensions()->minRatio(2 / 3)), + UploadedFile::fake()->image('foo.png', 200, 300), + ); + + $this->fails( File::image()->dimensions(Rule::dimensions()->minRatio(1 / 2)), - UploadedFile::fake()->image('foo.png', 100, 200), + UploadedFile::fake()->image('foo.png', 100, 300), + ['validation.dimensions'], ); } public function testDimensionWithTheMaxRatioMethod() { - $this->fails( + $this->passes( File::image()->dimensions(Rule::dimensions()->maxRatio(1 / 2)), UploadedFile::fake()->image('foo.png', 100, 300), ['validation.dimensions'], ); $this->passes( + File::image()->dimensions(Rule::dimensions()->maxRatio(1 / 3)), + UploadedFile::fake()->image('foo.png', 100, 300), + ); + + $this->fails( File::image()->dimensions(Rule::dimensions()->maxRatio(1 / 2)), UploadedFile::fake()->image('foo.png', 100, 100), + ['validation.dimensions'], ); } public function testDimensionWithTheRatioBetweenMethod() { $this->fails( - File::image()->dimensions(Rule::dimensions()->ratioBetween(1 / 2, 1 / 3)), + File::image()->dimensions(Rule::dimensions()->ratioBetween(1 / 3, 1 / 2)), UploadedFile::fake()->image('foo.png', 100, 100), ['validation.dimensions'], ); $this->passes( - File::image()->dimensions(Rule::dimensions()->ratioBetween(1 / 2, 1 / 3)), + File::image()->dimensions(Rule::dimensions()->ratioBetween(1 / 3, 1 / 2)), UploadedFile::fake()->image('foo.png', 100, 200), ); } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index ea3ade02b330..805aea4394db 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -5491,11 +5491,11 @@ public function testValidateImageDimensions() // for min_ratio $v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:min_ratio=1/2']); - $this->assertTrue($v->fails()); + $this->assertTrue($v->passes()); // for max_ratio $v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:max_ratio=2/5']); - $this->assertTrue($v->passes()); + $this->assertTrue($v->fails()); // Knowing that demo image2.png has width = 4 and height = 2 $uploadedFile = new UploadedFile(__DIR__.'/fixtures/image2.png', '', null, null, true); @@ -5556,11 +5556,11 @@ public function testValidateImageDimensions() // evaluates to (64 / 65) > (1 / 1.0) which is true/fails $v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:min_ratio=1']); - $this->assertFalse($v->fails()); + $this->assertTrue($v->fails()); - // evaluates to (64 / 65) < (1 / 1.0) which is false/passes + // evaluates to (64 / 65) > (1 / 1.0) which is false/passes $v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:max_ratio=1']); - $this->assertFalse($v->passes()); + $this->assertTrue($v->passes()); // Knowing that demo image5.png has width = 1366 and height = 768 $uploadedFile = new UploadedFile(__DIR__.'/fixtures/image5.png', '', null, null, true); From ae664e3dd3aaad74651e70dae45810c4b151bcf8 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 28 May 2026 00:47:19 +0100 Subject: [PATCH 479/596] [13.x] Allow scheduler to opt out of pause and interrupt cache checks (#60226) * [13.x] allow Schedule to opt out of pause * method like worker * dumb * Update Schedule.php --------- Co-authored-by: Taylor Otwell --- .../Console/Scheduling/Schedule.php | 27 +++++++++++++++++++ .../Console/Scheduling/ScheduleRunCommand.php | 8 ++++++ 2 files changed, 35 insertions(+) diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 1956160421d1..cd1baa1544dd 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -101,6 +101,20 @@ class Schedule */ protected array $groupStack = []; + /** + * Indicates if the schedule should check for the paused signal in the cache. + * + * @var bool + */ + public static $pausable = true; + + /** + * Indicates if the schedule should check for the interrupt signal in the cache. + * + * @var bool + */ + public static $interruptible = true; + /** * Create a new schedule instance. * @@ -489,6 +503,19 @@ protected function getDispatcher() return $this->dispatcher; } + /** + * Indicate that the scheduler should not poll for pause or interrupt signals. + * + * This prevents the scheduler from hitting the application cache to determine if it needs to pause or interrupt. + * + * @return void + */ + public static function withoutInterruptionPolling() + { + static::$pausable = false; + static::$interruptible = false; + } + /** * Dynamically handle calls into the schedule instance. * diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index eff60b53e5a9..4e7af5d728a2 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -296,6 +296,10 @@ protected function repeatEvents($events) */ protected function isPaused() { + if (! Schedule::$pausable) { + return false; + } + return $this->cache->get('illuminate:schedule:paused', false); } @@ -306,6 +310,10 @@ protected function isPaused() */ protected function shouldInterrupt() { + if (! Schedule::$interruptible) { + return false; + } + return $this->cache->get('illuminate:schedule:interrupt', false); } From 70054d7cad98f28542fbfd6e7d17f4e2a4321bb4 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 27 May 2026 23:47:52 +0000 Subject: [PATCH 480/596] Update facade docblocks --- src/Illuminate/Support/Facades/Schedule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php index ad0779f31a68..484ac6795e13 100644 --- a/src/Illuminate/Support/Facades/Schedule.php +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -16,6 +16,7 @@ * @method static \Illuminate\Console\Scheduling\Event[] events() * @method static \Illuminate\Console\Scheduling\Event[] eventsForEnvironments(array $environments) * @method static \Illuminate\Console\Scheduling\Schedule useCache(\UnitEnum|string $store) + * @method static void withoutInterruptionPolling() * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) * @method static bool hasMacro(string $name) From 673028105a7d0a4351a0b49e3f3afb6dbd4b3375 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Fri, 29 May 2026 02:59:05 +0330 Subject: [PATCH 481/596] Fix @params typo in Fluent and MessageBag toPrettyJson() docblocks (#60313) --- src/Illuminate/Support/Fluent.php | 3 +-- src/Illuminate/Support/MessageBag.php | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index a15085727086..75fc7e9ffb4a 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -206,8 +206,7 @@ public function toJson($options = 0) /** * Convert the fluent instance to pretty print formatted JSON. * - * @params int $options - * + * @param int $options * @return string */ public function toPrettyJson(int $options = 0) diff --git a/src/Illuminate/Support/MessageBag.php b/src/Illuminate/Support/MessageBag.php index 0868ff352a2c..983d5d8c00a1 100755 --- a/src/Illuminate/Support/MessageBag.php +++ b/src/Illuminate/Support/MessageBag.php @@ -434,8 +434,7 @@ public function toJson($options = 0) /** * Convert the object to pretty print formatted JSON. * - * @params int $options - * + * @param int $options * @return string */ public function toPrettyJson(int $options = 0) From c76283c7b543f70b816c3ae252c6c4211c461951 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Fri, 29 May 2026 03:13:21 +0330 Subject: [PATCH 482/596] Fix regex typo in Env::addVariableToEnvContents that prevented quoting values with special characters (#60312) --- src/Illuminate/Support/Env.php | 2 +- tests/Support/SupportHelpersTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Env.php b/src/Illuminate/Support/Env.php index 97fcb4637966..8889e7ad5cb2 100644 --- a/src/Illuminate/Support/Env.php +++ b/src/Illuminate/Support/Env.php @@ -187,7 +187,7 @@ protected static function addVariableToEnvContents(string $key, mixed $value, ar $prefix = explode('_', $key)[0].'_'; $lastPrefixIndex = -1; - $shouldQuote = preg_match('/^[a-zA-z0-9]+$/', $value) === 0; + $shouldQuote = preg_match('/^[a-zA-Z0-9]+$/', $value) === 0; $lineToAddVariations = [ $key.'='.(is_string($value) ? self::prepareQuotedValue($value) : $value), diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 8bb7c86467bd..ceca49fcdff1 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -1524,6 +1524,23 @@ public function testWriteVariableToFileAndOverwrite() ); } + public function testWriteVariableQuotesValuesWithSpecialCharacters() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, 'APP_NAME=Laravel'.PHP_EOL); + + Env::writeVariable('APP_BRACKET', 'pass[word', $path); + Env::writeVariable('APP_CARET', 'foo^bar', $path); + Env::writeVariable('APP_BACKTICK', 'foo`bar', $path); + + $contents = $filesystem->get($path); + + $this->assertStringContainsString('APP_BRACKET="pass[word"', $contents); + $this->assertStringContainsString('APP_CARET="foo^bar"', $contents); + $this->assertStringContainsString('APP_BACKTICK="foo`bar"', $contents); + } + public function testWillThrowAnExceptionIfFileIsMissingWhenTryingToWriteVariables(): void { $this->expectExceptionObject(new RuntimeException('The file [missing-file] does not exist.')); From ebd9c79edbd25648f10fb415ca6219077c224c2e Mon Sep 17 00:00:00 2001 From: Lynh Date: Fri, 29 May 2026 06:50:37 +0700 Subject: [PATCH 483/596] [13.x] Enhance Cache attribute to support memoization (#60309) * Enhance Cache attribute to support memoization in constructor and resolve method * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Container/Attributes/Cache.php | 10 +++++++--- tests/Container/ContextualAttributeBindingTest.php | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Container/Attributes/Cache.php b/src/Illuminate/Container/Attributes/Cache.php index 0b8636848ddb..92058850da9b 100644 --- a/src/Illuminate/Container/Attributes/Cache.php +++ b/src/Illuminate/Container/Attributes/Cache.php @@ -13,8 +13,10 @@ class Cache implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public UnitEnum|string|null $store = null) - { + public function __construct( + public UnitEnum|string|null $store = null, + public bool $memo = false, + ) { } /** @@ -26,6 +28,8 @@ public function __construct(public UnitEnum|string|null $store = null) */ public static function resolve(self $attribute, Container $container) { - return $container->make('cache')->store($attribute->store); + return $attribute->memo + ? $container->make('cache')->memo($attribute->store) + : $container->make('cache')->store($attribute->store); } } diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index c357b8fbde94..88da22176e07 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -173,6 +173,8 @@ public function testCacheAttribute() $manager->shouldReceive('store')->with('bar')->andReturn(m::mock(CacheRepository::class)); $manager->shouldReceive('store')->with(CacheStoreUnitEnum::unit)->andReturn(m::mock(CacheRepository::class)); $manager->shouldReceive('store')->with(CacheStoreBackedEnum::Backed)->andReturn(m::mock(CacheRepository::class)); + $manager->shouldReceive('memo')->with('foo')->andReturn(m::mock(CacheRepository::class)); + $manager->shouldReceive('memo')->with('bar')->andReturn(m::mock(CacheRepository::class)); return $manager; }); @@ -531,6 +533,8 @@ public function __construct( #[Cache('bar')] CacheRepository $bar, #[Cache(CacheStoreUnitEnum::unit)] CacheRepository $unit, #[Cache(CacheStoreBackedEnum::Backed)] CacheRepository $backed, + #[Cache('foo', memo: true)] CacheRepository $fooMemoized, + #[Cache('bar', memo: true)] CacheRepository $barMemoized, ) { } } From 6c95b95ff7fcb086c23c5f483caf59628015fe6d Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Thu, 28 May 2026 20:07:35 -0400 Subject: [PATCH 484/596] [13.x] Indicate an event was skipped (#60311) * indicate when skipped * tests * Update Event.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Console/Scheduling/Event.php | 11 ++++ tests/Console/Scheduling/EventTest.php | 61 +++++++++++++++++++ .../Scheduling/ScheduleRunCommandTest.php | 46 ++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 6463fb9575ae..b4d04b4a319b 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -91,6 +91,13 @@ class Event */ public $exitCode; + /** + * Indicates whether the execution was skipped due to the mutex already being reserved. + * + * @var bool + */ + public $skippedBecauseOverlapping = false; + /** * Create a new event instance. * @@ -127,7 +134,11 @@ public function getDefaultOutput() */ public function run(Container $container) { + $this->skippedBecauseOverlapping = false; + if ($this->shouldSkipDueToOverlapping()) { + $this->skippedBecauseOverlapping = true; + return; } diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 1ef62be6fd26..df6df6a22f9a 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -222,6 +222,67 @@ public function __invoke(): bool $this->assertSame(1, $reject->calls); } + public function testRunIndicatesWhenSkippedBecauseOverlapping() + { + $container = new Container; + $beforeCallbackCalled = false; + $mutex = m::mock(EventMutex::class); + $event = new class($mutex, 'php -i') extends Event + { + public $executed = false; + + protected function execute($container) + { + $this->executed = true; + + return 0; + } + }; + + $event->withoutOverlapping(); + $event->before(function () use (&$beforeCallbackCalled) { + $beforeCallbackCalled = true; + }); + + $mutex->shouldReceive('create')->once()->with($event)->andReturn(false); + + $event->run($container); + + $this->assertTrue($event->skippedBecauseOverlapping); + $this->assertFalse($event->executed); + $this->assertFalse($beforeCallbackCalled); + } + + public function testRunResetsSkippedBecauseOverlapping() + { + $container = new Container; + $mutex = m::mock(EventMutex::class); + $event = new class($mutex, 'php -i') extends Event + { + public $executions = 0; + + protected function execute($container) + { + $this->executions++; + + return 0; + } + }; + + $event->withoutOverlapping(); + + $mutex->shouldReceive('create')->twice()->with($event)->andReturn(false, true); + $mutex->shouldReceive('forget')->once()->with($event); + + $event->run($container); + $this->assertTrue($event->skippedBecauseOverlapping); + + $event->run($container); + + $this->assertFalse($event->skippedBecauseOverlapping); + $this->assertSame(1, $event->executions); + } + public function testDaysOfMonthMethod() { $event = new Event(m::mock(EventMutex::class), 'php -i'); diff --git a/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php index 796e497d0ab5..0db37a036aba 100644 --- a/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php @@ -5,6 +5,7 @@ use Illuminate\Console\Events\ScheduledTaskFailed; use Illuminate\Console\Events\ScheduledTaskFinished; use Illuminate\Console\Events\ScheduledTaskStarting; +use Illuminate\Console\Scheduling\EventMutex; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\ScheduleRunCommand; use Illuminate\Contracts\Container\BindingResolutionException; @@ -185,6 +186,51 @@ public function test_successful_command_in_background_does_not_trigger_event() Event::assertNotDispatched(ScheduledTaskFailed::class); } + public function test_overlapping_task_finished_event_indicates_skipped() + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + $this->app->instance(EventMutex::class, new class implements EventMutex + { + public function create(\Illuminate\Console\Scheduling\Event $event) + { + return false; + } + + public function exists(\Illuminate\Console\Scheduling\Event $event) + { + return false; + } + + public function forget(\Illuminate\Console\Scheduling\Event $event) + { + // + } + }); + + $ran = false; + $schedule = $this->app->make(Schedule::class); + $task = $schedule->call(function () use (&$ran) { + $ran = true; + })->name('test')->withoutOverlapping()->everyMinute(); + + $this->artisan('schedule:run'); + + Event::assertDispatched(ScheduledTaskStarting::class, function ($event) use ($task) { + return $event->task === $task; + }); + Event::assertDispatched(ScheduledTaskFinished::class, function ($event) use ($task) { + return $event->task === $task && + $event->task->skippedBecauseOverlapping === true; + }); + Event::assertNotDispatched(ScheduledTaskFailed::class); + $this->assertFalse($ran); + } + /** * @throws BindingResolutionException */ From 55d9fb855f93776c1e56769c4bf781bae05d39fa Mon Sep 17 00:00:00 2001 From: Amirhf Date: Sat, 30 May 2026 02:57:37 +0330 Subject: [PATCH 485/596] fix(Number): return INF/NAN as-is in trim() (#60322) --- src/Illuminate/Support/Number.php | 4 ++++ tests/Support/SupportNumberTest.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 8ee7966fab0d..aaa9bd541341 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -339,6 +339,10 @@ public static function pairs(int|float $to, int|float $by, int|float $start = 0, */ public static function trim(int|float $number) { + if (is_infinite($number) || is_nan($number)) { + return $number; + } + return json_decode(json_encode($number)); } diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 0122bfdae5d0..c9b938a5027f 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -340,6 +340,9 @@ public function testTrim() $this->assertSame(12.3, Number::trim(12.30)); $this->assertSame(12.3456789, Number::trim(12.3456789)); $this->assertSame(12.3456789, Number::trim(12.34567890000)); + $this->assertSame(INF, Number::trim(INF)); + $this->assertSame(-INF, Number::trim(-INF)); + $this->assertNan(Number::trim(NAN)); } #[RequiresPhpExtension('intl')] From 72fbc2343d0d8f83d1e462284522d03e7c07c78c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sat, 30 May 2026 07:30:38 +0800 Subject: [PATCH 486/596] [13.x] Fix `Illuminate\Http\Response` compatibility against Symfony 8.1 (#60318) * [13.x] Fix `Illuminate\Http\Response` compatibility against Symfony 8.1 Signed-off-by: Mior Muhammad Zaki * Ensure support for old structure as well Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: jnoordsij --- src/Illuminate/Http/Response.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Response.php b/src/Illuminate/Http/Response.php index 6576f47f6a14..6888471574cb 100755 --- a/src/Illuminate/Http/Response.php +++ b/src/Illuminate/Http/Response.php @@ -29,11 +29,15 @@ class Response extends SymfonyResponse */ public function __construct($content = '', $status = 200, array $headers = []) { - $this->headers = new ResponseHeaderBag($headers); + if (method_exists($this, 'setHeaders')) { + parent::__construct('', $status, new ResponseHeaderBag($headers)); + } else { + $this->headers = new ResponseHeaderBag($headers); + $this->setStatusCode($status); + $this->setProtocolVersion('1.0'); + } $this->setContent($content); - $this->setStatusCode($status); - $this->setProtocolVersion('1.0'); } /** From 1bfaa468e98ffe737c75c0c4959e9652a944d048 Mon Sep 17 00:00:00 2001 From: Erfan Momeni Date: Sat, 30 May 2026 03:01:52 +0330 Subject: [PATCH 487/596] fix: fix isUniqueConstraintError to catch SQL Server error 2627 (#60320) --- src/Illuminate/Database/SqlServerConnection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index b18f97cb1f23..d79413774f86 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -81,9 +81,9 @@ protected function escapeBinary($value) * @param \Exception $exception * @return bool */ - protected function isUniqueConstraintError(Exception $exception) + protected function isUniqueConstraintError(Exception $exception): bool { - return (bool) preg_match('#Cannot insert duplicate key row in object#i', $exception->getMessage()); + return (bool) preg_match('#Cannot insert duplicate key(?: row)? in object#i', $exception->getMessage()); } /** From e854b8c9addc510733daa17f2bac4b82bbb9dd05 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Sat, 30 May 2026 00:32:40 +0100 Subject: [PATCH 488/596] Fix FIFO queue name normalization in Cloud managed queues (#60316) Co-authored-by: Claude Opus 4.8 (1M context) --- src/Illuminate/Foundation/Cloud/Queue.php | 4 +++- tests/Foundation/Cloud/QueueTest.php | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php index 6490a717b863..5ffdb8232804 100644 --- a/src/Illuminate/Foundation/Cloud/Queue.php +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -366,7 +366,9 @@ public function normalizeQueue($queue) return Str::of($this->queue->getQueue($queue)) ->when($prefix, fn ($str) => $str->chopStart($prefix.'/')) - ->when($suffix, fn ($str) => $str->chopEnd($suffix)) + ->when($suffix, fn ($str) => $str->endsWith('.fifo') + ? $str->chopEnd('.fifo')->chopEnd($suffix)->append('.fifo') + : $str->chopEnd($suffix)) ->toString(); } diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 8ed19dde47a3..af004fee1a59 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -960,6 +960,22 @@ public function testItUsesConfigValuesToNormalizeQueueName() $this->assertSame('my-queue', $eventsFake->emitted[0]['queue']); } + public function testItNormalizesFifoQueueNamesWithoutLeakingTheSuffix() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); + + $queue->push(new FakeJob, queue: 'orders.fifo'); + + // The suffix is injected before the ".fifo" extension on the real SQS + // queue name, so the normalized name must strip it back out and keep + // the ".fifo" terminal rather than reporting "orders-env-....fifo". + $this->assertSame('orders.fifo', $eventsFake->emitted[0]['queue']); + } + /** * @return array{Queue, MockInterface} */ From 88c5851619024ecc99dde5d43259ed0471992251 Mon Sep 17 00:00:00 2001 From: Kieran Brown Date: Sat, 30 May 2026 00:32:52 +0100 Subject: [PATCH 489/596] Fix FIFO queue name normalization in Cloud managed queues (#60315) Co-authored-by: Claude Opus 4.8 (1M context) --- src/Illuminate/Foundation/Cloud/Queue.php | 4 +++- tests/Foundation/Cloud/QueueTest.php | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Cloud/Queue.php b/src/Illuminate/Foundation/Cloud/Queue.php index 6490a717b863..5ffdb8232804 100644 --- a/src/Illuminate/Foundation/Cloud/Queue.php +++ b/src/Illuminate/Foundation/Cloud/Queue.php @@ -366,7 +366,9 @@ public function normalizeQueue($queue) return Str::of($this->queue->getQueue($queue)) ->when($prefix, fn ($str) => $str->chopStart($prefix.'/')) - ->when($suffix, fn ($str) => $str->chopEnd($suffix)) + ->when($suffix, fn ($str) => $str->endsWith('.fifo') + ? $str->chopEnd('.fifo')->chopEnd($suffix)->append('.fifo') + : $str->chopEnd($suffix)) ->toString(); } diff --git a/tests/Foundation/Cloud/QueueTest.php b/tests/Foundation/Cloud/QueueTest.php index 8ed19dde47a3..af004fee1a59 100644 --- a/tests/Foundation/Cloud/QueueTest.php +++ b/tests/Foundation/Cloud/QueueTest.php @@ -960,6 +960,22 @@ public function testItUsesConfigValuesToNormalizeQueueName() $this->assertSame('my-queue', $eventsFake->emitted[0]['queue']); } + public function testItNormalizesFifoQueueNamesWithoutLeakingTheSuffix() + { + Cloud::configureManagedQueues($this->app); + Cloud::bootManagedQueues($this->app); + $eventsFake = $this->fakeEvents(); + [$queue, $client] = $this->mockedQueue(); + $client->shouldReceive('sendMessage')->times(1)->andReturn(new Result()); + + $queue->push(new FakeJob, queue: 'orders.fifo'); + + // The suffix is injected before the ".fifo" extension on the real SQS + // queue name, so the normalized name must strip it back out and keep + // the ".fifo" terminal rather than reporting "orders-env-....fifo". + $this->assertSame('orders.fifo', $eventsFake->emitted[0]['queue']); + } + /** * @return array{Queue, MockInterface} */ From 4d83904b41fe668d759272b2eacab6cad6b7d5c0 Mon Sep 17 00:00:00 2001 From: Adrian <53199186+ahawlitschek@users.noreply.github.com> Date: Sat, 30 May 2026 01:38:04 +0200 Subject: [PATCH 490/596] fix: `whereDate` and `whereTime` crash when $column is an `Expression` (#60314) The `->whereDate` and `->whereTime` functions are typed to accept `Expression` or string as first parameter (`$column`). Since Laravel 12, both underlying functions in `PostgresGrammar` checks whether the `$column` is a JSON selector using the `isJsonSelector($value)` function defined in the base grammar. This function uses `str_contains` and causes a runtime exception if it receives something other than string. Currently, the function is called with the origin value of the `$column` parameter which in some cases may be `Expression`. This changes the parameter from the `isJsonSelector` to the wrapped column instead of the original one. It also adds tests which check, that using `Expression` or `Stringable` in `whereDate` and `whereTime` produces proper SQL. --- .../Query/Grammars/PostgresGrammar.php | 4 ++-- tests/Database/DatabaseQueryBuilderTest.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index f12b4d225ee9..3aa18bf08be9 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -111,7 +111,7 @@ protected function whereDate(Builder $query, $where) $column = $this->wrap($where['column']); $value = $this->parameter($where['value']); - if ($this->isJsonSelector($where['column'])) { + if ($this->isJsonSelector($column)) { $column = '('.$column.')'; } @@ -130,7 +130,7 @@ protected function whereTime(Builder $query, $where) $column = $this->wrap($where['column']); $value = $this->parameter($where['value']); - if ($this->isJsonSelector($where['column'])) { + if ($this->isJsonSelector($column)) { $column = '('.$column.')'; } diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index e5ddd15fd914..17b9fd0a3748 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -29,6 +29,7 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use Illuminate\Tests\Database\Fixtures\Enums\Bar; use InvalidArgumentException; use Mockery as m; @@ -617,6 +618,14 @@ public function testWhereDatePostgres() $builder = $this->getPostgresBuilder(); $builder->select('*')->from('users')->whereDate('result->created_at', new Raw('NOW()')); $this->assertSame('select * from "users" where ("result"->>\'created_at\')::date = NOW()', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->whereDate(new Raw('COALESCE(created_at, updated_at)'), new Raw('NOW()')); + $this->assertSame('select * from "users" where COALESCE(created_at, updated_at)::date = NOW()', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->whereDate(Str::of('result->created_at'), new Raw('NOW()')); + $this->assertSame('select * from "users" where ("result"->>\'created_at\')::date = NOW()', $builder->toSql()); } public function testWhereDayPostgres() @@ -654,6 +663,16 @@ public function testWhereTimePostgres() $builder->select('*')->from('users')->whereTime('result->created_at', '>=', '22:00'); $this->assertSame('select * from "users" where ("result"->>\'created_at\')::time >= ?', $builder->toSql()); $this->assertEquals([0 => '22:00'], $builder->getBindings()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->whereTime(new Raw('COALESCE(created_at, updated_at)'), '>=', '22:00'); + $this->assertSame('select * from "users" where COALESCE(created_at, updated_at)::time >= ?', $builder->toSql()); + $this->assertEquals([0 => '22:00'], $builder->getBindings()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->whereTime(Str::of('result->created_at'), '>=', '22:00'); + $this->assertSame('select * from "users" where ("result"->>\'created_at\')::time >= ?', $builder->toSql()); + $this->assertEquals([0 => '22:00'], $builder->getBindings()); } public function testWherePast() From 0f326e485d3f96579fda072edb765fbec06144ad Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sat, 30 May 2026 07:16:21 +0100 Subject: [PATCH 491/596] [13.x] Introduce Bus::dispatchBulk() (#60297) * new method * fake * bulk ofc * tests * oops * rename * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/Dispatcher.php | 37 +++++++++++++++++++ .../Support/Testing/Fakes/BusFake.php | 13 +++++++ tests/Bus/BusDispatcherTest.php | 28 ++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/Illuminate/Bus/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index 89e1e008dbcd..ba4f7b6811de 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -140,6 +140,43 @@ public function dispatchNow($command, $handler = null) return $this->pipeline->send($command)->through($this->pipes)->then($callback); } + /** + * Dispatch multiple commands in bulk to their appropriate handlers on the queue. + * + * @param iterable $jobs + * @return void + */ + public function bulk($jobs) + { + $groups = []; + + foreach ($jobs as $job) { + if (! $this->queueResolver || ! $this->commandShouldBeQueued($job)) { + $this->dispatchNow($job); + + continue; + } + + $connection = $this->getAttributeValue($job, Connection::class, 'connection') + ?? $this->resolveConnectionFromQueueRoute($job) + ?? null; + + $queue = $this->getAttributeValue($job, QueueAttribute::class, 'queue') + ?? $this->resolveQueueFromQueueRoute($job) + ?? null; + + $groups[$connection.':'.$queue]['connection'] = $connection; + $groups[$connection.':'.$queue]['queue'] = $queue; + $groups[$connection.':'.$queue]['jobs'][] = $job; + } + + foreach ($groups as $group) { + ($this->queueResolver)($group['connection'])->bulk( + $group['jobs'], '', $group['queue'] + ); + } + } + /** * Attempt to find the batch with the given ID. * diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index fb822a36f64b..81342471616c 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -736,6 +736,19 @@ public function dispatchAfterResponse($command, $handler = null) } } + /** + * Dispatch multiple commands in bulk to their appropriate handlers on the queue. + * + * @param iterable $jobs + * @return void + */ + public function bulk($jobs) + { + foreach ($jobs as $job) { + $this->dispatch($job); + } + } + /** * Create a new chain of queueable jobs. * diff --git a/tests/Bus/BusDispatcherTest.php b/tests/Bus/BusDispatcherTest.php index c71100c2586e..be09a0dc025d 100644 --- a/tests/Bus/BusDispatcherTest.php +++ b/tests/Bus/BusDispatcherTest.php @@ -149,6 +149,29 @@ public function testOnConnectionOnJobWhenDispatching() Container::setInstance(null); } + + public function testDispatchBulk() + { + $container = new Container; + $container->instance('queue.routes', $queueRoutes = m::mock()); + $queueRoutes->shouldReceive('getQueue')->andReturn(null); + $queueRoutes->shouldReceive('getConnection')->andReturn(null); + Container::setInstance($container); + + $mock = m::mock(Queue::class); + $mock->shouldReceive('bulk')->once()->with(m::on(fn ($jobs) => count($jobs) === 2), '', null); + $mock->shouldReceive('bulk')->once()->with(m::on(fn ($jobs) => count($jobs) === 1), '', 'high'); + + $dispatcher = new Dispatcher($container, fn () => $mock); + + $dispatcher->bulk([ + new BusDispatcherQueueable, + new BusDispatcherQueueable, + new BusDispatcherTestSpecificQueueCommand, + ]); + + Container::setInstance(null); + } } class BusInjectionStub @@ -185,6 +208,11 @@ class BusDispatcherTestSpecificQueueAndDelayCommand implements ShouldQueue public $delay = 10; } +class BusDispatcherTestSpecificQueueCommand implements ShouldQueue +{ + public $queue = 'high'; +} + class BusDispatcherQueueable implements ShouldQueue { use Queueable; From 7044b41d30a9628cdfbd21a8cec39f7e8712996a Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Sat, 30 May 2026 06:16:50 +0000 Subject: [PATCH 492/596] Update facade docblocks --- src/Illuminate/Support/Facades/Bus.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index 9894bc405900..0656af30f3fd 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -11,6 +11,7 @@ * @method static mixed dispatch(mixed $command) * @method static mixed dispatchSync(mixed $command, mixed $handler = null) * @method static mixed dispatchNow(mixed $command, mixed $handler = null) + * @method static void bulk(iterable $jobs) * @method static \Illuminate\Bus\Batch|null findBatch(string $batchId) * @method static \Illuminate\Bus\PendingBatch batch(\Illuminate\Support\Collection|mixed $jobs) * @method static \Illuminate\Foundation\Bus\PendingChain chain(\Illuminate\Support\Collection|array|null $jobs = null) From 5740ab250343c6aeda076fc8a41543437f909fe8 Mon Sep 17 00:00:00 2001 From: KentarouTakeda <4785040+KentarouTakeda@users.noreply.github.com> Date: Mon, 1 Jun 2026 07:00:38 +0900 Subject: [PATCH 493/596] [13.x] Fix time-sensitive flaky test in NotificationDatabaseChannelTest (#60333) --- .../NotificationDatabaseChannelTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Notifications/NotificationDatabaseChannelTest.php b/tests/Notifications/NotificationDatabaseChannelTest.php index 9a2c5b6b2580..6119e18189ac 100644 --- a/tests/Notifications/NotificationDatabaseChannelTest.php +++ b/tests/Notifications/NotificationDatabaseChannelTest.php @@ -11,6 +11,20 @@ class NotificationDatabaseChannelTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + Carbon::setTestNow(Carbon::now()); + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + + parent::tearDown(); + } + public function testDatabaseChannelCreatesDatabaseRecordWithProperData() { $notification = new NotificationDatabaseChannelTestNotification; From a417b01b37e21bf52cbf9345bb04d6e1059df26f Mon Sep 17 00:00:00 2001 From: Pablo Gonzalez Date: Mon, 1 Jun 2026 00:01:59 +0200 Subject: [PATCH 494/596] fix: Add content_id to inline attachment handling in CloudflareTransport (#60330) CloudFlare requires the `content_id` key to be set for inline attachments. See docs: https://developers.cloudflare.com/api/resources/email_sending/methods/send/ If missing, it fails with Symfony\Component\Mailer\Exception\TransportException: email.sending.error.invalid_request_schema This solution uses the same idea as src/Illuminate/Mail/Transport/ResendTransport.php:94 which also requires `content_id`. Besides the test, I have also manually sent an email with an inline image and confirmed the image is shown in the email I received. --- .../Mail/Transport/CloudflareTransport.php | 14 ++++++-- tests/Mail/MailCloudflareTransportTest.php | 34 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Mail/Transport/CloudflareTransport.php b/src/Illuminate/Mail/Transport/CloudflareTransport.php index e96cb833e71e..2681df1197d2 100644 --- a/src/Illuminate/Mail/Transport/CloudflareTransport.php +++ b/src/Illuminate/Mail/Transport/CloudflareTransport.php @@ -131,13 +131,21 @@ protected function getAttachments(Email $email): array foreach ($email->getAttachments() as $attachment) { $headers = $attachment->getPreparedHeaders(); + $disposition = $headers->getHeaderBody('Content-Disposition') ?: 'attachment'; + $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); - $attachments[] = [ + $item = [ 'content' => str_replace("\r\n", '', $attachment->bodyToString()), - 'filename' => $headers->getHeaderParameter('Content-Disposition', 'filename'), + 'filename' => $filename, 'type' => $headers->get('Content-Type')->getBody(), - 'disposition' => $headers->getHeaderBody('Content-Disposition') ?: 'attachment', + 'disposition' => $disposition, ]; + + if ($disposition === 'inline') { + $item['content_id'] = $attachment->hasContentId() ? $attachment->getContentId() : $filename; + } + + $attachments[] = $item; } return $attachments; diff --git a/tests/Mail/MailCloudflareTransportTest.php b/tests/Mail/MailCloudflareTransportTest.php index 7bcb0af4703a..d773eb7f91b9 100644 --- a/tests/Mail/MailCloudflareTransportTest.php +++ b/tests/Mail/MailCloudflareTransportTest.php @@ -164,6 +164,40 @@ public function testSendWithAttachment(): void $this->assertNotEmpty($requestBody['attachments'][0]['content']); } + public function testSendWithInlineAttachment(): void + { + $requestBody = null; + + $client = new MockHttpClient(function ($method, $url, $options) use (&$requestBody) { + $requestBody = json_decode($options['body'], true); + + return new MockResponse(json_encode([ + 'success' => true, + 'errors' => [], + 'messages' => [], + 'result' => ['delivered' => ['me@example.com'], 'permanent_bounces' => [], 'queued' => []], + ]), ['http_code' => 200]); + }); + + $transport = new CloudflareTransport('test-account-id', 'test-key', $client); + + $message = new Email(); + $message->subject('With inline attachment'); + $message->text('See image'); + $message->sender('sender@example.com'); + $message->to('me@example.com'); + $message->embed('file contents', 'image.png', 'image/png'); + + $transport->send($message); + + $this->assertCount(1, $requestBody['attachments']); + $this->assertSame('image.png', $requestBody['attachments'][0]['filename']); + $this->assertSame('image/png', $requestBody['attachments'][0]['type']); + $this->assertSame('inline', $requestBody['attachments'][0]['disposition']); + $this->assertSame('image.png', $requestBody['attachments'][0]['content_id']); + $this->assertNotEmpty($requestBody['attachments'][0]['content']); + } + public function testSendThrowsOnApiFailure(): void { $client = new MockHttpClient(function () { From 7b57397ae208564529a8240039b2dd74ec22b2c7 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 31 May 2026 23:09:29 +0100 Subject: [PATCH 495/596] [13.x] Add payload to InspectedJob (#60326) * 13.x add payload to inspectedJob * cs * better test * cs * fake --- src/Illuminate/Queue/Jobs/InspectedJob.php | 5 ++++- .../Support/Testing/Fakes/QueueFake.php | 2 ++ .../Queue/QueueDatabaseQueueIntegrationTest.php | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Jobs/InspectedJob.php b/src/Illuminate/Queue/Jobs/InspectedJob.php index f4e98d62d8d0..1989e20ad022 100644 --- a/src/Illuminate/Queue/Jobs/InspectedJob.php +++ b/src/Illuminate/Queue/Jobs/InspectedJob.php @@ -12,13 +12,15 @@ class InspectedJob * @param string|null $uuid The unique identifier for the job. * @param string|null $name The display name of the job. * @param int $attempts The number of times the job has been attempted. + * @param array $payload * @param \Illuminate\Support\Carbon|null $createdAt The date and time the job was created. */ public function __construct( public readonly ?string $uuid, public readonly ?string $name, public readonly int $attempts, - public readonly ?Carbon $createdAt, + public readonly array $payload = [], + public readonly ?Carbon $createdAt = null, ) { } @@ -37,6 +39,7 @@ public static function fromPayload(string $payload, ?int $attempts = null): stat uuid: $decoded['uuid'] ?? null, name: $decoded['displayName'] ?? null, attempts: $attempts ?? $decoded['attempts'] ?? 0, + payload: $decoded, createdAt: isset($decoded['createdAt']) ? Carbon::createFromTimestamp($decoded['createdAt']) : null, ); } diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index fe3df9b8dc9e..e1d36b158fa5 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -493,6 +493,7 @@ public function pendingJobs($queue = null): Collection ? (method_exists($data['job'], 'displayName') ? $data['job']->displayName() : get_class($data['job'])) : $data['job'], attempts: 0, + payload: [], createdAt: null, )); } @@ -534,6 +535,7 @@ public function allPendingJobs(): Collection ? (method_exists($data['job'], 'displayName') ? $data['job']->displayName() : get_class($data['job'])) : $data['job'], attempts: 0, + payload: [], createdAt: null, )); } diff --git a/tests/Queue/QueueDatabaseQueueIntegrationTest.php b/tests/Queue/QueueDatabaseQueueIntegrationTest.php index eb764f81dab0..e480739bc8d8 100644 --- a/tests/Queue/QueueDatabaseQueueIntegrationTest.php +++ b/tests/Queue/QueueDatabaseQueueIntegrationTest.php @@ -10,6 +10,7 @@ use Illuminate\Queue\DatabaseQueue; use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\JobQueueing; +use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; use Illuminate\Support\Str; use PHPUnit\Framework\TestCase; @@ -250,6 +251,21 @@ public function testThatReservedJobsAreNotPopped() $this->assertNull($popped_job); } + public function testCustomPayloadIsExposedOnInspectedJob() + { + Queue::createPayloadUsing(function ($connection, $queue, $payload) { + return ['context' => ['tenant' => 'acme']]; + }); + + $this->queue->push('MyJob', []); + + $job = $this->queue->pendingJobs()->first(); + + $this->assertSame(['tenant' => 'acme'], $job->payload['context']); + + Queue::createPayloadUsing(null); + } + public function testJobPayloadIsAvailableOnEvents() { $jobQueueingEvent = null; From 33afd1e5541b57ceb367b0c560b81df87e0d592b Mon Sep 17 00:00:00 2001 From: Amirhf Date: Mon, 1 Jun 2026 01:40:17 +0330 Subject: [PATCH 496/596] [12.x] Fix Number::pairs() infinite loop when $by is zero or negative (#60324) * fix(Number): throw InvalidArgumentException when <= 0 in pairs() * Be forgiving with negative $by in Number::pairs() by using abs() Instead of throwing for negative step values, use abs($by) to match Laravel's convention of guessing user intent. Only throw for $by === 0 since no sensible default exists for a zero step. --- src/Illuminate/Support/Number.php | 6 ++++++ tests/Support/SupportNumberTest.php | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index aaa9bd541341..6673ad26234a 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -316,6 +316,12 @@ public static function clamp(int|float $number, int|float $min, int|float $max) */ public static function pairs(int|float $to, int|float $by, int|float $start = 0, int|float $offset = 1) { + if ($by == 0) { + throw new \InvalidArgumentException('The $by argument must not be zero.'); + } + + $by = abs($by); + $output = []; for ($lower = $start; $lower < $to; $lower += $by) { diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index c9b938a5027f..5c0172103fff 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -331,6 +331,18 @@ public function testPairs() $this->assertSame([[0.5, 2.5], [3.0, 5.0], [5.5, 7.5], [8.0, 10.0]], Number::pairs(10, 2.5, 0.5, 0.5)); } + public function testPairsThrowsWhenByIsZero() + { + $this->expectException(\InvalidArgumentException::class); + + Number::pairs(100, 0); + } + + public function testPairsWithNegativeByWorksLikePositive() + { + $this->assertSame(Number::pairs(100, 10), Number::pairs(100, -10)); + } + public function testTrim() { $this->assertSame(12, Number::trim(12)); From fbb3f5344f32938b240d7f3ded491f8779f6eff1 Mon Sep 17 00:00:00 2001 From: Michiel van Eerd Date: Mon, 1 Jun 2026 01:24:23 +0200 Subject: [PATCH 497/596] Added MariaDB vector index capability (#60334) * Added MariaDB vector index capability * Explanation for defaults * Fixed 2 style ci issues * Style ci issue fixed * Style ci issue fix * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Schema/Blueprint.php | 7 ++++++- .../Schema/Grammars/MariaDbGrammar.php | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 8584b29a0704..90a3290999bd 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Grammars\Grammar; +use Illuminate\Database\Schema\Grammars\MariaDbGrammar; use Illuminate\Database\Schema\Grammars\MySqlGrammar; use Illuminate\Database\Schema\Grammars\SQLiteGrammar; use Illuminate\Support\Collection; @@ -711,7 +712,11 @@ public function spatialIndex($columns, $name = null, $operatorClass = null) */ public function vectorIndex($column, $name = null) { - return $this->indexCommand('vectorIndex', $column, $name, 'hnsw', 'vector_cosine_ops'); + [$algorithm, $operatorClass] = $this->grammar instanceof MariaDbGrammar + ? [null, 'M=6 DISTANCE=cosine'] + : ['hnsw', 'vector_cosine_ops']; + + return $this->indexCommand('vectorIndex', $column, $name, $algorithm, $operatorClass); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php b/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php index ec15f50c78f9..8c34233e47da 100755 --- a/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php @@ -52,6 +52,26 @@ protected function typeGeometry(Fluent $column) ); } + /** + * Compile a vector index key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileVectorIndex(Blueprint $blueprint, Fluent $command) + { + return sprintf( + 'alter table %s add %s %s(%s) %s%s', + $this->wrapTable($blueprint), + 'vector index', + $this->wrap($command->index), + $this->columnize($command->columns), + $command->operatorClass ?? '', + $command->lock ? ', lock='.$command->lock : '' + ); + } + /** * Wrap the given JSON selector. * From b26aae617a0b509f1495c4edad9ed8266b74c7e6 Mon Sep 17 00:00:00 2001 From: Cathrine Vaage Date: Mon, 1 Jun 2026 15:51:29 +0200 Subject: [PATCH 498/596] Fix @theme directive collision in health check view (#60340) Escape the Tailwind @theme at-rule as @@theme in the health check Blade template so custom @theme Blade directives in consuming applications are not invoked when the view is compiled. No change to rendered HTML for default installations. --- src/Illuminate/Foundation/resources/health-up.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/resources/health-up.blade.php b/src/Illuminate/Foundation/resources/health-up.blade.php index 6b93b1652378..af048774768b 100644 --- a/src/Illuminate/Foundation/resources/health-up.blade.php +++ b/src/Illuminate/Foundation/resources/health-up.blade.php @@ -14,7 +14,7 @@ From 0158f160e991f8e4908d5dad7222c1996e10c609 Mon Sep 17 00:00:00 2001 From: Oliver Quynh Date: Mon, 1 Jun 2026 20:52:19 +0700 Subject: [PATCH 499/596] Remove unused variables from tests (#60338) --- tests/Integration/Routing/CompiledRouteCollectionTest.php | 4 ++-- tests/Validation/ValidationImageFileRuleTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Integration/Routing/CompiledRouteCollectionTest.php b/tests/Integration/Routing/CompiledRouteCollectionTest.php index 355df95d81ca..6129f798732f 100644 --- a/tests/Integration/Routing/CompiledRouteCollectionTest.php +++ b/tests/Integration/Routing/CompiledRouteCollectionTest.php @@ -146,7 +146,7 @@ public function testRouteCollectionCanGetIteratorWhenEmpty() public function testRouteCollectionCanGetIteratorWhenRoutesAreAdded() { - $this->routeCollection->add($routeIndex = $this->newRoute('GET', 'foo/index', [ + $this->routeCollection->add($this->newRoute('GET', 'foo/index', [ 'uses' => 'FooController@index', 'as' => 'foo_index', ])); @@ -155,7 +155,7 @@ public function testRouteCollectionCanGetIteratorWhenRoutesAreAdded() $this->assertCount(1, $routes); - $this->routeCollection->add($routeShow = $this->newRoute('GET', 'bar/show', [ + $this->routeCollection->add($this->newRoute('GET', 'bar/show', [ 'uses' => 'BarController@show', 'as' => 'bar_show', ])); diff --git a/tests/Validation/ValidationImageFileRuleTest.php b/tests/Validation/ValidationImageFileRuleTest.php index 3f2ac90505b4..cffda23eb7ec 100644 --- a/tests/Validation/ValidationImageFileRuleTest.php +++ b/tests/Validation/ValidationImageFileRuleTest.php @@ -34,13 +34,13 @@ public function testDimensionsWithCustomImageSizeMethod() { $this->fails( File::image()->dimensions(Rule::dimensions()->width(100)->height(100)), - new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data($tmpFile = tmpfile())['uri'], 'foo.png'), + new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data(tmpfile())['uri'], 'foo.png'), ['validation.dimensions'], ); $this->passes( File::image()->dimensions(Rule::dimensions()->width(200)->height(200)), - new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data($tmpFile = tmpfile())['uri'], 'foo.png'), + new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data(tmpfile())['uri'], 'foo.png'), ); } From 42b494e1ed2bc80511bca064c8a395b776ff1516 Mon Sep 17 00:00:00 2001 From: Fazle Rabbi <35403788+irabbi360@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:23:19 +0600 Subject: [PATCH 500/596] [13.x] fix: Queue and Connection attributes in Mailable::queue() and later() (#60328) * [13.x] fix: respect #[Queue] and #[Connection] attributes in Mailable::queue() and later() fix #60327 : respect #[Queue] and #[Connection] attributes in Mailable::queue() and later() * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Mail/Mailable.php | 14 +++---- tests/Mail/MailableQueuedTest.php | 68 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index d890c58dac02..e497979647fb 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -12,7 +12,9 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\Translation\HasLocalePreference; +use Illuminate\Queue\Attributes\Connection; use Illuminate\Queue\Attributes\Delay; +use Illuminate\Queue\Attributes\Queue as QueueAttribute; use Illuminate\Support\Collection; use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\HtmlString; @@ -232,15 +234,13 @@ public function queue(Queue $queue) return $this->later($delay, $queue); } - $connection = property_exists($this, 'connection') ? $this->connection : null; + $connection = $this->getAttributeValue($this, Connection::class, 'connection'); if (is_null($connection) && method_exists($queue, 'resolveConnectionFromQueueRoute')) { $connection = $queue->resolveConnectionFromQueueRoute($this); } - $queueName = property_exists($this, 'queue') - ? $this->queue - : null; + $queueName = $this->getAttributeValue($this, QueueAttribute::class, 'queue'); if (is_null($queueName) && method_exists($queue, 'resolveQueueFromQueueRoute')) { $queueName = $queue->resolveQueueFromQueueRoute($this); @@ -260,11 +260,9 @@ public function queue(Queue $queue) */ public function later($delay, Queue $queue) { - $connection = property_exists($this, 'connection') ? $this->connection : null; + $connection = $this->getAttributeValue($this, Connection::class, 'connection'); - $queueName = property_exists($this, 'queue') - ? $this->queue - : null; + $queueName = $this->getAttributeValue($this, QueueAttribute::class, 'queue'); if (is_null($connection) && method_exists($queue, 'resolveConnectionFromQueueRoute')) { $connection = $queue->resolveConnectionFromQueueRoute($this); diff --git a/tests/Mail/MailableQueuedTest.php b/tests/Mail/MailableQueuedTest.php index eb6f3a59abe9..0e34db033b80 100644 --- a/tests/Mail/MailableQueuedTest.php +++ b/tests/Mail/MailableQueuedTest.php @@ -11,7 +11,9 @@ use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailer; use Illuminate\Mail\SendQueuedMailable; +use Illuminate\Queue\Attributes\Connection; use Illuminate\Queue\Attributes\Delay; +use Illuminate\Queue\Attributes\Queue as QueueAttribute; use Illuminate\Support\Testing\Fakes\QueueFake; use Laravel\SerializableClosure\SerializableClosure; use Mockery as m; @@ -180,6 +182,45 @@ public function testQueuedMailableDelayPropertyOverridesAttribute(): void $this->assertEquals(60, $pushedJob->delay); } + public function testQueuedMailableRespectsQueueAndConnectionAttributes(): void + { + $queueFake = new MailableQueueFake(new Application); + $mailer = $this->getMockBuilder(Mailer::class) + ->setConstructorArgs($this->getMocks()) + ->onlyMethods(['createMessage', 'to']) + ->getMock(); + $mailer->setQueue($queueFake); + $mailable = new MailableQueueableStubWithQueueAndConnectionAttributes; + $queueFake->assertNothingPushed(); + $mailer->send($mailable); + $queueFake->assertPushedOn('mail-queue', SendQueuedMailable::class); + + $pushedJob = $queueFake->pushed(SendQueuedMailable::class)->first(); + $this->assertSame('redis', $queueFake->connectionName); + $this->assertSame('mail-queue', $pushedJob->queue); + $this->assertSame('redis', $pushedJob->connection); + } + + public function testDelayedQueuedMailableRespectsQueueAndConnectionAttributes(): void + { + $queueFake = new MailableQueueFake(new Application); + $mailer = $this->getMockBuilder(Mailer::class) + ->setConstructorArgs($this->getMocks()) + ->onlyMethods(['createMessage', 'to']) + ->getMock(); + $mailer->setQueue($queueFake); + $mailable = new MailableQueueableStubWithDelayQueueAndConnectionAttributes; + $queueFake->assertNothingPushed(); + $mailer->send($mailable); + $queueFake->assertPushedOn('delayed-mail-queue', SendQueuedMailable::class); + + $pushedJob = $queueFake->pushed(SendQueuedMailable::class)->first(); + $this->assertSame('sqs', $queueFake->connectionName); + $this->assertSame('delayed-mail-queue', $pushedJob->queue); + $this->assertSame('sqs', $pushedJob->connection); + $this->assertEquals(30, $pushedJob->delay); + } + public function testQueuedMailableForwardsDeduplicationIdMethodToQueueJob(): void { $queueFake = new QueueFake(new Application); @@ -274,3 +315,30 @@ public function deduplicationId($payload, $queue) return hash('sha256', $payload); } } + +#[Connection('redis')] +#[QueueAttribute('mail-queue')] +class MailableQueueableStubWithQueueAndConnectionAttributes extends MailableQueueableStub +{ + // +} + +#[Connection('sqs')] +#[Delay(30)] +#[QueueAttribute('delayed-mail-queue')] +class MailableQueueableStubWithDelayQueueAndConnectionAttributes extends MailableQueueableStub +{ + // +} + +class MailableQueueFake extends QueueFake +{ + public $connectionName; + + public function connection($value = null) + { + $this->connectionName = $value; + + return parent::connection($value); + } +} From 4ada21ab35a65c13d0f85f16cdc2560926b8b5fb Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 2 Jun 2026 02:42:18 +0100 Subject: [PATCH 501/596] [13.x] Fix ValidationImageFileRuleTest (#60348) * Update ValidationImageFileRuleTest.php * name what it is --- tests/Validation/ValidationImageFileRuleTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/Validation/ValidationImageFileRuleTest.php b/tests/Validation/ValidationImageFileRuleTest.php index cffda23eb7ec..3b9a4e2b4a2e 100644 --- a/tests/Validation/ValidationImageFileRuleTest.php +++ b/tests/Validation/ValidationImageFileRuleTest.php @@ -32,15 +32,18 @@ public function testDimensions() public function testDimensionsWithCustomImageSizeMethod() { + $stream = tmpfile(); // To prevent PHP from deleting the temp file early. + $path = stream_get_meta_data($stream)['uri']; + $this->fails( File::image()->dimensions(Rule::dimensions()->width(100)->height(100)), - new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data(tmpfile())['uri'], 'foo.png'), + new UploadedFileWithCustomImageSizeMethod($path, 'foo.png'), ['validation.dimensions'], ); $this->passes( File::image()->dimensions(Rule::dimensions()->width(200)->height(200)), - new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data(tmpfile())['uri'], 'foo.png'), + new UploadedFileWithCustomImageSizeMethod($path, 'foo.png'), ); } From 800cd64f73b029248b74625e73c87c1d407b06fa Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 2 Jun 2026 02:42:34 +0100 Subject: [PATCH 502/596] 13.x - Ensure schedule pause warns when its disabled (#60347) --- .../Console/Scheduling/SchedulePauseCommand.php | 6 ++++++ .../Scheduling/SchedulePauseCommandTest.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Illuminate/Console/Scheduling/SchedulePauseCommand.php b/src/Illuminate/Console/Scheduling/SchedulePauseCommand.php index cb6483f527ec..f4d314305be7 100644 --- a/src/Illuminate/Console/Scheduling/SchedulePauseCommand.php +++ b/src/Illuminate/Console/Scheduling/SchedulePauseCommand.php @@ -25,6 +25,12 @@ class SchedulePauseCommand extends Command */ public function handle(Cache $cache, Dispatcher $dispatcher) { + if (! Schedule::$pausable) { + $this->components->error('Schedule pausing is currently disabled.'); + + return 1; + } + $cache->forever('illuminate:schedule:paused', true); $dispatcher->dispatch(new SchedulePaused); diff --git a/tests/Integration/Console/Scheduling/SchedulePauseCommandTest.php b/tests/Integration/Console/Scheduling/SchedulePauseCommandTest.php index a86776b1a0d3..ce0b31476723 100644 --- a/tests/Integration/Console/Scheduling/SchedulePauseCommandTest.php +++ b/tests/Integration/Console/Scheduling/SchedulePauseCommandTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Console\Scheduling; use Illuminate\Console\Events\SchedulePaused; +use Illuminate\Console\Scheduling\Schedule; use Illuminate\Support\Facades\Event; use Orchestra\Testbench\TestCase; @@ -16,4 +17,17 @@ public function testDispatchesEvent() Event::assertDispatched(SchedulePaused::class); } + + public function testFailsWhenPausingIsDisabled() + { + Event::fake(); + + Schedule::$pausable = false; + + $this->artisan('schedule:pause')->assertFailed(); + + Event::assertNotDispatched(SchedulePaused::class); + + Schedule::$pausable = true; + } } From 12df688e942117d201bb37ddc7bf035f2e8bf607 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Tue, 2 Jun 2026 14:54:37 +0100 Subject: [PATCH 503/596] [12.x] Ensure path seperators aren't encoded in LocalFilesystemAdapter (#60350) * fix path seperator being encoded (#60194) betterr tests just the one for now. simples clear test * Fix path separator encoding in temporaryUrl on local disk (#60230) * imports * fix merge propblems * cs * cs2 --------- Co-authored-by: Kay W. --- .../Filesystem/LocalFilesystemAdapter.php | 4 +- .../Filesystem/ReceiveFileTest.php | 40 +++++++++++++++++- .../Integration/Filesystem/ServeFileTest.php | 42 ++++++++++++++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index 03377e0f4c80..dc9aa2d6c869 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -80,7 +80,7 @@ public function temporaryUrl($path, $expiration, array $options = []) return $url->to($url->temporarySignedRoute( 'storage.'.$this->disk, $expiration, - ['path' => rawurldecode($path)], + ['path' => strtr(rawurlencode($path), ['%2F' => '/'])], absolute: false )); } @@ -111,7 +111,7 @@ public function temporaryUploadUrl($path, $expiration, array $options = []) 'url' => $url->to($url->temporarySignedRoute( 'storage.'.$this->disk.'.upload', $expiration, - ['path' => rawurlencode($path), 'upload' => true], + ['path' => strtr(rawurlencode($path), ['%2F' => '/']), 'upload' => true], absolute: false )), 'headers' => [], diff --git a/tests/Integration/Filesystem/ReceiveFileTest.php b/tests/Integration/Filesystem/ReceiveFileTest.php index 227841eba558..c756c1adfdeb 100644 --- a/tests/Integration/Filesystem/ReceiveFileTest.php +++ b/tests/Integration/Filesystem/ReceiveFileTest.php @@ -2,9 +2,11 @@ namespace Illuminate\Tests\Integration\Filesystem; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; #[WithConfig('filesystems.disks.local.serve', true)] class ReceiveFileTest extends TestCase @@ -12,7 +14,11 @@ class ReceiveFileTest extends TestCase protected function setUp(): void { $this->beforeApplicationDestroyed(function () { - Storage::delete('receive-file-test.txt'); + Storage::delete([ + 'receive-file-test.txt', + 'receive-file-test.txt?pad=x', + 'nested/folder/receive-file-test.txt', + ]); }); parent::setUp(); @@ -72,4 +78,36 @@ public function testUploadUrlCannotBeUsedForDownload() $response->assertForbidden(); } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testItCanReceiveAFileWithUriDelimitersInThePath() + { + $result = Storage::temporaryUploadUrl('receive-file-test.txt?pad=x', Carbon::now()->addMinute()); + + $response = $this->call('PUT', $result['url'], [], [], [], [], 'Hello Question'); + + $response->assertNoContent(); + Storage::assertExists('receive-file-test.txt?pad=x', 'Hello Question'); + Storage::assertMissing('receive-file-test.txt'); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testTemporaryUploadUrlPreservesPathSeparatorsInNestedPaths() + { + $result = Storage::temporaryUploadUrl('nested/folder/receive-file-test.txt', Carbon::now()->addMinute()); + + $this->assertStringContainsString('nested/folder/receive-file-test.txt', $result['url']); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testUriDelimitersInThePathCannotHideAnExpiredUploadUrl() + { + $result = Storage::temporaryUploadUrl('receive-file-test.txt?pad=x', Carbon::now()->subMinute()); + + $response = $this->call('PUT', $result['url'], [], [], [], [], 'Hello Question'); + + $response->assertForbidden(); + Storage::assertMissing('receive-file-test.txt'); + Storage::assertMissing('receive-file-test.txt?pad=x'); + } } diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php index ccc99350e60f..91cca3d81e75 100644 --- a/tests/Integration/Filesystem/ServeFileTest.php +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -2,9 +2,11 @@ namespace Illuminate\Tests\Integration\Filesystem; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; #[WithConfig('filesystems.disks.local.serve', true)] class ServeFileTest extends TestCase @@ -13,10 +15,16 @@ protected function setUp(): void { $this->afterApplicationCreated(function () { Storage::put('serve-file-test.txt', 'Hello World'); + Storage::put('serve-file-test.txt?pad=x', 'Hello Question'); + Storage::put('nested/folder/serve-file-test.txt', 'Hello Nested'); }); $this->beforeApplicationDestroyed(function () { - Storage::delete('serve-file-test.txt'); + Storage::delete([ + 'serve-file-test.txt', + 'serve-file-test.txt?pad=x', + 'nested/folder/serve-file-test.txt', + ]); }); parent::setUp(); @@ -50,4 +58,36 @@ public function testItWill403OnWrongSignature() $response->assertForbidden(); } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testItCanServeAFileWithUriDelimitersInThePath() + { + $url = Storage::temporaryUrl('serve-file-test.txt?pad=x', Carbon::now()->addMinute()); + + $response = $this->get($url); + + $this->assertSame('Hello Question', $response->streamedContent()); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testTemporaryUrlPreservesPathSeparatorsInNestedPaths() + { + $url = Storage::temporaryUrl('nested/folder/serve-file-test.txt', Carbon::now()->addMinute()); + + $this->assertStringContainsString('nested/folder/serve-file-test.txt', $url); + + $response = $this->get($url); + + $this->assertSame('Hello Nested', $response->streamedContent()); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testUriDelimitersInThePathCannotHideAnExpiredUrl() + { + $url = Storage::temporaryUrl('serve-file-test.txt?pad=x', Carbon::now()->subMinute()); + + $response = $this->get($url); + + $response->assertForbidden(); + } } From 1daa6d3b4defe46976ccfa4fb0a7ab62717712a2 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:28:17 +0000 Subject: [PATCH 504/596] Update version to v13.13.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index f9331abe8d60..ca983e8296be 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 = '13.12.0'; + const VERSION = '13.13.0'; /** * The base path for the Laravel installation. From 4ebcf492a99f1715b54ff339080bc28d5641551f Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:30:14 +0000 Subject: [PATCH 505/596] Update CHANGELOG --- CHANGELOG.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb54002c8c9f..be342260e9b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,33 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.12.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.13.0...13.x) + +## [v13.13.0](https://github.com/laravel/framework/compare/v13.12.0...v13.13.0) - 2026-06-02 + +* [13.x] Add stan ignore for MySqlSchemaState by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60284 +* [13.x] Add attachFromStorage helpers to notification MailMessage by [@LucasCavalheri](https://github.com/LucasCavalheri) in https://github.com/laravel/framework/pull/60268 +* [13.x] Allow Http Client to be used as PSR Client by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/60295 +* [13.x] Normalize HTTP client header values by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/60292 +* [13.x] Report MultipleRecordsFoundException from sole() by [@PeteBishwhip](https://github.com/PeteBishwhip) in https://github.com/laravel/framework/pull/60294 +* [13.x] Hint unit in `UniqueFor` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60289 +* [13.x] Fix inverted ratio comparison operators in image dimension validation by [@Button99](https://github.com/Button99) in https://github.com/laravel/framework/pull/60290 +* [13.x] Allow scheduler to opt out of pause and interrupt cache checks by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60226 +* [13.x] Enhance Cache attribute to support memoization by [@jenky](https://github.com/jenky) in https://github.com/laravel/framework/pull/60309 +* [13.x] Indicate an event was skipped by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60311 +* [13.x] Fix `Illuminate\Http\Response` compatibility against Symfony 8.1 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60318 +* fix: fix isUniqueConstraintError to catch SQL Server error 2627 by [@ErfanMomeniii](https://github.com/ErfanMomeniii) in https://github.com/laravel/framework/pull/60320 +* [13.x] Fix FIFO queue name normalization in Cloud managed queues by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60315 +* fix: `whereDate` and `whereTime` crash when $column is an `Expression` by [@ahawlitschek](https://github.com/ahawlitschek) in https://github.com/laravel/framework/pull/60314 +* [13.x] Introduce Bus::bulk() by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60297 +* [13.x] Fix time-sensitive flaky test in NotificationDatabaseChannelTest by [@KentarouTakeda](https://github.com/KentarouTakeda) in https://github.com/laravel/framework/pull/60333 +* fix: Add content_id to inline attachment handling in CloudflareTransport by [@pablo-gonzalez-helpwan](https://github.com/pablo-gonzalez-helpwan) in https://github.com/laravel/framework/pull/60330 +* [13.x] Add payload to InspectedJob by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60326 +* Added MariaDB vector index capability by [@michielvaneerd](https://github.com/michielvaneerd) in https://github.com/laravel/framework/pull/60334 +* Fix [@theme](https://github.com/theme) directive collision in health check view by [@cathrinevaage](https://github.com/cathrinevaage) in https://github.com/laravel/framework/pull/60340 +* [13.x] Remove unused variables from tests by [@oliverquynh](https://github.com/oliverquynh) in https://github.com/laravel/framework/pull/60338 +* [13.x] fix: Queue and Connection attributes in Mailable::queue() and later() by [@irabbi360](https://github.com/irabbi360) in https://github.com/laravel/framework/pull/60328 +* [13.x] Fix ValidationImageFileRuleTest by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60348 +* [13.x] `schedule:pause` command should error when its disabled by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60347 ## [v13.12.0](https://github.com/laravel/framework/compare/v13.11.2...v13.12.0) - 2026-05-26 From 2456501c0157301334023e7a15e4a44c2a2dd87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=BEuris?= Date: Wed, 3 Jun 2026 04:35:58 +0300 Subject: [PATCH 506/596] [13.x] Register the lazy refresh hook on all connections (#60359) * Test LazilyRefreshDatabase * Fix formatting * Register the lazy refresh hook on all connections --- .../Testing/LazilyRefreshDatabase.php | 6 +- .../Testing/LazilyRefreshDatabaseTest.php | 108 ++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 tests/Foundation/Testing/LazilyRefreshDatabaseTest.php diff --git a/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php b/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php index 194fc3d8de62..6f785e7302d6 100644 --- a/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php @@ -37,8 +37,10 @@ public function refreshDatabase() } }; - $database->beforeStartingTransaction($callback); - $database->beforeExecuting($callback); + foreach ($this->connectionsToTransact() as $connection) { + $database->connection($connection)->beforeStartingTransaction($callback); + $database->connection($connection)->beforeExecuting($callback); + } $this->beforeApplicationDestroyed(function () { RefreshDatabaseState::$lazilyRefreshed = false; diff --git a/tests/Foundation/Testing/LazilyRefreshDatabaseTest.php b/tests/Foundation/Testing/LazilyRefreshDatabaseTest.php new file mode 100644 index 000000000000..0d686920f8ae --- /dev/null +++ b/tests/Foundation/Testing/LazilyRefreshDatabaseTest.php @@ -0,0 +1,108 @@ +setUpTheApplicationTestingHooks(); + $this->withoutMockingConsoleOutput(); + + $config = $this->app->make('config'); + $config->set( + 'database.connections', + [ + ...$config->get('database.connections'), + 'testing2' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ], + ], + ); + } + + protected function tearDown(): void + { + $this->tearDownTheApplicationTestingHooks(); + + RefreshDatabaseState::$migrated = false; + } + + protected function refreshApplication() + { + $this->app = Testbench::create( + basePath: package_path('vendor/orchestra/testbench-core/laravel'), + ); + } + + public function testDatabaseIsRefreshedOnInteraction() + { + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') + ->once() + ->with('migrate:fresh', [ + '--drop-views' => false, + '--drop-types' => false, + '--seed' => false, + ]); + + $this->refreshDatabase(); + $this->app->make('db')->select('select 1'); + } + + public function testDatabaseIsNotRefreshedWithoutInteraction() + { + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') + ->never(); + + $this->refreshDatabase(); + + // Some dummy interaction to make sure DB class can be tinkered with + $this->app->make('db')->getPdo(); + } + + public function testNonDefaultConnectionTriggersRefresh() + { + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') + ->once() + ->with('migrate:fresh', [ + '--drop-views' => false, + '--drop-types' => false, + '--seed' => false, + ]); + + $this->refreshDatabase(); + + $this->app->make('db')->connection('testing2')->select('select 1'); + } +} From a831b058e35c0445f5e58ff94885ee81494a18ff Mon Sep 17 00:00:00 2001 From: Christos Koumpis <56029580+Button99@users.noreply.github.com> Date: Wed, 3 Jun 2026 04:45:09 +0300 Subject: [PATCH 507/596] cache falsy http client json responses (#60357) --- src/Illuminate/Http/Client/Response.php | 10 ++++- tests/Http/HttpClientTest.php | 57 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index f2eebf4661a6..d5e6e48e5485 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -34,6 +34,13 @@ class Response implements ArrayAccess, Stringable */ protected $decoded; + /** + * Indicates if the JSON response has been decoded. + * + * @var bool + */ + protected bool $decodedJson = false; + /** * The flags that were used when decoding the JSON response. * @@ -101,12 +108,13 @@ public function json($key = null, $default = null, $flags = null) { $flags ??= self::$defaultJsonDecodingFlags; - if (! $this->decoded || (isset($this->decodingFlags) && $this->decodingFlags !== $flags)) { + if (! $this->decodedJson || $this->decodingFlags !== $flags) { $this->decoded = json_decode( $this->body(), true, flags: $flags ); $this->decodingFlags = $flags; + $this->decodedJson = true; } if (is_null($key)) { diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index b83a81f5941b..db84389c405c 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -4730,6 +4730,63 @@ public function testJsonDecodingIsCachedWhenFlagsMatch() $response->json(); $this->assertSame(3, $response->bodyCallCount); } + + public function testJsonDecodingIsCachedForFalsyPayloads() + { + $payloads = [ + '[]' => [], + 'false' => false, + '0' => 0, + 'null' => null, + '""' => '', + ]; + + foreach ($payloads as $body => $expected) { + $response = new BodyTrackingResponse(Factory::psr7Response($body)); + + // First call decodes and caches + $this->assertSame($expected, $response->json()); + $this->assertSame(1, $response->bodyCallCount, "Failed for body: $body"); + + // Subsequent calls use cache (body() not called again) + $this->assertSame($expected, $response->json()); + $this->assertSame(1, $response->bodyCallCount, "body() called again for falsy payload: $body"); + + // Third call to be sure + $this->assertSame($expected, $response->json()); + $this->assertSame(1, $response->bodyCallCount, "body() called again for falsy payload: $body"); + } + } + + public function testJsonDecodingWithFalsyPayloadRespectsFlags() + { + $response = new BodyTrackingResponse(Factory::psr7Response('0')); + + // First call decodes with default flags + $this->assertSame(0, $response->json()); + $this->assertSame(1, $response->bodyCallCount); + + // Different flags triggers re-decode + $response->json(flags: JSON_BIGINT_AS_STRING); + $this->assertSame(2, $response->bodyCallCount); + + // Same flags uses cache + $response->json(flags: JSON_BIGINT_AS_STRING); + $this->assertSame(2, $response->bodyCallCount); + } + + public function testJsonDecodingWithEmptyArrayRespectsKeyAccess() + { + $response = new BodyTrackingResponse(Factory::psr7Response('[]')); + + // Accessing a key on an empty array returns default + $this->assertNull($response->json('missing')); + $this->assertSame('fallback', $response->json('missing', 'fallback')); + + // body() should only be called once + $response->json(); + $this->assertSame(1, $response->bodyCallCount); + } } class CustomFactory extends Factory From 2bf554497eafe81359da87174b3b8d89e2793479 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 3 Jun 2026 07:58:33 +0100 Subject: [PATCH 508/596] Pin GitHub Actions to commit SHAs and add Dependabot config --- .github/dependabot.yml | 10 ++++ .github/workflows/databases-nightly.yml | 19 +++++-- .github/workflows/databases.yml | 75 ++++++++++++++++--------- .github/workflows/facades.yml | 8 +-- .github/workflows/issues.yml | 2 +- .github/workflows/pull-requests.yml | 2 +- .github/workflows/queues.yml | 27 ++++++--- .github/workflows/redis.yml | 21 ++++--- .github/workflows/releases.yml | 14 ++--- .github/workflows/static-analysis.yml | 11 +++- .github/workflows/tests.yml | 23 +++++--- .github/workflows/update-assets.yml | 6 +- 12 files changed, 142 insertions(+), 76 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..f6faee69383d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/databases-nightly.yml b/.github/workflows/databases-nightly.yml index c7074e605a03..95dd1984713b 100644 --- a/.github/workflows/databases-nightly.yml +++ b/.github/workflows/databases-nightly.yml @@ -4,6 +4,9 @@ on: schedule: - cron: '0 0 * * *' +permissions: + contents: read + jobs: mysql_9: runs-on: ubuntu-24.04 @@ -22,10 +25,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -36,7 +41,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -65,10 +70,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -79,7 +86,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 43d6ce29dd27..8e14192bbdf8 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -7,6 +7,9 @@ on: - '*.x' pull_request: +permissions: + contents: read + jobs: mysql_57: runs-on: ubuntu-24.04 @@ -26,10 +29,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -40,7 +45,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -70,10 +75,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -84,7 +91,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -113,10 +120,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -127,7 +136,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -157,10 +166,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, :php-psr @@ -171,7 +182,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -203,10 +214,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, :php-psr @@ -217,7 +230,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -249,10 +262,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, :php-psr @@ -263,7 +278,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -293,10 +308,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr @@ -307,7 +324,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -338,10 +355,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr @@ -352,7 +371,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -374,10 +393,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr @@ -388,7 +409,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index a412b2f74fd1..ccebd224468a 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: :php-psr @@ -32,7 +32,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -89,7 +89,7 @@ jobs: Illuminate\\Support\\Facades\\Vite - name: Commit facade docblocks - uses: stefanzweifel/git-auto-commit-action@v7 + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7 with: commit_message: Update facade docblocks file_pattern: src/ diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 9634a0edb3e0..c935a8d2cb2f 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -9,4 +9,4 @@ permissions: jobs: help-wanted: - uses: laravel/.github/.github/workflows/issues.yml@main + uses: laravel/.github/.github/workflows/issues.yml@dd86ce0a18475504e42fa809d7454c1cb1a88028 # main diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 2aa858fb68e0..8678df56c5d6 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -9,4 +9,4 @@ permissions: jobs: pull-requests: - uses: laravel/.github/.github/workflows/pull-requests.yml@main + uses: laravel/.github/.github/workflows/pull-requests.yml@dd86ce0a18475504e42fa809d7454c1cb1a88028 # main diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 7703c1d5c8d3..0e8876da7f16 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -7,6 +7,9 @@ on: - '*.x' pull_request: +permissions: + contents: read + jobs: sync: runs-on: ubuntu-24.04 @@ -15,10 +18,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -29,7 +34,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -47,10 +52,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -61,7 +68,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -92,7 +99,9 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Download & Extract beanstalkd run: curl -L https://github.com/beanstalkd/beanstalkd/archive/refs/tags/v1.13.tar.gz | tar xz @@ -102,7 +111,7 @@ jobs: working-directory: beanstalkd-1.13 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -113,7 +122,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/redis.yml b/.github/workflows/redis.yml index d45e7dead357..2f66ab2e24d3 100644 --- a/.github/workflows/redis.yml +++ b/.github/workflows/redis.yml @@ -7,6 +7,9 @@ on: - "*.x" pull_request: +permissions: + contents: read + jobs: redis: runs-on: ubuntu-24.04 @@ -27,10 +30,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -41,7 +46,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -73,10 +78,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr @@ -87,7 +94,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -104,7 +111,7 @@ jobs: redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 --cluster-replicas 0 --cluster-yes - name: Check Redis Cluster is ready - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_seconds: 5 max_attempts: 5 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 586e8684918a..884a72c73a96 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Remove optional "v" prefix id: version @@ -52,7 +52,7 @@ jobs: - name: Fail if branch and release tag do not match if: ${{ steps.guard.outputs.VERSION_MISMATCH == 'true' }} - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | core.setFailed('Workflow failed. Release version does not match with selected target branch. Did you select the correct branch?') @@ -61,12 +61,12 @@ jobs: run: sed -i "s/const VERSION = '.*';/const VERSION = '${{ steps.version.outputs.version }}';/g" src/Illuminate/Foundation/Application.php - name: Commit version change - uses: stefanzweifel/git-auto-commit-action@v7 + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7 with: commit_message: "Update version to v${{ steps.version.outputs.version }}" - name: SSH into splitter server - uses: appleboy/ssh-action@master + uses: appleboy/ssh-action@1530429296e979861824d74fb800013190b5dee0 # master with: host: 104.248.56.26 username: forge @@ -79,7 +79,7 @@ jobs: - name: Generate release notes id: generated-notes - uses: RedCrafter07/release-notes-action@main + uses: RedCrafter07/release-notes-action@31674bfa3a219e7c661fc0c5b7b3851c741b9965 # main with: tag-name: v${{ steps.version.outputs.version }} token: ${{ secrets.GITHUB_TOKEN }} @@ -116,7 +116,7 @@ jobs: RELEASE_NOTES: ${{ steps.generated-notes.outputs.release-notes }} - name: Create release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -131,7 +131,7 @@ jobs: name: Update changelog - uses: laravel/.github/.github/workflows/update-changelog.yml@main + uses: laravel/.github/.github/workflows/update-changelog.yml@dd86ce0a18475504e42fa809d7454c1cb1a88028 # main with: branch: ${{ github.ref_name }} version: "v${{ needs.release.outputs.version }}" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index d66649d25584..0f2ed6a4ffaa 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,6 +7,9 @@ on: - '*.x' pull_request: +permissions: + contents: read + jobs: types: runs-on: ubuntu-24.04 @@ -20,10 +23,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 8.3 tools: composer:v2 @@ -33,7 +38,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a763d5dd183..4d17711e0ea4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,6 +9,9 @@ on: schedule: - cron: '0 0 * * *' +permissions: + contents: read + jobs: linux_tests: runs-on: ubuntu-24.04 @@ -50,10 +53,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, igbinary, msgpack, memcached, gmp, :php-psr @@ -68,7 +73,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -85,7 +90,7 @@ jobs: AWS_SECRET_ACCESS_KEY: randomSecret - name: Store artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: linux-logs-${{ matrix.php }}-${{ matrix.phpunit }}-${{ matrix.stability }} path: | @@ -114,10 +119,12 @@ jobs: git config --global core.eol lf - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, gd, pdo_mysql, fileinfo, ftp, redis, memcached, gmp, intl, :php-psr @@ -128,7 +135,7 @@ jobs: run: composer config version "13.x-dev" - name: Install dependencies - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 with: timeout_minutes: 5 max_attempts: 5 @@ -141,7 +148,7 @@ jobs: AWS_SECRET_ACCESS_KEY: random_secret - name: Store artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: windows-logs-${{ matrix.php }}-${{ matrix.phpunit }}-${{ matrix.stability }} path: | diff --git a/.github/workflows/update-assets.yml b/.github/workflows/update-assets.yml index 4adb3a6925a7..dd2085d64393 100644 --- a/.github/workflows/update-assets.yml +++ b/.github/workflows/update-assets.yml @@ -22,9 +22,9 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 @@ -34,6 +34,6 @@ jobs: npm run build --prefix "./src/Illuminate/Foundation/resources/exceptions/renderer" - name: Commit Compiled Files - uses: stefanzweifel/git-auto-commit-action@v7 + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7 with: commit_message: Update Assets From e37cb0055b650b8a0dfe557e87a9f0363fd837ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:25:35 +0000 Subject: [PATCH 509/596] Bump the github-actions group with 4 updates Bumps the github-actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/github-script](https://github.com/actions/github-script), [softprops/action-gh-release](https://github.com/softprops/action-gh-release) and [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) Updates `actions/github-script` from 7.1.0 to 9.0.0 - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/f28e40c7f34bde8b3046d885e986cb6290c5673b...3a2844b7e9c422d3c10d287c895573f7108da1b3) Updates `softprops/action-gh-release` from 2.6.2 to 3.0.0 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/3bb12739c298aeb8a4eeaf626c5b8d85266b0e65...b4309332981a82ec1c5618f44dd2e27cc8bfbfda) Updates `actions/upload-artifact` from 6.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/github-script dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: softprops/action-gh-release dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/databases-nightly.yml | 4 ++-- .github/workflows/databases.yml | 18 +++++++++--------- .github/workflows/facades.yml | 2 +- .github/workflows/queues.yml | 6 +++--- .github/workflows/redis.yml | 4 ++-- .github/workflows/releases.yml | 6 +++--- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- .github/workflows/update-assets.yml | 2 +- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/databases-nightly.yml b/.github/workflows/databases-nightly.yml index 95dd1984713b..694380710177 100644 --- a/.github/workflows/databases-nightly.yml +++ b/.github/workflows/databases-nightly.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 8e14192bbdf8..f10fd6df4b03 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -75,7 +75,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -120,7 +120,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -166,7 +166,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -214,7 +214,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -262,7 +262,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -308,7 +308,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -355,7 +355,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -393,7 +393,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index ccebd224468a..f12b106a6f5d 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 0e8876da7f16..99a425c2ccf4 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -99,7 +99,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/redis.yml b/.github/workflows/redis.yml index 2f66ab2e24d3..f5a601c4f04f 100644 --- a/.github/workflows/redis.yml +++ b/.github/workflows/redis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -78,7 +78,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 884a72c73a96..b40f050e6395 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Remove optional "v" prefix id: version @@ -52,7 +52,7 @@ jobs: - name: Fail if branch and release tag do not match if: ${{ steps.guard.outputs.VERSION_MISMATCH == 'true' }} - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | core.setFailed('Workflow failed. Release version does not match with selected target branch. Did you select the correct branch?') @@ -116,7 +116,7 @@ jobs: RELEASE_NOTES: ${{ steps.generated-notes.outputs.release-notes }} - name: Create release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 0f2ed6a4ffaa..05a48b085a5e 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d17711e0ea4..a374be37b5cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -90,7 +90,7 @@ jobs: AWS_SECRET_ACCESS_KEY: randomSecret - name: Store artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: linux-logs-${{ matrix.php }}-${{ matrix.phpunit }}-${{ matrix.stability }} path: | @@ -119,7 +119,7 @@ jobs: git config --global core.eol lf - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -148,7 +148,7 @@ jobs: AWS_SECRET_ACCESS_KEY: random_secret - name: Store artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: windows-logs-${{ matrix.php }}-${{ matrix.phpunit }}-${{ matrix.stability }} path: | diff --git a/.github/workflows/update-assets.yml b/.github/workflows/update-assets.yml index dd2085d64393..f35a686abea6 100644 --- a/.github/workflows/update-assets.yml +++ b/.github/workflows/update-assets.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: From bab9a8dbf0a95baed19b38e46f5d95faa60129a4 Mon Sep 17 00:00:00 2001 From: Fazle Rabbi <35403788+irabbi360@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:01:48 +0600 Subject: [PATCH 510/596] [13.x] Fix `Request::createFromBase()` compatibility with Symfony 8.1 (#60354) * [13.x] Fix Request::createFromBase compatibility with Symfony 8.1 [13.x] Fix Request::createFromBase compatibility with Symfony 8.1 fixes #60353 * fix styleci error * Simplify code using `$request->duplicate()` and improves tests Signed-off-by: Mior Muhammad Zaki * Simplify to bare minimum changes Signed-off-by: Mior Muhammad Zaki * simplify tests Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: Mior Muhammad Zaki --- src/Illuminate/Http/Request.php | 3 ++- tests/Http/HttpRequestTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index b6d8b3d69eaa..abbe7ea15ba0 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -546,7 +546,8 @@ public static function createFromBase(SymfonyRequest $request) $newRequest->content = $request->content; if ($newRequest->isJson()) { - $newRequest->request = $newRequest->json(); + $newRequest->request->replace($newRequest->json()->all()); + $newRequest->setJson($newRequest->request); } return $newRequest; diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index fc7e144e9859..d26dfc4f4eab 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -1888,6 +1888,16 @@ public function testGeneratingJsonRequestFromParentRequestUsesCorrectType() $this->assertSame('world', $request->getPayload()->get('hello')); } + public function testCreatingJsonRequestFromBaseDoesNotTriggerRequestPropertyDeprecation() + { + $request = Request::createFromBase( + SymfonyRequest::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: '{"hello":"world"}') + ); + + $this->assertTrue($request->isJson()); + $this->assertSame('world', $request->input('hello')); + } + public function testJsonRequestsCanMergeDataIntoJsonRequest() { if (! method_exists(SymfonyRequest::class, 'getPayload')) { From 247460c797693f3316335f563c2d402190a3572f Mon Sep 17 00:00:00 2001 From: JurianArie <28654085+JurianArie@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:05:08 +0200 Subject: [PATCH 511/596] Set default cooldown for GitHub Actions updates (#60368) --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f6faee69383d..ae67c4dd858d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,8 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 5 groups: github-actions: patterns: From 5d38c57ec1a9a1adbf1fcd26185c65a07e356461 Mon Sep 17 00:00:00 2001 From: Milad Date: Wed, 3 Jun 2026 17:40:39 +0330 Subject: [PATCH 512/596] [13.x] Fix `Message::embed` data attachment handling (#60361) * Fix Mail\Message embed data typo and add test coverage * Fix Mail\Message embed data coverage --- src/Illuminate/Mail/Message.php | 2 +- tests/Mail/MailMessageTest.php | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Mail/Message.php b/src/Illuminate/Mail/Message.php index faef13ffbcad..9cc67410d75e 100755 --- a/src/Illuminate/Mail/Message.php +++ b/src/Illuminate/Mail/Message.php @@ -396,7 +396,7 @@ function ($path) use ($file) { }, function ($data) use ($file) { $this->message->addPart( - $part = $part = (new DataPart($data(), $file->as, $file->mime))->asInline() + $part = (new DataPart($data(), $file->as, $file->mime))->asInline() ); return "cid:{$part->getContentId()}"; diff --git a/tests/Mail/MailMessageTest.php b/tests/Mail/MailMessageTest.php index eff028ddadfe..c61bacf7479e 100755 --- a/tests/Mail/MailMessageTest.php +++ b/tests/Mail/MailMessageTest.php @@ -217,6 +217,27 @@ public function toMailAttachment() unlink($path); } + public function testItEmbedsFilesViaAttachableContractFromData(): void + { + $cid = $this->message->embed(new class() implements Attachable + { + public function toMailAttachment() + { + return Attachment::fromData(fn () => 'bar', 'foo.jpg')->withMime('image/png'); + } + }); + + $this->assertStringStartsWith('cid:', $cid); + $contentId = Str::after($cid, 'cid:'); + $attachment = $this->message->getSymfonyMessage()->getAttachments()[0]; + $headers = $attachment->getPreparedHeaders()->toArray(); + $this->assertSame($contentId, $attachment->getContentId()); + $this->assertSame('bar', $attachment->getBody()); + $this->assertStringContainsString('Content-Type: image/png', $headers[0]); + $this->assertSame('Content-Transfer-Encoding: base64', $headers[1]); + $this->assertStringContainsString('Content-Disposition: inline', $headers[2]); + } + public function testItGeneratesARandomNameWhenAttachableHasNone(): void { file_put_contents($path = __DIR__.'/foo.jpg', 'bar'); From 5ee37e85f3ce4e1e104b359ee176f5c93d6b05e2 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 3 Jun 2026 23:11:01 +0900 Subject: [PATCH 513/596] Namespace the cloud logging formatter (#60362) --- src/Illuminate/Foundation/Cloud.php | 3 ++- .../JsonFormatter.php} | 6 +++--- .../JsonFormatterTest.php} | 14 +++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) rename src/Illuminate/Foundation/{LaravelCloudJsonFormatter.php => Cloud/JsonFormatter.php} (80%) rename tests/Foundation/{LaravelCloudJsonFormatterTest.php => Cloud/JsonFormatterTest.php} (89%) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 7f9b98b5d5d8..3c63075a4b5a 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -8,6 +8,7 @@ use Illuminate\Foundation\Bootstrap\LoadConfiguration; use Illuminate\Foundation\Cloud\Events; use Illuminate\Foundation\Cloud\FailedJobProvider; +use Illuminate\Foundation\Cloud\JsonFormatter; use Illuminate\Foundation\Cloud\QueueConnector; use Illuminate\Queue\Connectors\SqsConnector; use Monolog\Handler\SocketHandler; @@ -188,7 +189,7 @@ public static function configureCloudLogging(Application $app): void 'driver' => 'monolog', 'level' => $_ENV['LOG_LEVEL'] ?? $_SERVER['LOG_LEVEL'] ?? 'debug', 'handler' => SocketHandler::class, - 'formatter' => LaravelCloudJsonFormatter::class, + 'formatter' => JsonFormatter::class, 'formatter_with' => [ 'includeStacktraces' => true, ], diff --git a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php b/src/Illuminate/Foundation/Cloud/JsonFormatter.php similarity index 80% rename from src/Illuminate/Foundation/LaravelCloudJsonFormatter.php rename to src/Illuminate/Foundation/Cloud/JsonFormatter.php index 0501655e1566..c56898a90d01 100644 --- a/src/Illuminate/Foundation/LaravelCloudJsonFormatter.php +++ b/src/Illuminate/Foundation/Cloud/JsonFormatter.php @@ -1,12 +1,12 @@ headers->set('Cloud-Request-ID', '550e8400-e29b-41d4-a716-446655440000'); $app->instance('request', $request); - $formatter = new LaravelCloudJsonFormatter; + $formatter = new JsonFormatter(); $formatted = $formatter->format($this->createRecord()); $decoded = json_decode($formatted, true); @@ -51,7 +51,7 @@ public function test_adds_cloud_request_id_as_top_level_key() public function test_does_not_add_field_when_no_request_bound() { - $formatter = new LaravelCloudJsonFormatter; + $formatter = new JsonFormatter; $formatted = $formatter->format($this->createRecord()); $decoded = json_decode($formatted, true); @@ -64,7 +64,7 @@ public function test_does_not_add_field_when_no_header_present() $request = Request::create('/'); $app->instance('request', $request); - $formatter = new LaravelCloudJsonFormatter; + $formatter = new JsonFormatter; $formatted = $formatter->format($this->createRecord()); $decoded = json_decode($formatted, true); @@ -87,7 +87,7 @@ public function test_preserves_existing_log_fields() context: ['context_field' => 'context_value'], ); - $formatter = new LaravelCloudJsonFormatter; + $formatter = new JsonFormatter; $formatted = $formatter->format($record); $decoded = json_decode($formatted, true); From 92fb2e02713f198b45fc2a8e25a136a9d7cf451b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 3 Jun 2026 15:16:57 +0100 Subject: [PATCH 514/596] Grant contents: read to pull requests workflow --- .github/workflows/pull-requests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 8678df56c5d6..72be176853e6 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -5,6 +5,7 @@ on: types: [opened] permissions: + contents: read pull-requests: write jobs: From 437d38ae6d64b99f250fdfed0f4e7005e7bfe329 Mon Sep 17 00:00:00 2001 From: Mattias Geniar Date: Wed, 3 Jun 2026 16:31:28 +0200 Subject: [PATCH 515/596] [13.x] Respect child queue properties over inherited attributes (#60369) --- .../Support/Traits/ReadsClassAttributes.php | 33 ++++++- tests/Queue/QueueDatabaseQueueUnitTest.php | 86 +++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Traits/ReadsClassAttributes.php b/src/Illuminate/Support/Traits/ReadsClassAttributes.php index 56f7800f313a..f559e263d8d7 100644 --- a/src/Illuminate/Support/Traits/ReadsClassAttributes.php +++ b/src/Illuminate/Support/Traits/ReadsClassAttributes.php @@ -26,7 +26,11 @@ protected function getAttributeValue($target, string $attributeClass, ?string $p return $target->{$property}; } - if ($instance = $this->getAttributeInstance($target, $attributeClass)) { + if ($instance = $this->getAttributeInstance($target, $attributeClass, $attributeDeclaringClass)) { + if ($this->propertyOverridesAttribute($target, $reflection, $property, $attributeDeclaringClass)) { + return $target->{$property}; + } + return $this->extractAttributeValue($instance); } @@ -51,9 +55,10 @@ protected function extractAttributeValue($instance) * * @param object $target * @param class-string $attributeClass + * @param \ReflectionClass|null $declaringClass * @return object|null */ - protected function getAttributeInstance($target, string $attributeClass) + protected function getAttributeInstance($target, string $attributeClass, ?ReflectionClass &$declaringClass = null) { $reflection = new ReflectionClass($target); @@ -62,6 +67,8 @@ protected function getAttributeInstance($target, string $attributeClass) $attributes = $reflection->getAttributes($attributeClass); if (count($attributes) > 0) { + $declaringClass = $reflection; + return $attributes[0]->newInstance(); } } while ($reflection = $reflection->getParentClass()); @@ -71,4 +78,26 @@ protected function getAttributeInstance($target, string $attributeClass) return null; } + + /** + * Determine if a property declared on a child class overrides an inherited attribute. + * + * @param object $target + * @param \ReflectionClass $reflection + * @param string|null $property + * @param \ReflectionClass $attributeDeclaringClass + * @return bool + */ + protected function propertyOverridesAttribute($target, ReflectionClass $reflection, ?string $property, ReflectionClass $attributeDeclaringClass) + { + if (is_null($property) || ! $reflection->hasProperty($property)) { + return false; + } + + $property = $reflection->getProperty($property); + + return $property->isPublic() + && $property->isInitialized($target) + && $property->getDeclaringClass()->isSubclassOf($attributeDeclaringClass->getName()); + } } diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 9e235c47d63d..b9effe2f49ca 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -4,7 +4,13 @@ use Illuminate\Bus\Batchable; use Illuminate\Container\Container; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Connection; +use Illuminate\Queue\Attributes\Backoff; +use Illuminate\Queue\Attributes\FailOnTimeout; +use Illuminate\Queue\Attributes\MaxExceptions; +use Illuminate\Queue\Attributes\Timeout; +use Illuminate\Queue\Attributes\Tries; use Illuminate\Queue\DatabaseQueue; use Illuminate\Queue\Jobs\InspectedJob; use Illuminate\Queue\Queue; @@ -119,6 +125,46 @@ public function testPushIncludesBatchIdInPayloadForBatchableJob() Str::createUuidsNormally(); } + public function testPushUsesPropertiesDeclaredOnChildClassOverInheritedAttributes() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer($container = m::spy(Container::class)); + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) { + $payload = json_decode($array['payload'], true); + + $this->assertSame(1700, $payload['timeout']); + $this->assertSame(7, $payload['maxTries']); + $this->assertSame('13', $payload['backoff']); + $this->assertSame(11, $payload['maxExceptions']); + $this->assertFalse($payload['failOnTimeout']); + }); + + $queue->push(new ChildJobWithPropertiesOverridingParentAttributes, ['data']); + + $container->shouldHaveReceived('bound')->with('events')->twice(); + } + + public function testPushStillUsesAttributesDeclaredOnSameClassOverDefaultProperties() + { + $queue = new DatabaseQueue($database = m::mock(Connection::class), 'table', 'default'); + $queue->setContainer($container = m::spy(Container::class)); + $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); + $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) { + $payload = json_decode($array['payload'], true); + + $this->assertSame(40, $payload['timeout']); + $this->assertSame(2, $payload['maxTries']); + $this->assertSame('9', $payload['backoff']); + $this->assertSame(3, $payload['maxExceptions']); + $this->assertTrue($payload['failOnTimeout']); + }); + + $queue->push(new JobWithAttributesAndDefaultProperties, ['data']); + + $container->shouldHaveReceived('bound')->with('events')->twice(); + } + public function testFailureToCreatePayloadFromObject() { $this->expectException('InvalidArgumentException'); @@ -390,3 +436,43 @@ class MyBatchableJob { use Batchable; } + +#[Backoff(9)] +#[FailOnTimeout] +#[MaxExceptions(3)] +#[Timeout(40)] +#[Tries(2)] +abstract class ParentJobWithAttributes implements ShouldQueue +{ +} + +class ChildJobWithPropertiesOverridingParentAttributes extends ParentJobWithAttributes +{ + public $backoff = 13; + + public $failOnTimeout = false; + + public $maxExceptions = 11; + + public $timeout = 1700; + + public $tries = 7; +} + +#[Backoff(9)] +#[FailOnTimeout] +#[MaxExceptions(3)] +#[Timeout(40)] +#[Tries(2)] +class JobWithAttributesAndDefaultProperties implements ShouldQueue +{ + public $backoff = 13; + + public $failOnTimeout = false; + + public $maxExceptions = 11; + + public $timeout = 1700; + + public $tries = 7; +} From 0fadb69e65429206e0909db4f6e722722ab450de Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 3 Jun 2026 15:32:18 +0100 Subject: [PATCH 516/596] Grant contents: read and pull-requests: write to issues workflow --- .github/workflows/issues.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index c935a8d2cb2f..1275c1229da2 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -5,7 +5,9 @@ on: types: [labeled] permissions: + contents: read issues: write + pull-requests: write jobs: help-wanted: From 8d85265a1d076fc7f0bcae0764aa11be468e32e6 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 3 Jun 2026 15:47:28 +0100 Subject: [PATCH 517/596] Use least-privilege permissions for pull requests and issues workflows --- .github/workflows/issues.yml | 4 +--- .github/workflows/pull-requests.yml | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 1275c1229da2..7bf45ecda710 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -5,10 +5,8 @@ on: types: [labeled] permissions: - contents: read issues: write - pull-requests: write jobs: help-wanted: - uses: laravel/.github/.github/workflows/issues.yml@dd86ce0a18475504e42fa809d7454c1cb1a88028 # main + uses: laravel/.github/.github/workflows/issues.yml@ae831fb2746b4ad21f6751fde03da7d146822d61 # main diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 72be176853e6..fe6613705443 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -5,9 +5,8 @@ on: types: [opened] permissions: - contents: read pull-requests: write jobs: pull-requests: - uses: laravel/.github/.github/workflows/pull-requests.yml@dd86ce0a18475504e42fa809d7454c1cb1a88028 # main + uses: laravel/.github/.github/workflows/pull-requests.yml@ae831fb2746b4ad21f6751fde03da7d146822d61 # main From 5d1a40a40b69560b2a2df44168947f17cc4df7b4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 3 Jun 2026 16:08:30 +0100 Subject: [PATCH 518/596] Pin pull requests and issues workflows to latest laravel/.github --- .github/workflows/issues.yml | 2 +- .github/workflows/pull-requests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 7bf45ecda710..2c662e006ce2 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -9,4 +9,4 @@ permissions: jobs: help-wanted: - uses: laravel/.github/.github/workflows/issues.yml@ae831fb2746b4ad21f6751fde03da7d146822d61 # main + uses: laravel/.github/.github/workflows/issues.yml@b019b7649633cd7149a2d2a4a6e3bba5d7e5ba01 # main diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index fe6613705443..60b8b06aaf24 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -9,4 +9,4 @@ permissions: jobs: pull-requests: - uses: laravel/.github/.github/workflows/pull-requests.yml@ae831fb2746b4ad21f6751fde03da7d146822d61 # main + uses: laravel/.github/.github/workflows/pull-requests.yml@b019b7649633cd7149a2d2a4a6e3bba5d7e5ba01 # main From dd0b0e604e798077d7bd2562d2dd4a226777f1b3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 4 Jun 2026 09:08:22 +0100 Subject: [PATCH 519/596] Enable Dependabot auto-merge --- .github/workflows/dependabot-auto-merge.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000000..0871d485e24f --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,12 @@ +name: dependabot-auto-merge + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + if: ${{ github.actor == 'dependabot[bot]' }} + uses: laravel/.github/.github/workflows/dependabot-auto-merge.yml@c265c45c41ccb723befb7a2b807dffff4595bd39 From 7433202012c624807ca84531f7f3c0de02f45a3c Mon Sep 17 00:00:00 2001 From: Oliver Quynh Date: Thu, 4 Jun 2026 20:27:31 +0700 Subject: [PATCH 520/596] Foundation\Cloud\Events: remove an unused import and fix docblock (#60378) --- src/Illuminate/Foundation/Cloud/Events.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Illuminate/Foundation/Cloud/Events.php b/src/Illuminate/Foundation/Cloud/Events.php index 2d90ccad5b11..b49549cfa781 100644 --- a/src/Illuminate/Foundation/Cloud/Events.php +++ b/src/Illuminate/Foundation/Cloud/Events.php @@ -2,7 +2,6 @@ namespace Illuminate\Foundation\Cloud; -use Illuminate\Foundation\Cloud; use RuntimeException; use Throwable; @@ -55,8 +54,6 @@ public function emitMany(array $payloads): void /** * Write the payload to the socket. - * - * @param list> $payloads */ protected function write(string $payload): void { From da9b7b7341c7b1ce0c918ed3359b2885fab2b433 Mon Sep 17 00:00:00 2001 From: Oliver Quynh Date: Thu, 4 Jun 2026 20:28:18 +0700 Subject: [PATCH 521/596] Fix StartSession docblock (#60375) --- src/Illuminate/Session/Middleware/StartSession.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Session/Middleware/StartSession.php b/src/Illuminate/Session/Middleware/StartSession.php index d0cd6e36165e..fc5cc0b4065c 100644 --- a/src/Illuminate/Session/Middleware/StartSession.php +++ b/src/Illuminate/Session/Middleware/StartSession.php @@ -300,7 +300,7 @@ protected function sessionIsPersistent(?array $config = null) * Resolve the given cache driver. * * @param string $driver - * @return \Illuminate\Cache\Store + * @return \Illuminate\Contracts\Cache\Repository */ protected function cache($driver) { From 61d555733d6b42ffc7b722c8cac6c72c2219af59 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Jun 2026 22:21:04 +0800 Subject: [PATCH 522/596] [12.x] Ensure `config` is bound before trying to log deprecation notice (#60376) * [12.x] Ensure `config` is bound before trying to log deprecation notice * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Database/Schema/MySqlSchemaState.php | 1 + src/Illuminate/Foundation/Bootstrap/HandleExceptions.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index b72440935981..87454086fd2e 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -130,6 +130,7 @@ protected function connectionString(array $versionInfo) $value .= ' --ssl-key="${:LARAVEL_LOAD_SSL_KEY}"'; } + /** @phpstan-ignore classConstant.notFound */ if (($config['options'][Mysql::ATTR_SSL_VERIFY_SERVER_CERT] ?? null) === false) { if (version_compare($versionInfo['version'], '5.7.11', '>=') && ! $versionInfo['isMariaDb']) { $value .= ' --ssl-mode=DISABLED'; diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index a5588cf0694e..3f79291061c0 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -92,6 +92,10 @@ public function handleDeprecationError($message, $file, $line, $level = E_DEPREC return; } + if (! static::$app->bound('config')) { + return; + } + try { $logger = static::$app->make(LogManager::class); } catch (Exception) { From e8472ca9774452fe50841d9bdced060679f4d58d Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:22:52 +0000 Subject: [PATCH 523/596] Update version to v12.61.1 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index b99b746f3624..ce670fc90d1c 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.61.0'; + const VERSION = '12.61.1'; /** * The base path for the Laravel installation. From d01a3901a33f0c70be24fbbc607be7a8290531a5 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:24:47 +0000 Subject: [PATCH 524/596] Update CHANGELOG --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56cf9cd6ea64..ac396563cfa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.61.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.61.1...12.x) + +## [v12.61.1](https://github.com/laravel/framework/compare/v12.61.0...v12.61.1) - 2026-06-04 + +* [12.x] Preserve empty HTTP attach contents by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/60291 +* Fix [@params](https://github.com/params) typo in Fluent and MessageBag toPrettyJson() docblocks by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60313 +* [12.x] Fix regex typo in Env::addVariableToEnvContents that prevented quotin… by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60312 +* [12.x] Fix Number::trim() returning null for INF and NAN values by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60322 +* [12.x] Fix FIFO queue name normalization in Cloud managed queues by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60316 +* [12.x] Fix Number::pairs() infinite loop when $by is zero or negative by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60324 +* [12.x] Ensure path seperators aren't encoded in LocalFilesystemAdapter by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60350 +* [12.x] Ensure `config` is bound before trying to log deprecation notice by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60376 ## [v12.61.0](https://github.com/laravel/framework/compare/v12.60.2...v12.61.0) - 2026-05-26 From 67e927ef639e0464fd21e89667d15fd4767e0a3a Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Thu, 4 Jun 2026 20:11:23 +0530 Subject: [PATCH 525/596] [13.x] Add JSON Schema array deserializer (#60384) * Add JSON Schema array to Type deserializer * Formatting * Tighten JSON Schema deserializer and rename entry point to fromArray * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/JsonSchema/Deserializer.php | 524 +++++++++++++++++++ src/Illuminate/JsonSchema/JsonSchema.php | 12 + tests/JsonSchema/DeserializerTest.php | 577 +++++++++++++++++++++ 3 files changed, 1113 insertions(+) create mode 100644 src/Illuminate/JsonSchema/Deserializer.php create mode 100644 tests/JsonSchema/DeserializerTest.php diff --git a/src/Illuminate/JsonSchema/Deserializer.php b/src/Illuminate/JsonSchema/Deserializer.php new file mode 100644 index 000000000000..dab25e9ce89e --- /dev/null +++ b/src/Illuminate/JsonSchema/Deserializer.php @@ -0,0 +1,524 @@ + + */ + protected array $root; + + /** + * Create a new deserializer instance. + * + * @param array $root + */ + protected function __construct(array $root) + { + $this->root = $root; + } + + /** + * Deserialize the Laravel-supported JSON Schema subset into a type. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + public static function deserialize(array $schema): Types\Type + { + return (new static($schema))->build($schema); + } + + /** + * Build a type from the given schema fragment. + * + * @param array $schema + * @param array $refs + * + * @throws \InvalidArgumentException + */ + protected function build(array $schema, array $refs = []): Types\Type + { + [$schema, $refs] = $this->resolveRef($schema, $refs); + + [$schema, $nullableFromUnion, $refs] = $this->normalizeUnions($schema, $refs); + + [$name, $nullableFromType] = $this->resolveType($schema); + + $type = match ($name) { + 'object' => $this->buildObject($schema, $refs), + 'array' => $this->buildArray($schema, $refs), + 'string' => $this->buildString($schema), + 'integer' => $this->buildInteger($schema), + 'number' => $this->buildNumber($schema), + 'boolean' => new Types\BooleanType, + default => throw new InvalidArgumentException("Unsupported JSON Schema type [{$name}]."), + }; + + $this->applyCommon($type, $schema); + + if ($nullableFromUnion || $nullableFromType) { + $type->nullable(); + } + + return $type; + } + + /** + * Build an object type from the given schema fragment. + * + * @param array $schema + * @param array $refs + * + * @throws \InvalidArgumentException + */ + protected function buildObject(array $schema, array $refs = []): Types\ObjectType + { + $properties = []; + + if (isset($schema['properties']) && is_array($schema['properties'])) { + $required = is_array($schema['required'] ?? null) + ? array_map('strval', $schema['required']) + : []; + + foreach ($schema['properties'] as $key => $definition) { + if (! is_array($definition)) { + throw new InvalidArgumentException( + "Unable to represent the schema for property [{$key}]; boolean schemas are not supported." + ); + } + + $property = $this->build($definition, $refs); + + if (in_array((string) $key, $required, true)) { + $property->required(); + } + + $properties[$key] = $property; + } + } + + $type = new Types\ObjectType($properties); + + if (($schema['additionalProperties'] ?? null) === false) { + $type->withoutAdditionalProperties(); + } + + return $type; + } + + /** + * Build an array type from the given schema fragment. + * + * @param array $schema + * @param array $refs + * + * @throws \InvalidArgumentException + */ + protected function buildArray(array $schema, array $refs = []): Types\ArrayType + { + $type = new Types\ArrayType; + + if (isset($schema['items']) && $schema['items'] !== []) { + if (! is_array($schema['items']) || array_is_list($schema['items'])) { + throw new InvalidArgumentException('Tuple and boolean JSON Schema "items" are not supported.'); + } + + $type->items($this->build($schema['items'], $refs)); + } + + if (isset($schema['minItems'])) { + $type->min((int) $schema['minItems']); + } + + if (isset($schema['maxItems'])) { + $type->max((int) $schema['maxItems']); + } + + if (isset($schema['uniqueItems'])) { + $type->unique((bool) $schema['uniqueItems']); + } + + return $type; + } + + /** + * Build a string type from the given schema fragment. + * + * @param array $schema + */ + protected function buildString(array $schema): Types\StringType + { + $type = new Types\StringType; + + if (isset($schema['minLength'])) { + $type->min((int) $schema['minLength']); + } + + if (isset($schema['maxLength'])) { + $type->max((int) $schema['maxLength']); + } + + if (isset($schema['pattern'])) { + $type->pattern((string) $schema['pattern']); + } + + if (isset($schema['format'])) { + $type->format((string) $schema['format']); + } + + return $type; + } + + /** + * Build an integer type from the given schema fragment. + * + * @param array $schema + */ + protected function buildInteger(array $schema): Types\IntegerType + { + return $this->applyNumericBounds(new Types\IntegerType, $schema, $this->toInteger(...)); + } + + /** + * Build a number type from the given schema fragment. + * + * @param array $schema + */ + protected function buildNumber(array $schema): Types\NumberType + { + return $this->applyNumericBounds(new Types\NumberType, $schema); + } + + /** + * Apply the numeric bound keywords to the given integer or number type. + * + * @template TType of Types\IntegerType|Types\NumberType + * + * @param TType $type + * @param array $schema + * @param (callable(int|float): (int|float))|null $cast + * @return TType + * + * @throws \InvalidArgumentException + */ + protected function applyNumericBounds(Types\IntegerType|Types\NumberType $type, array $schema, ?callable $cast = null) + { + $cast ??= static fn (int|float $value) => $value; + + foreach (['minimum' => 'min', 'maximum' => 'max', 'multipleOf' => 'multipleOf'] as $keyword => $method) { + if (! isset($schema[$keyword])) { + continue; + } + + if (($value = $this->toNumber($schema[$keyword])) === null) { + throw new InvalidArgumentException("The JSON Schema [{$keyword}] constraint must be a number."); + } + + $type->{$method}($cast($value)); + } + + return $type; + } + + /** + * Apply the keywords shared by every type to the given instance. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + protected function applyCommon(Types\Type $type, array $schema): void + { + if (isset($schema['title'])) { + $type->title((string) $schema['title']); + } + + if (isset($schema['description'])) { + $type->description((string) $schema['description']); + } + + if (isset($schema['enum']) && is_array($schema['enum'])) { + $type->enum($schema['enum']); + } + + if (array_key_exists('default', $schema)) { + if ($schema['default'] === null) { + throw new InvalidArgumentException('A null JSON Schema [default] is not supported.'); + } + + // The "default" setter is typed per concrete type, so assign it directly... + (fn () => $this->default = $schema['default'])->call($type); + } + } + + /** + * Resolve the base type name and whether the schema is nullable. + * + * @param array $schema + * @return array{0: string, 1: bool} + * + * @throws \InvalidArgumentException + */ + protected function resolveType(array $schema): array + { + $type = $schema['type'] ?? null; + $nullable = false; + + if (is_array($type)) { + $nullable = in_array('null', $type, true); + + $names = array_values(array_unique(array_filter( + $type, + static fn ($value) => $value !== 'null', + ))); + + if (count($names) > 1) { + throw new InvalidArgumentException( + 'Unable to represent a multi-type JSON Schema union ['.implode(', ', array_map('strval', $names)).'].' + ); + } + + $type = $names[0] ?? null; + } + + $type ??= $this->inferType($schema); + + if (! is_string($type)) { + throw new InvalidArgumentException('Unable to determine the JSON Schema type for the given schema.'); + } + + return [$type, $nullable]; + } + + /** + * Infer the type name when "type" is absent but the shape is unambiguous. + * + * @param array $schema + */ + protected function inferType(array $schema): ?string + { + return match (true) { + isset($schema['properties']), isset($schema['additionalProperties']), isset($schema['required']) => 'object', + isset($schema['items']), isset($schema['minItems']), isset($schema['maxItems']), isset($schema['uniqueItems']) => 'array', + isset($schema['enum']) && is_array($schema['enum']) => $this->inferEnumType($schema['enum']), + isset($schema['minLength']), isset($schema['maxLength']), isset($schema['pattern']), isset($schema['format']) => 'string', + isset($schema['minimum']), isset($schema['maximum']), isset($schema['multipleOf']) => 'number', + default => null, + }; + } + + /** + * Infer the scalar type shared by a homogeneous enum of scalars. + * + * @param array $enum + */ + protected function inferEnumType(array $enum): ?string + { + $resolved = null; + + foreach ($enum as $value) { + $current = match (true) { + is_bool($value) => 'boolean', + is_int($value) => 'integer', + is_float($value) => 'number', + is_string($value) => 'string', + default => null, + }; + + if ($current === null) { + return null; + } + + if ($resolved === null || $resolved === $current) { + $resolved = $current; + + continue; + } + + // A mix of integers and floats is still numeric; anything else is ambiguous... + if (in_array($resolved, ['integer', 'number'], true) && in_array($current, ['integer', 'number'], true)) { + $resolved = 'number'; + + continue; + } + + return null; + } + + return $resolved; + } + + /** + * Collapse "anyOf" / "oneOf" null branches into a single effective schema. + * + * @param array $schema + * @param array $refs + * @return array{0: array, 1: bool, 2: array} + * + * @throws \InvalidArgumentException + */ + protected function normalizeUnions(array $schema, array $refs = []): array + { + foreach (['anyOf', 'oneOf'] as $key) { + if (! isset($schema[$key]) || ! is_array($schema[$key])) { + continue; + } + + $nullable = false; + $branches = []; + + foreach ($schema[$key] as $branch) { + if (! is_array($branch)) { + continue; + } + + [$branch, $branchRefs] = $this->resolveRef($branch, $refs); + + if ($this->isNullBranch($branch)) { + $nullable = true; + } else { + $branches[] = [$branch, $branchRefs]; + } + } + + if (! $nullable || count($branches) !== 1) { + throw new InvalidArgumentException( + "Only a nullable \"{$key}\" (a single schema plus a \"null\" branch) is supported." + ); + } + + [$branch, $branchRefs] = $branches[0]; + + $siblings = $schema; + unset($siblings[$key]); + + foreach ($siblings as $siblingKey => $value) { + if (array_key_exists($siblingKey, $branch) && $branch[$siblingKey] !== $value) { + throw new InvalidArgumentException( + "Conflicting [{$siblingKey}] between a \"{$key}\" branch and its sibling keys." + ); + } + } + + return [array_merge($siblings, $branch), true, $branchRefs]; + } + + return [$schema, false, $refs]; + } + + /** + * Determine if the given schema branch describes only the "null" type. + * + * @param array $branch + */ + protected function isNullBranch(array $branch): bool + { + $type = $branch['type'] ?? null; + + return $type === 'null' || $type === ['null']; + } + + /** + * Resolve a local "$ref" against the root schema, merging sibling keys. + * + * @param array $schema + * @param array $refs + * @return array{0: array, 1: array} + * + * @throws \InvalidArgumentException + */ + protected function resolveRef(array $schema, array $refs = []): array + { + if (! isset($schema['$ref']) || ! is_string($schema['$ref'])) { + return [$schema, $refs]; + } + + $ref = $schema['$ref']; + + if (in_array($ref, $refs, true)) { + throw new InvalidArgumentException("Circular JSON Schema \$ref [{$ref}] detected."); + } + + $refs[] = $ref; + + $resolved = $this->lookupRef($ref); + + $siblings = $schema; + unset($siblings['$ref']); + + return $this->resolveRef(array_merge($resolved, $siblings), $refs); + } + + /** + * Look up a local JSON pointer reference within the root schema. + * + * @return array + * + * @throws \InvalidArgumentException + */ + protected function lookupRef(string $ref): array + { + if ($ref === '#') { + return $this->root; + } + + if (! str_starts_with($ref, '#/')) { + throw new InvalidArgumentException("Unable to resolve non-local JSON Schema \$ref [{$ref}]."); + } + + $target = $this->root; + + foreach (explode('/', substr($ref, 2)) as $segment) { + $segment = str_replace(['~1', '~0'], ['/', '~'], rawurldecode($segment)); + + if (! is_array($target) || ! array_key_exists($segment, $target)) { + throw new InvalidArgumentException("Unable to resolve JSON Schema \$ref [{$ref}]."); + } + + $target = $target[$segment]; + } + + if (! is_array($target)) { + throw new InvalidArgumentException("The JSON Schema \$ref [{$ref}] does not point to a schema."); + } + + return $target; + } + + /** + * Normalize the given value to an integer or float, or null when not numeric. + */ + protected function toNumber(mixed $value): int|float|null + { + if (is_int($value) || is_float($value)) { + return $value; + } + + if (is_string($value) && is_numeric($value)) { + return $value + 0; + } + + return null; + } + + /** + * Cast the given number to an integer, rejecting non-integer values. + * + * @throws \InvalidArgumentException + */ + protected function toInteger(int|float $value): int + { + if (is_float($value) && floor($value) !== $value) { + throw new InvalidArgumentException("The JSON Schema integer constraint [{$value}] must be an integer."); + } + + return (int) $value; + } +} diff --git a/src/Illuminate/JsonSchema/JsonSchema.php b/src/Illuminate/JsonSchema/JsonSchema.php index 20fdb2719976..5ca003061ce6 100644 --- a/src/Illuminate/JsonSchema/JsonSchema.php +++ b/src/Illuminate/JsonSchema/JsonSchema.php @@ -15,6 +15,18 @@ */ class JsonSchema { + /** + * Build a type from a raw array of the Laravel-supported JSON Schema subset. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + public static function fromArray(array $schema): Type + { + return Deserializer::deserialize($schema); + } + /** * Dynamically pass static methods to the schema instance. */ diff --git a/tests/JsonSchema/DeserializerTest.php b/tests/JsonSchema/DeserializerTest.php new file mode 100644 index 000000000000..d0b1ed5db4d7 --- /dev/null +++ b/tests/JsonSchema/DeserializerTest.php @@ -0,0 +1,577 @@ + JsonSchema::string()->min(1)->max(50)->pattern('^[a-z]+$')->required(), + 'age' => JsonSchema::integer()->min(0)->max(120)->default(18), + 'score' => JsonSchema::number()->min(0)->max(100)->multipleOf(0.5), + 'active' => JsonSchema::boolean()->default(true), + 'tags' => JsonSchema::array()->items(JsonSchema::string()->max(20))->min(1)->max(5)->unique(), + 'meta' => JsonSchema::object([ + 'created' => JsonSchema::string()->format('date-time')->required(), + ])->withoutAdditionalProperties(), + 'status' => JsonSchema::string()->enum(['draft', 'published'])->nullable(), + ])->title('User')->description('A user payload'); + + $array = Serializer::serialize($type); + + $rebuilt = JsonSchema::fromArray($array); + + $this->assertInstanceOf(ObjectType::class, $rebuilt); + $this->assertSame($array, Serializer::serialize($rebuilt)); + $this->assertEquals($type, $rebuilt); + } + + public function test_it_maps_every_supported_type(): void + { + $this->assertInstanceOf(ObjectType::class, JsonSchema::fromArray(['type' => 'object'])); + $this->assertInstanceOf(ArrayType::class, JsonSchema::fromArray(['type' => 'array'])); + $this->assertInstanceOf(StringType::class, JsonSchema::fromArray(['type' => 'string'])); + $this->assertInstanceOf(IntegerType::class, JsonSchema::fromArray(['type' => 'integer'])); + $this->assertInstanceOf(NumberType::class, JsonSchema::fromArray(['type' => 'number'])); + $this->assertInstanceOf(BooleanType::class, JsonSchema::fromArray(['type' => 'boolean'])); + } + + public function test_it_applies_string_constraints(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'string', + 'minLength' => 2, + 'maxLength' => 8, + 'pattern' => '^foo.*$', + 'format' => 'email', + ]); + + $this->assertEquals([ + 'type' => 'string', + 'minLength' => 2, + 'maxLength' => 8, + 'pattern' => '^foo.*$', + 'format' => 'email', + ], $type->toArray()); + } + + public function test_it_applies_integer_constraints(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'integer', + 'minimum' => 0, + 'maximum' => 100, + 'multipleOf' => 5, + ]); + + $this->assertInstanceOf(IntegerType::class, $type); + $this->assertEquals([ + 'type' => 'integer', + 'minimum' => 0, + 'maximum' => 100, + 'multipleOf' => 5, + ], $type->toArray()); + } + + public function test_it_applies_number_constraints_and_preserves_floats(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'number', + 'minimum' => 0.5, + 'maximum' => 9.9, + 'multipleOf' => 0.1, + ]); + + $this->assertInstanceOf(NumberType::class, $type); + + $array = $type->toArray(); + + $this->assertSame(0.5, $array['minimum']); + $this->assertSame(9.9, $array['maximum']); + $this->assertSame(0.1, $array['multipleOf']); + } + + public function test_it_applies_array_constraints_and_nested_items(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'array', + 'items' => ['type' => 'string', 'maxLength' => 3], + 'minItems' => 1, + 'maxItems' => 4, + 'uniqueItems' => true, + ]); + + $this->assertInstanceOf(ArrayType::class, $type); + $this->assertEquals([ + 'type' => 'array', + 'minItems' => 1, + 'maxItems' => 4, + 'items' => [ + 'type' => 'string', + 'maxLength' => 3, + ], + 'uniqueItems' => true, + ], $type->toArray()); + } + + public function test_it_builds_nested_objects_and_marks_required_children(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'minLength' => 1], + 'age' => ['type' => 'integer', 'minimum' => 0], + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'city' => ['type' => 'string'], + ], + 'required' => ['city'], + ], + ], + 'required' => ['name'], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'minLength' => 1], + 'age' => ['type' => 'integer', 'minimum' => 0], + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'city' => ['type' => 'string'], + ], + 'required' => ['city'], + ], + ], + 'required' => ['name'], + ], $type->toArray()); + } + + public function test_it_preserves_numeric_string_property_names_when_marking_required(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + '1' => ['type' => 'string'], + '4' => ['type' => 'string'], + ], + 'required' => ['1', '4'], + ]); + + $array = $type->toArray(); + + $this->assertEquals(['1', '4'], $array['required']); + $this->assertIsString($array['required'][0]); + } + + public function test_it_disallows_additional_properties_when_false(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'additionalProperties' => false, + ]); + + $this->assertEquals([ + 'type' => 'object', + 'additionalProperties' => false, + ], $type->toArray()); + } + + public function test_it_normalizes_nullable_from_a_type_array(): void + { + $type = JsonSchema::fromArray([ + 'type' => ['string', 'null'], + 'minLength' => 1, + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertEquals([ + 'type' => ['string', 'null'], + 'minLength' => 1, + ], $type->toArray()); + } + + public function test_it_normalizes_nullable_from_an_any_of_null_branch(): void + { + $type = JsonSchema::fromArray([ + 'title' => 'Nickname', + 'anyOf' => [ + ['type' => 'string', 'minLength' => 1], + ['type' => 'null'], + ], + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertEquals([ + 'title' => 'Nickname', + 'minLength' => 1, + 'type' => ['string', 'null'], + ], $type->toArray()); + } + + public function test_it_normalizes_nullable_from_a_one_of_null_branch(): void + { + $type = JsonSchema::fromArray([ + 'oneOf' => [ + ['type' => 'null'], + ['type' => 'integer', 'minimum' => 0], + ], + ]); + + $this->assertInstanceOf(IntegerType::class, $type); + $this->assertEquals([ + 'minimum' => 0, + 'type' => ['integer', 'null'], + ], $type->toArray()); + } + + public function test_it_resolves_a_local_ref_against_defs(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'author' => ['$ref' => '#/$defs/User'], + ], + 'required' => ['author'], + '$defs' => [ + 'User' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + ], + ], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'author' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + ], + ], + 'required' => ['author'], + ], $type->toArray()); + } + + public function test_it_resolves_a_local_ref_against_definitions(): void + { + $type = JsonSchema::fromArray([ + '$ref' => '#/definitions/Tag', + 'definitions' => [ + 'Tag' => ['type' => 'string', 'minLength' => 1], + ], + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertEquals([ + 'type' => 'string', + 'minLength' => 1, + ], $type->toArray()); + } + + public function test_it_merges_sibling_keys_over_a_ref(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'handle' => [ + '$ref' => '#/$defs/Name', + 'description' => 'Overridden description', + ], + ], + '$defs' => [ + 'Name' => [ + 'type' => 'string', + 'description' => 'Original description', + 'minLength' => 1, + ], + ], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'handle' => [ + 'description' => 'Overridden description', + 'minLength' => 1, + 'type' => 'string', + ], + ], + ], $type->toArray()); + } + + public function test_it_throws_for_an_unresolvable_ref(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to resolve JSON Schema $ref [#/$defs/Missing].'); + + JsonSchema::fromArray([ + '$ref' => '#/$defs/Missing', + '$defs' => [], + ]); + } + + public function test_it_throws_for_a_remote_ref(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to resolve non-local JSON Schema $ref [https://example.com/user.json].'); + + JsonSchema::fromArray([ + '$ref' => 'https://example.com/user.json', + ]); + } + + public function test_it_infers_object_type_from_properties(): void + { + $type = JsonSchema::fromArray([ + 'properties' => [ + 'name' => ['type' => 'string'], + ], + ]); + + $this->assertInstanceOf(ObjectType::class, $type); + } + + public function test_it_infers_array_type_from_items(): void + { + $type = JsonSchema::fromArray([ + 'items' => ['type' => 'integer'], + ]); + + $this->assertInstanceOf(ArrayType::class, $type); + $this->assertEquals([ + 'type' => 'array', + 'items' => ['type' => 'integer'], + ], $type->toArray()); + } + + public function test_it_infers_scalar_type_from_a_homogeneous_enum(): void + { + $this->assertInstanceOf(StringType::class, JsonSchema::fromArray([ + 'enum' => ['draft', 'published'], + ])); + + $this->assertInstanceOf(IntegerType::class, JsonSchema::fromArray([ + 'enum' => [1, 2, 3], + ])); + + $this->assertInstanceOf(NumberType::class, JsonSchema::fromArray([ + 'enum' => [1, 2.5, 3], + ])); + + $this->assertInstanceOf(BooleanType::class, JsonSchema::fromArray([ + 'enum' => [true, false], + ])); + } + + public function test_it_applies_enum_and_default(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'string', + 'enum' => ['draft', 'published'], + 'default' => 'draft', + ]); + + $this->assertEquals([ + 'type' => 'string', + 'default' => 'draft', + 'enum' => ['draft', 'published'], + ], $type->toArray()); + } + + public function test_it_ignores_unknown_keywords(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'string', + 'minLength' => 1, + '$schema' => 'https://json-schema.org/draft/2020-12/schema', + '$comment' => 'ignore me', + 'readOnly' => true, + 'contentEncoding' => 'base64', + ]); + + $this->assertEquals([ + 'type' => 'string', + 'minLength' => 1, + ], $type->toArray()); + } + + public function test_it_throws_when_the_type_cannot_be_determined(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to determine the JSON Schema type for the given schema.'); + + JsonSchema::fromArray([ + 'title' => 'Mystery', + ]); + } + + public function test_it_detects_a_circular_ref_instead_of_recursing(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Circular JSON Schema $ref [#/$defs/node] detected.'); + + JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'children' => ['type' => 'array', 'items' => ['$ref' => '#/$defs/node']], + ], + '$defs' => [ + 'node' => [ + 'type' => 'object', + 'properties' => [ + 'children' => ['type' => 'array', 'items' => ['$ref' => '#/$defs/node']], + ], + ], + ], + ]); + } + + public function test_it_resolves_the_same_ref_used_in_sibling_positions(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'home' => ['$ref' => '#/$defs/address'], + 'work' => ['$ref' => '#/$defs/address'], + ], + '$defs' => [ + 'address' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]], + ], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'home' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]], + 'work' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]], + ], + ], $type->toArray()); + } + + public function test_it_throws_for_a_multi_type_union(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to represent a multi-type JSON Schema union [string, integer].'); + + JsonSchema::fromArray([ + 'type' => ['string', 'integer'], + ]); + } + + public function test_it_throws_for_a_boolean_property_schema(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to represent the schema for property [meta]; boolean schemas are not supported.'); + + JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'meta' => true, + ], + ]); + } + + public function test_it_throws_for_a_non_numeric_numeric_constraint(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The JSON Schema [minimum] constraint must be a number.'); + + JsonSchema::fromArray([ + 'type' => 'number', + 'minimum' => 'oops', + ]); + } + + public function test_it_throws_for_a_non_integer_integer_constraint(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The JSON Schema integer constraint [1.9] must be an integer.'); + + JsonSchema::fromArray([ + 'type' => 'integer', + 'minimum' => 1.9, + ]); + } + + public function test_it_throws_for_tuple_items(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Tuple and boolean JSON Schema "items" are not supported.'); + + JsonSchema::fromArray([ + 'type' => 'array', + 'items' => [ + ['type' => 'string'], + ['type' => 'integer'], + ], + ]); + } + + public function test_it_throws_when_a_union_branch_conflicts_with_sibling_keys(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Conflicting [type] between a "anyOf" branch and its sibling keys.'); + + JsonSchema::fromArray([ + 'type' => 'integer', + 'anyOf' => [ + ['type' => 'string', 'minLength' => 3], + ['type' => 'null'], + ], + ]); + } + + public function test_it_throws_for_an_unsupported_union(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only a nullable "anyOf" (a single schema plus a "null" branch) is supported.'); + + JsonSchema::fromArray([ + 'anyOf' => [ + ['type' => 'string'], + ['type' => 'integer'], + ], + ]); + } + + public function test_it_throws_for_a_null_default(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A null JSON Schema [default] is not supported.'); + + JsonSchema::fromArray([ + 'type' => 'string', + 'default' => null, + ]); + } + + public function test_it_resolves_the_root_ref_pointer(): void + { + // "#" resolves to the root, so a self-reference is detected as circular... + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Circular JSON Schema $ref [#] detected.'); + + JsonSchema::fromArray(['$ref' => '#']); + } +} From bfbf5cf7b7ecae32c30318fbeee8d81179b0fd45 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 4 Jun 2026 16:32:59 +0100 Subject: [PATCH 526/596] [13.x] Add queue to InspectedJob (#60374) * 13.x-queue-inspecteJob-prop Update InspectedJob.php database fake fake test this is the same redis queue name bits specific cluster test * improve those * queue bits in the unit test * comment I imagine I will forget this in 3 weeks * switch about the constructor --- src/Illuminate/Queue/DatabaseQueue.php | 12 +++---- src/Illuminate/Queue/Jobs/InspectedJob.php | 6 +++- src/Illuminate/Queue/RedisQueue.php | 33 ++++++++----------- .../Support/Testing/Fakes/QueueFake.php | 2 ++ tests/Integration/Queue/RedisQueueTest.php | 6 ++++ tests/Queue/QueueDatabaseQueueUnitTest.php | 9 +++++ tests/Queue/QueueRedisQueueTest.php | 13 ++++++++ tests/Support/SupportTestingQueueFakeTest.php | 1 + 8 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index 4d341418175d..3441fd2775ba 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -145,7 +145,7 @@ public function pendingJobs($queue = null): Collection ->whereNull('reserved_at') ->where('available_at', '<=', $this->currentTime()) ->get() - ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts, $record->queue)); } /** @@ -161,7 +161,7 @@ public function delayedJobs($queue = null): Collection ->whereNull('reserved_at') ->where('available_at', '>', $this->currentTime()) ->get() - ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts, $record->queue)); } /** @@ -176,7 +176,7 @@ public function reservedJobs($queue = null): Collection ->where('queue', $this->getQueue($queue)) ->whereNotNull('reserved_at') ->get() - ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts, $record->queue)); } /** @@ -190,7 +190,7 @@ public function allPendingJobs(): Collection ->whereNull('reserved_at') ->where('available_at', '<=', $this->currentTime()) ->get() - ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts, $record->queue)); } /** @@ -204,7 +204,7 @@ public function allDelayedJobs(): Collection ->whereNull('reserved_at') ->where('available_at', '>', $this->currentTime()) ->get() - ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts, $record->queue)); } /** @@ -217,7 +217,7 @@ public function allReservedJobs(): Collection return $this->database->table($this->table) ->whereNotNull('reserved_at') ->get() - ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts, $record->queue)); } /** diff --git a/src/Illuminate/Queue/Jobs/InspectedJob.php b/src/Illuminate/Queue/Jobs/InspectedJob.php index 1989e20ad022..d07adc70d6e4 100644 --- a/src/Illuminate/Queue/Jobs/InspectedJob.php +++ b/src/Illuminate/Queue/Jobs/InspectedJob.php @@ -10,6 +10,7 @@ class InspectedJob * Create a new inspected job instance. * * @param string|null $uuid The unique identifier for the job. + * @param string|null $queue The name of the queue the job is on. * @param string|null $name The display name of the job. * @param int $attempts The number of times the job has been attempted. * @param array $payload @@ -17,6 +18,7 @@ class InspectedJob */ public function __construct( public readonly ?string $uuid, + public readonly ?string $queue, public readonly ?string $name, public readonly int $attempts, public readonly array $payload = [], @@ -29,14 +31,16 @@ public function __construct( * * @param string $payload The raw JSON job payload. * @param int|null $attempts The number of times the job has been attempted. + * @param string|null $queue The name of the queue the job is on. * @return static */ - public static function fromPayload(string $payload, ?int $attempts = null): static + public static function fromPayload(string $payload, ?int $attempts = null, ?string $queue = null): static { $decoded = json_decode($payload, true); return new static( uuid: $decoded['uuid'] ?? null, + queue: $queue, name: $decoded['displayName'] ?? null, attempts: $attempts ?? $decoded['attempts'] ?? 0, payload: $decoded, diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index a464840c3272..1da127c1abbd 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -160,10 +160,10 @@ public function reservedSize($queue = null) */ public function pendingJobs($queue = null): Collection { - $queue = $this->getQueueRedisKey($queue); + $name = $queue ?: $this->default; - return (new Collection($this->getConnection()->lrange($queue, 0, -1))) - ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + return (new Collection($this->getConnection()->lrange($this->getQueueRedisKey($queue), 0, -1))) + ->map(fn ($payload) => InspectedJob::fromPayload($payload, queue: $name)); } /** @@ -174,10 +174,10 @@ public function pendingJobs($queue = null): Collection */ public function delayedJobs($queue = null): Collection { - $queue = $this->getQueueRedisKey($queue); + $name = $queue ?: $this->default; - return (new Collection($this->getConnection()->zrange($queue.':delayed', 0, -1))) - ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + return (new Collection($this->getConnection()->zrange($this->getQueueRedisKey($queue).':delayed', 0, -1))) + ->map(fn ($payload) => InspectedJob::fromPayload($payload, queue: $name)); } /** @@ -188,10 +188,10 @@ public function delayedJobs($queue = null): Collection */ public function reservedJobs($queue = null): Collection { - $queue = $this->getQueueRedisKey($queue); + $name = $queue ?: $this->default; - return (new Collection($this->getConnection()->zrange($queue.':reserved', 0, -1))) - ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + return (new Collection($this->getConnection()->zrange($this->getQueueRedisKey($queue).':reserved', 0, -1))) + ->map(fn ($payload) => InspectedJob::fromPayload($payload, queue: $name)); } /** @@ -201,9 +201,7 @@ public function reservedJobs($queue = null): Collection */ public function allPendingJobs(): Collection { - return $this->allQueueNames() - ->flatMap(fn ($name) => $this->getConnection()->lrange($this->getQueueRedisKey($name), 0, -1)) - ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + return $this->allQueueNames()->flatMap(fn ($name) => $this->pendingJobs($name)); } /** @@ -213,9 +211,7 @@ public function allPendingJobs(): Collection */ public function allDelayedJobs(): Collection { - return $this->allQueueNames() - ->flatMap(fn ($name) => $this->getConnection()->zrange($this->getQueueRedisKey($name).':delayed', 0, -1)) - ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + return $this->allQueueNames()->flatMap(fn ($name) => $this->delayedJobs($name)); } /** @@ -225,9 +221,7 @@ public function allDelayedJobs(): Collection */ public function allReservedJobs(): Collection { - return $this->allQueueNames() - ->flatMap(fn ($name) => $this->getConnection()->zrange($this->getQueueRedisKey($name).':reserved', 0, -1)) - ->map(fn ($payload) => InspectedJob::fromPayload($payload)); + return $this->allQueueNames()->flatMap(fn ($name) => $this->reservedJobs($name)); } /** @@ -238,7 +232,8 @@ public function allReservedJobs(): Collection protected function allQueueNames(): Collection { return (new Collection($this->getConnection()->keys('queues:*'))) - ->map(fn ($key) => Str::between($key, 'queues:', ':')) + // Trim to ensure clusters get their braces removed... + ->map(fn ($key) => trim(Str::between($key, 'queues:', ':'), '{}')) ->unique() ->values(); } diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index e1d36b158fa5..3b229606d668 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -494,6 +494,7 @@ public function pendingJobs($queue = null): Collection : $data['job'], attempts: 0, payload: [], + queue: $queue, createdAt: null, )); } @@ -536,6 +537,7 @@ public function allPendingJobs(): Collection : $data['job'], attempts: 0, payload: [], + queue: $data['queue'], createdAt: null, )); } diff --git a/tests/Integration/Queue/RedisQueueTest.php b/tests/Integration/Queue/RedisQueueTest.php index d3e61d080337..61d61534c8e6 100644 --- a/tests/Integration/Queue/RedisQueueTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -628,6 +628,7 @@ public function testPendingJobs($driver) $this->assertSame(0, $pending->first()->attempts); $this->assertNotNull($pending->first()->uuid); $this->assertInstanceOf(Carbon::class, $pending->first()->createdAt); + $this->assertSame($default, $pending->first()->queue); } #[DataProvider('redisDriverProvider')] @@ -647,6 +648,7 @@ public function testDelayedJobs($driver) $this->assertSame(0, $delayed->first()->attempts); $this->assertNotNull($delayed->first()->uuid); $this->assertInstanceOf(Carbon::class, $delayed->first()->createdAt); + $this->assertSame($default, $delayed->first()->queue); } #[DataProvider('redisDriverProvider')] @@ -667,6 +669,7 @@ public function testReservedJobs($driver) $this->assertSame(1, $reserved->first()->attempts); $this->assertNotNull($reserved->first()->uuid); $this->assertInstanceOf(Carbon::class, $reserved->first()->createdAt); + $this->assertSame($default, $reserved->first()->queue); } #[DataProvider('redisDriverProvider')] @@ -686,6 +689,7 @@ public function testAllPendingJobs($driver) $this->assertSame(0, $pending->first()->attempts); $this->assertNotNull($pending->first()->uuid); $this->assertInstanceOf(Carbon::class, $pending->first()->createdAt); + $this->assertSame([$default, 'emails'], $pending->pluck('queue')->sort()->values()->all()); } #[DataProvider('redisDriverProvider')] @@ -705,6 +709,7 @@ public function testAllDelayedJobs($driver) $this->assertSame(0, $delayed->first()->attempts); $this->assertNotNull($delayed->first()->uuid); $this->assertInstanceOf(Carbon::class, $delayed->first()->createdAt); + $this->assertSame([$default, 'emails'], $delayed->pluck('queue')->sort()->values()->all()); } #[DataProvider('redisDriverProvider')] @@ -726,6 +731,7 @@ public function testAllReservedJobs($driver) $this->assertSame(1, $reserved->first()->attempts); $this->assertNotNull($reserved->first()->uuid); $this->assertInstanceOf(Carbon::class, $reserved->first()->createdAt); + $this->assertSame([$default, 'emails'], $reserved->pluck('queue')->sort()->values()->all()); } } diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index b9effe2f49ca..5ddc1f7f6dfa 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -264,6 +264,7 @@ public function testPendingJobs() $this->assertSame('MyTestJob', $jobs->first()->name); $this->assertSame('test-uuid', $jobs->first()->uuid); $this->assertSame(0, $jobs->first()->attempts); + $this->assertSame('default', $jobs->first()->queue); $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); } @@ -288,6 +289,7 @@ public function testDelayedJobs() $this->assertSame('MyDelayedJob', $jobs->first()->name); $this->assertSame('test-uuid', $jobs->first()->uuid); $this->assertSame(0, $jobs->first()->attempts); + $this->assertSame('default', $jobs->first()->queue); $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); } @@ -311,6 +313,7 @@ public function testReservedJobs() $this->assertSame('MyTestJob', $jobs->first()->name); $this->assertSame('test-uuid', $jobs->first()->uuid); $this->assertSame(1, $jobs->first()->attempts); + $this->assertSame('default', $jobs->first()->queue); $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); } @@ -338,10 +341,12 @@ public function testAllPendingJobs() $this->assertSame('JobA', $jobs->first()->name); $this->assertSame('uuid-1', $jobs->first()->uuid); $this->assertSame(0, $jobs->first()->attempts); + $this->assertSame('default', $jobs->first()->queue); $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); $this->assertSame('JobB', $jobs->last()->name); $this->assertSame('uuid-2', $jobs->last()->uuid); + $this->assertSame('emails', $jobs->last()->queue); } public function testAllDelayedJobs() @@ -367,10 +372,12 @@ public function testAllDelayedJobs() $this->assertSame('JobA', $jobs->first()->name); $this->assertSame('uuid-1', $jobs->first()->uuid); $this->assertSame(0, $jobs->first()->attempts); + $this->assertSame('default', $jobs->first()->queue); $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); $this->assertSame('JobB', $jobs->last()->name); $this->assertSame('uuid-2', $jobs->last()->uuid); + $this->assertSame('emails', $jobs->last()->queue); } public function testAllReservedJobs() @@ -395,11 +402,13 @@ public function testAllReservedJobs() $this->assertSame('JobA', $jobs->first()->name); $this->assertSame('uuid-1', $jobs->first()->uuid); $this->assertSame(1, $jobs->first()->attempts); + $this->assertSame('default', $jobs->first()->queue); $this->assertInstanceOf(Carbon::class, $jobs->first()->createdAt); $this->assertSame(1000000, $jobs->first()->createdAt->getTimestamp()); $this->assertSame('JobB', $jobs->last()->name); $this->assertSame('uuid-2', $jobs->last()->uuid); $this->assertSame(2, $jobs->last()->attempts); + $this->assertSame('emails', $jobs->last()->queue); } public function testGetLockForPoppingIsCached() diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 8511d5e793b7..2eeabfcfc6c8 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -411,6 +411,14 @@ public function testIsClusterConnectionCachesResult() $this->assertTrue($queue->testIsClusterConnection()); $this->assertTrue($queue->testIsClusterConnection()); } + + public function testAllQueueNamesStripsClusterBraces() + { + $queue = new TestableRedisQueue($redis = m::mock(Factory::class), 'default'); + $redis->shouldReceive('connection->keys')->andReturn(['queues:{default}', 'queues:{default}:delayed', 'queues:{emails}']); + + $this->assertSame(['default', 'emails'], $queue->testAllQueueNames()->all()); + } } class TestableRedisQueue extends RedisQueue @@ -424,4 +432,9 @@ public function testIsClusterConnection() { return $this->isClusterConnection(); } + + public function testAllQueueNames() + { + return $this->allQueueNames(); + } } diff --git a/tests/Support/SupportTestingQueueFakeTest.php b/tests/Support/SupportTestingQueueFakeTest.php index 66ed7062381b..71f905dbc0fc 100644 --- a/tests/Support/SupportTestingQueueFakeTest.php +++ b/tests/Support/SupportTestingQueueFakeTest.php @@ -514,6 +514,7 @@ public function testPendingJobs() $this->assertInstanceOf(InspectedJob::class, $pending->first()); $this->assertSame(JobStub::class, $pending->first()->name); $this->assertSame(0, $pending->first()->attempts); + $this->assertSame('foo', $pending->first()->queue); } public function testPendingJobsAcceptsUnitEnums() From 733409d3c7d14ac78a87121e755e4d6bd392ea2e Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 4 Jun 2026 16:35:31 +0100 Subject: [PATCH 527/596] Fix treatment of null headers (#60386) --- src/Illuminate/Http/Client/Factory.php | 6 ++-- src/Illuminate/Http/Client/PendingRequest.php | 9 +++-- tests/Http/HttpClientTest.php | 35 ++++++++++--------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 5d147c6abbfb..dd5eadb5a10e 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -205,9 +205,10 @@ protected static function normalizeResponseHeaders(array $headers): array foreach ($value as $key => $item) { $value[$key] = match (true) { + $item === null => '', is_scalar($item) => (string) $item, $item instanceof Stringable => $item->toString(), - default => throw new InvalidArgumentException('HTTP fake response header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + default => throw new InvalidArgumentException('HTTP fake response header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.'), }; } @@ -217,9 +218,10 @@ protected static function normalizeResponseHeaders(array $headers): array } $headers[$name] = match (true) { + $value === null => '', is_scalar($value) => (string) $value, $value instanceof Stringable => $value->toString(), - default => throw new InvalidArgumentException('HTTP fake response header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + default => throw new InvalidArgumentException('HTTP fake response header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.'), }; } diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 0b5518bb4170..cd95c3316673 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1431,9 +1431,10 @@ protected function normalizeHeaderValue($value): string|array foreach ($value as $key => $item) { $value[$key] = match (true) { + $item === null => '', is_scalar($item) => (string) $item, $item instanceof Stringable => $item->toString(), - default => throw new InvalidArgumentException('HTTP header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + default => throw new InvalidArgumentException('HTTP header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.'), }; } @@ -1441,9 +1442,10 @@ protected function normalizeHeaderValue($value): string|array } return match (true) { + $value === null => '', is_scalar($value) => (string) $value, $value instanceof Stringable => $value->toString(), - default => throw new InvalidArgumentException('HTTP header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'), + default => throw new InvalidArgumentException('HTTP header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.'), }; } @@ -1489,9 +1491,10 @@ protected function normalizeMultipartHeaders(array $multipart): array foreach ($part['headers'] as $name => $value) { $multipart[$index]['headers'][$name] = match (true) { $value === [] => '', + $value === null => '', is_scalar($value) => (string) $value, $value instanceof Stringable => $value->toString(), - default => throw new InvalidArgumentException('Multipart header values must be scalar or Laravel Stringable.'), + default => throw new InvalidArgumentException('Multipart header values must be scalar, null, or Laravel Stringable.'), }; } } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index db84389c405c..30d9556acceb 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -104,24 +104,26 @@ public function testFakeResponseHeaderValuesAreSerialized() { $response = $this->factory::response('OK', 200, [ 'X-Int' => 123, + 'X-Null' => null, 'X-False' => false, 'X-Empty' => [], 'X-Laravel-Stringable' => new Stringable('laravel stringable'), - 'X-Multiple' => ['first', 123, true, false], + 'X-Multiple' => ['first', 123, true, false, null], ])->wait(); $this->assertSame(['123'], $response->getHeader('X-Int')); + $this->assertSame([''], $response->getHeader('X-Null')); $this->assertSame([''], $response->getHeader('X-False')); $this->assertSame([''], $response->getHeader('X-Empty')); $this->assertSame(['laravel stringable'], $response->getHeader('X-Laravel-Stringable')); - $this->assertSame(['first', '123', '1', ''], $response->getHeader('X-Multiple')); + $this->assertSame(['first', '123', '1', '', ''], $response->getHeader('X-Multiple')); } #[DataProvider('invalidFakeResponseHeaderValuesProvider')] public function testInvalidFakeResponseHeaderValuesAreRejected($value) { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('HTTP fake response header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'); + $this->expectExceptionMessage('HTTP fake response header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.'); $this->factory::response('OK', 200, ['X-Test' => $value]); } @@ -711,20 +713,22 @@ public function testHeaderValuesAreSerialized() $this->factory->withHeaders([ 'X-Int' => 123, 'X-Float' => 1.5, + 'X-Null' => null, 'X-True' => true, 'X-False' => false, 'X-Laravel-Stringable' => new Stringable('laravel stringable'), - 'X-Multiple' => ['first', 123, true, false], + 'X-Multiple' => ['first', 123, true, false, null], 'X-Empty' => [], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('X-Int', '123') && $request->hasHeader('X-Float', '1.5') + && $request->hasHeader('X-Null', '') && $request->hasHeader('X-True', '1') && $request->hasHeader('X-False', '') && $request->hasHeader('X-Laravel-Stringable', 'laravel stringable') - && $request->hasHeader('X-Multiple', ['first', '123', '1', '']) + && $request->hasHeader('X-Multiple', ['first', '123', '1', '', '']) && $request->hasHeader('X-Empty', ''); }); } @@ -735,7 +739,7 @@ public function testInvalidHeaderValuesAreRejected($value) $this->factory->fake(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('HTTP header values must be scalar, Laravel Stringable, or arrays of scalar or Laravel Stringable values.'); + $this->expectExceptionMessage('HTTP header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.'); $this->factory->withHeaders(['X-Test' => $value])->post('http://foo.com/json'); } @@ -745,11 +749,12 @@ public function testHeaderValuesProvidedThroughOptionsAreSerialized() $this->factory->fake(); $this->factory->withOptions([ - 'headers' => ['X-Test' => 123], + 'headers' => ['X-Test' => 123, 'X-Null' => null], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { - return $request->hasHeader('X-Test', '123'); + return $request->hasHeader('X-Test', '123') + && $request->hasHeader('X-Null', ''); }); } @@ -907,10 +912,11 @@ public function testAttachHeaderValuesAreSerialized() { $this->factory->fake(); - $this->factory->attach('file', 'data', 'file.txt', ['X-Part' => 123])->post('http://foo.com/file'); + $this->factory->attach('file', 'data', 'file.txt', ['X-Part' => 123, 'X-Null' => null])->post('http://foo.com/file'); $this->factory->assertSent(function (Request $request) { - return $request[0]['headers']['X-Part'] === '123'; + return $request[0]['headers']['X-Part'] === '123' + && $request[0]['headers']['X-Null'] === ''; }); } @@ -924,6 +930,7 @@ public function testMultipartHeaderValuesAreSerialized() 'contents' => 'data', 'headers' => [ 'X-Part' => 123, + 'X-Null' => null, 'X-Empty' => [], 'X-Laravel-Stringable' => new Stringable('laravel stringable'), ], @@ -932,6 +939,7 @@ public function testMultipartHeaderValuesAreSerialized() $this->factory->assertSent(function (Request $request) { return $request[0]['headers']['X-Part'] === '123' + && $request[0]['headers']['X-Null'] === '' && $request[0]['headers']['X-Empty'] === '' && $request[0]['headers']['X-Laravel-Stringable'] === 'laravel stringable'; }); @@ -943,7 +951,7 @@ public function testInvalidMultipartHeaderValuesAreRejected($value) $this->factory->fake(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Multipart header values must be scalar or Laravel Stringable.'); + $this->expectExceptionMessage('Multipart header values must be scalar, null, or Laravel Stringable.'); $this->factory->asMultipart()->post('http://foo.com/multipart', [ [ @@ -4580,10 +4588,8 @@ public static function methodsReceivingArrayableDataProvider() public static function invalidHeaderValuesProvider() { return [ - 'null' => [null], 'object' => [new stdClass], 'resource' => [fopen('php://temp', 'r')], - 'array with null' => [['valid', null]], 'array with object' => [['valid', new stdClass]], 'array with resource' => [['valid', fopen('php://temp', 'r')]], 'array with nested array' => [['valid', ['nested']]], @@ -4593,7 +4599,6 @@ public static function invalidHeaderValuesProvider() public static function invalidMultipartHeaderValuesProvider() { return [ - 'null' => [null], 'array' => [['nested']], 'object' => [new stdClass], 'resource' => [fopen('php://temp', 'r')], @@ -4603,10 +4608,8 @@ public static function invalidMultipartHeaderValuesProvider() public static function invalidFakeResponseHeaderValuesProvider() { return [ - 'null' => [null], 'object' => [new stdClass], 'resource' => [fopen('php://temp', 'r')], - 'array with null' => [['valid', null]], 'array with object' => [['valid', new stdClass]], 'array with resource' => [['valid', fopen('php://temp', 'r')]], 'array with nested array' => [['valid', ['nested']]], From 127dcda8f652a2b7455cb648c3b8b77d45866bad Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Thu, 4 Jun 2026 22:54:08 +0530 Subject: [PATCH 528/596] [12.x] Add JSON Schema array deserializer (#60387) * Fix numeric property names being cast to integers in JsonSchema required array #60146 (#60149) * Add JSON Schema array deserializer Backport of laravel/framework#60384. Adds JsonSchema::fromArray() backed by a new Deserializer that turns a raw JSON Schema array back into Type objects, round-tripping with Serializer::serialize(). --------- Co-authored-by: Fazle Rabbi <35403788+irabbi360@users.noreply.github.com> --- src/Illuminate/JsonSchema/Deserializer.php | 524 +++++++++++++++++++ src/Illuminate/JsonSchema/JsonSchema.php | 12 + src/Illuminate/JsonSchema/Serializer.php | 11 +- tests/JsonSchema/DeserializerTest.php | 577 +++++++++++++++++++++ tests/JsonSchema/ObjectTypeTest.php | 14 + 5 files changed, 1134 insertions(+), 4 deletions(-) create mode 100644 src/Illuminate/JsonSchema/Deserializer.php create mode 100644 tests/JsonSchema/DeserializerTest.php diff --git a/src/Illuminate/JsonSchema/Deserializer.php b/src/Illuminate/JsonSchema/Deserializer.php new file mode 100644 index 000000000000..dab25e9ce89e --- /dev/null +++ b/src/Illuminate/JsonSchema/Deserializer.php @@ -0,0 +1,524 @@ + + */ + protected array $root; + + /** + * Create a new deserializer instance. + * + * @param array $root + */ + protected function __construct(array $root) + { + $this->root = $root; + } + + /** + * Deserialize the Laravel-supported JSON Schema subset into a type. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + public static function deserialize(array $schema): Types\Type + { + return (new static($schema))->build($schema); + } + + /** + * Build a type from the given schema fragment. + * + * @param array $schema + * @param array $refs + * + * @throws \InvalidArgumentException + */ + protected function build(array $schema, array $refs = []): Types\Type + { + [$schema, $refs] = $this->resolveRef($schema, $refs); + + [$schema, $nullableFromUnion, $refs] = $this->normalizeUnions($schema, $refs); + + [$name, $nullableFromType] = $this->resolveType($schema); + + $type = match ($name) { + 'object' => $this->buildObject($schema, $refs), + 'array' => $this->buildArray($schema, $refs), + 'string' => $this->buildString($schema), + 'integer' => $this->buildInteger($schema), + 'number' => $this->buildNumber($schema), + 'boolean' => new Types\BooleanType, + default => throw new InvalidArgumentException("Unsupported JSON Schema type [{$name}]."), + }; + + $this->applyCommon($type, $schema); + + if ($nullableFromUnion || $nullableFromType) { + $type->nullable(); + } + + return $type; + } + + /** + * Build an object type from the given schema fragment. + * + * @param array $schema + * @param array $refs + * + * @throws \InvalidArgumentException + */ + protected function buildObject(array $schema, array $refs = []): Types\ObjectType + { + $properties = []; + + if (isset($schema['properties']) && is_array($schema['properties'])) { + $required = is_array($schema['required'] ?? null) + ? array_map('strval', $schema['required']) + : []; + + foreach ($schema['properties'] as $key => $definition) { + if (! is_array($definition)) { + throw new InvalidArgumentException( + "Unable to represent the schema for property [{$key}]; boolean schemas are not supported." + ); + } + + $property = $this->build($definition, $refs); + + if (in_array((string) $key, $required, true)) { + $property->required(); + } + + $properties[$key] = $property; + } + } + + $type = new Types\ObjectType($properties); + + if (($schema['additionalProperties'] ?? null) === false) { + $type->withoutAdditionalProperties(); + } + + return $type; + } + + /** + * Build an array type from the given schema fragment. + * + * @param array $schema + * @param array $refs + * + * @throws \InvalidArgumentException + */ + protected function buildArray(array $schema, array $refs = []): Types\ArrayType + { + $type = new Types\ArrayType; + + if (isset($schema['items']) && $schema['items'] !== []) { + if (! is_array($schema['items']) || array_is_list($schema['items'])) { + throw new InvalidArgumentException('Tuple and boolean JSON Schema "items" are not supported.'); + } + + $type->items($this->build($schema['items'], $refs)); + } + + if (isset($schema['minItems'])) { + $type->min((int) $schema['minItems']); + } + + if (isset($schema['maxItems'])) { + $type->max((int) $schema['maxItems']); + } + + if (isset($schema['uniqueItems'])) { + $type->unique((bool) $schema['uniqueItems']); + } + + return $type; + } + + /** + * Build a string type from the given schema fragment. + * + * @param array $schema + */ + protected function buildString(array $schema): Types\StringType + { + $type = new Types\StringType; + + if (isset($schema['minLength'])) { + $type->min((int) $schema['minLength']); + } + + if (isset($schema['maxLength'])) { + $type->max((int) $schema['maxLength']); + } + + if (isset($schema['pattern'])) { + $type->pattern((string) $schema['pattern']); + } + + if (isset($schema['format'])) { + $type->format((string) $schema['format']); + } + + return $type; + } + + /** + * Build an integer type from the given schema fragment. + * + * @param array $schema + */ + protected function buildInteger(array $schema): Types\IntegerType + { + return $this->applyNumericBounds(new Types\IntegerType, $schema, $this->toInteger(...)); + } + + /** + * Build a number type from the given schema fragment. + * + * @param array $schema + */ + protected function buildNumber(array $schema): Types\NumberType + { + return $this->applyNumericBounds(new Types\NumberType, $schema); + } + + /** + * Apply the numeric bound keywords to the given integer or number type. + * + * @template TType of Types\IntegerType|Types\NumberType + * + * @param TType $type + * @param array $schema + * @param (callable(int|float): (int|float))|null $cast + * @return TType + * + * @throws \InvalidArgumentException + */ + protected function applyNumericBounds(Types\IntegerType|Types\NumberType $type, array $schema, ?callable $cast = null) + { + $cast ??= static fn (int|float $value) => $value; + + foreach (['minimum' => 'min', 'maximum' => 'max', 'multipleOf' => 'multipleOf'] as $keyword => $method) { + if (! isset($schema[$keyword])) { + continue; + } + + if (($value = $this->toNumber($schema[$keyword])) === null) { + throw new InvalidArgumentException("The JSON Schema [{$keyword}] constraint must be a number."); + } + + $type->{$method}($cast($value)); + } + + return $type; + } + + /** + * Apply the keywords shared by every type to the given instance. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + protected function applyCommon(Types\Type $type, array $schema): void + { + if (isset($schema['title'])) { + $type->title((string) $schema['title']); + } + + if (isset($schema['description'])) { + $type->description((string) $schema['description']); + } + + if (isset($schema['enum']) && is_array($schema['enum'])) { + $type->enum($schema['enum']); + } + + if (array_key_exists('default', $schema)) { + if ($schema['default'] === null) { + throw new InvalidArgumentException('A null JSON Schema [default] is not supported.'); + } + + // The "default" setter is typed per concrete type, so assign it directly... + (fn () => $this->default = $schema['default'])->call($type); + } + } + + /** + * Resolve the base type name and whether the schema is nullable. + * + * @param array $schema + * @return array{0: string, 1: bool} + * + * @throws \InvalidArgumentException + */ + protected function resolveType(array $schema): array + { + $type = $schema['type'] ?? null; + $nullable = false; + + if (is_array($type)) { + $nullable = in_array('null', $type, true); + + $names = array_values(array_unique(array_filter( + $type, + static fn ($value) => $value !== 'null', + ))); + + if (count($names) > 1) { + throw new InvalidArgumentException( + 'Unable to represent a multi-type JSON Schema union ['.implode(', ', array_map('strval', $names)).'].' + ); + } + + $type = $names[0] ?? null; + } + + $type ??= $this->inferType($schema); + + if (! is_string($type)) { + throw new InvalidArgumentException('Unable to determine the JSON Schema type for the given schema.'); + } + + return [$type, $nullable]; + } + + /** + * Infer the type name when "type" is absent but the shape is unambiguous. + * + * @param array $schema + */ + protected function inferType(array $schema): ?string + { + return match (true) { + isset($schema['properties']), isset($schema['additionalProperties']), isset($schema['required']) => 'object', + isset($schema['items']), isset($schema['minItems']), isset($schema['maxItems']), isset($schema['uniqueItems']) => 'array', + isset($schema['enum']) && is_array($schema['enum']) => $this->inferEnumType($schema['enum']), + isset($schema['minLength']), isset($schema['maxLength']), isset($schema['pattern']), isset($schema['format']) => 'string', + isset($schema['minimum']), isset($schema['maximum']), isset($schema['multipleOf']) => 'number', + default => null, + }; + } + + /** + * Infer the scalar type shared by a homogeneous enum of scalars. + * + * @param array $enum + */ + protected function inferEnumType(array $enum): ?string + { + $resolved = null; + + foreach ($enum as $value) { + $current = match (true) { + is_bool($value) => 'boolean', + is_int($value) => 'integer', + is_float($value) => 'number', + is_string($value) => 'string', + default => null, + }; + + if ($current === null) { + return null; + } + + if ($resolved === null || $resolved === $current) { + $resolved = $current; + + continue; + } + + // A mix of integers and floats is still numeric; anything else is ambiguous... + if (in_array($resolved, ['integer', 'number'], true) && in_array($current, ['integer', 'number'], true)) { + $resolved = 'number'; + + continue; + } + + return null; + } + + return $resolved; + } + + /** + * Collapse "anyOf" / "oneOf" null branches into a single effective schema. + * + * @param array $schema + * @param array $refs + * @return array{0: array, 1: bool, 2: array} + * + * @throws \InvalidArgumentException + */ + protected function normalizeUnions(array $schema, array $refs = []): array + { + foreach (['anyOf', 'oneOf'] as $key) { + if (! isset($schema[$key]) || ! is_array($schema[$key])) { + continue; + } + + $nullable = false; + $branches = []; + + foreach ($schema[$key] as $branch) { + if (! is_array($branch)) { + continue; + } + + [$branch, $branchRefs] = $this->resolveRef($branch, $refs); + + if ($this->isNullBranch($branch)) { + $nullable = true; + } else { + $branches[] = [$branch, $branchRefs]; + } + } + + if (! $nullable || count($branches) !== 1) { + throw new InvalidArgumentException( + "Only a nullable \"{$key}\" (a single schema plus a \"null\" branch) is supported." + ); + } + + [$branch, $branchRefs] = $branches[0]; + + $siblings = $schema; + unset($siblings[$key]); + + foreach ($siblings as $siblingKey => $value) { + if (array_key_exists($siblingKey, $branch) && $branch[$siblingKey] !== $value) { + throw new InvalidArgumentException( + "Conflicting [{$siblingKey}] between a \"{$key}\" branch and its sibling keys." + ); + } + } + + return [array_merge($siblings, $branch), true, $branchRefs]; + } + + return [$schema, false, $refs]; + } + + /** + * Determine if the given schema branch describes only the "null" type. + * + * @param array $branch + */ + protected function isNullBranch(array $branch): bool + { + $type = $branch['type'] ?? null; + + return $type === 'null' || $type === ['null']; + } + + /** + * Resolve a local "$ref" against the root schema, merging sibling keys. + * + * @param array $schema + * @param array $refs + * @return array{0: array, 1: array} + * + * @throws \InvalidArgumentException + */ + protected function resolveRef(array $schema, array $refs = []): array + { + if (! isset($schema['$ref']) || ! is_string($schema['$ref'])) { + return [$schema, $refs]; + } + + $ref = $schema['$ref']; + + if (in_array($ref, $refs, true)) { + throw new InvalidArgumentException("Circular JSON Schema \$ref [{$ref}] detected."); + } + + $refs[] = $ref; + + $resolved = $this->lookupRef($ref); + + $siblings = $schema; + unset($siblings['$ref']); + + return $this->resolveRef(array_merge($resolved, $siblings), $refs); + } + + /** + * Look up a local JSON pointer reference within the root schema. + * + * @return array + * + * @throws \InvalidArgumentException + */ + protected function lookupRef(string $ref): array + { + if ($ref === '#') { + return $this->root; + } + + if (! str_starts_with($ref, '#/')) { + throw new InvalidArgumentException("Unable to resolve non-local JSON Schema \$ref [{$ref}]."); + } + + $target = $this->root; + + foreach (explode('/', substr($ref, 2)) as $segment) { + $segment = str_replace(['~1', '~0'], ['/', '~'], rawurldecode($segment)); + + if (! is_array($target) || ! array_key_exists($segment, $target)) { + throw new InvalidArgumentException("Unable to resolve JSON Schema \$ref [{$ref}]."); + } + + $target = $target[$segment]; + } + + if (! is_array($target)) { + throw new InvalidArgumentException("The JSON Schema \$ref [{$ref}] does not point to a schema."); + } + + return $target; + } + + /** + * Normalize the given value to an integer or float, or null when not numeric. + */ + protected function toNumber(mixed $value): int|float|null + { + if (is_int($value) || is_float($value)) { + return $value; + } + + if (is_string($value) && is_numeric($value)) { + return $value + 0; + } + + return null; + } + + /** + * Cast the given number to an integer, rejecting non-integer values. + * + * @throws \InvalidArgumentException + */ + protected function toInteger(int|float $value): int + { + if (is_float($value) && floor($value) !== $value) { + throw new InvalidArgumentException("The JSON Schema integer constraint [{$value}] must be an integer."); + } + + return (int) $value; + } +} diff --git a/src/Illuminate/JsonSchema/JsonSchema.php b/src/Illuminate/JsonSchema/JsonSchema.php index 20fdb2719976..5ca003061ce6 100644 --- a/src/Illuminate/JsonSchema/JsonSchema.php +++ b/src/Illuminate/JsonSchema/JsonSchema.php @@ -15,6 +15,18 @@ */ class JsonSchema { + /** + * Build a type from a raw array of the Laravel-supported JSON Schema subset. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + public static function fromArray(array $schema): Type + { + return Deserializer::deserialize($schema); + } + /** * Dynamically pass static methods to the schema instance. */ diff --git a/src/Illuminate/JsonSchema/Serializer.php b/src/Illuminate/JsonSchema/Serializer.php index 7750caebd403..93454ae29e86 100644 --- a/src/Illuminate/JsonSchema/Serializer.php +++ b/src/Illuminate/JsonSchema/Serializer.php @@ -53,10 +53,13 @@ public static function serialize(Types\Type $type): array if (count($attributes['properties']) === 0) { unset($attributes['properties']); } else { - $required = array_keys(array_filter( - $attributes['properties'], - static fn (Types\Type $property) => static::isRequired($property), - )); + $required = array_map( + 'strval', + array_keys(array_filter( + $attributes['properties'], + static fn (Types\Type $property) => static::isRequired($property), + )) + ); if (count($required) > 0) { $attributes['required'] = $required; diff --git a/tests/JsonSchema/DeserializerTest.php b/tests/JsonSchema/DeserializerTest.php new file mode 100644 index 000000000000..d0b1ed5db4d7 --- /dev/null +++ b/tests/JsonSchema/DeserializerTest.php @@ -0,0 +1,577 @@ + JsonSchema::string()->min(1)->max(50)->pattern('^[a-z]+$')->required(), + 'age' => JsonSchema::integer()->min(0)->max(120)->default(18), + 'score' => JsonSchema::number()->min(0)->max(100)->multipleOf(0.5), + 'active' => JsonSchema::boolean()->default(true), + 'tags' => JsonSchema::array()->items(JsonSchema::string()->max(20))->min(1)->max(5)->unique(), + 'meta' => JsonSchema::object([ + 'created' => JsonSchema::string()->format('date-time')->required(), + ])->withoutAdditionalProperties(), + 'status' => JsonSchema::string()->enum(['draft', 'published'])->nullable(), + ])->title('User')->description('A user payload'); + + $array = Serializer::serialize($type); + + $rebuilt = JsonSchema::fromArray($array); + + $this->assertInstanceOf(ObjectType::class, $rebuilt); + $this->assertSame($array, Serializer::serialize($rebuilt)); + $this->assertEquals($type, $rebuilt); + } + + public function test_it_maps_every_supported_type(): void + { + $this->assertInstanceOf(ObjectType::class, JsonSchema::fromArray(['type' => 'object'])); + $this->assertInstanceOf(ArrayType::class, JsonSchema::fromArray(['type' => 'array'])); + $this->assertInstanceOf(StringType::class, JsonSchema::fromArray(['type' => 'string'])); + $this->assertInstanceOf(IntegerType::class, JsonSchema::fromArray(['type' => 'integer'])); + $this->assertInstanceOf(NumberType::class, JsonSchema::fromArray(['type' => 'number'])); + $this->assertInstanceOf(BooleanType::class, JsonSchema::fromArray(['type' => 'boolean'])); + } + + public function test_it_applies_string_constraints(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'string', + 'minLength' => 2, + 'maxLength' => 8, + 'pattern' => '^foo.*$', + 'format' => 'email', + ]); + + $this->assertEquals([ + 'type' => 'string', + 'minLength' => 2, + 'maxLength' => 8, + 'pattern' => '^foo.*$', + 'format' => 'email', + ], $type->toArray()); + } + + public function test_it_applies_integer_constraints(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'integer', + 'minimum' => 0, + 'maximum' => 100, + 'multipleOf' => 5, + ]); + + $this->assertInstanceOf(IntegerType::class, $type); + $this->assertEquals([ + 'type' => 'integer', + 'minimum' => 0, + 'maximum' => 100, + 'multipleOf' => 5, + ], $type->toArray()); + } + + public function test_it_applies_number_constraints_and_preserves_floats(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'number', + 'minimum' => 0.5, + 'maximum' => 9.9, + 'multipleOf' => 0.1, + ]); + + $this->assertInstanceOf(NumberType::class, $type); + + $array = $type->toArray(); + + $this->assertSame(0.5, $array['minimum']); + $this->assertSame(9.9, $array['maximum']); + $this->assertSame(0.1, $array['multipleOf']); + } + + public function test_it_applies_array_constraints_and_nested_items(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'array', + 'items' => ['type' => 'string', 'maxLength' => 3], + 'minItems' => 1, + 'maxItems' => 4, + 'uniqueItems' => true, + ]); + + $this->assertInstanceOf(ArrayType::class, $type); + $this->assertEquals([ + 'type' => 'array', + 'minItems' => 1, + 'maxItems' => 4, + 'items' => [ + 'type' => 'string', + 'maxLength' => 3, + ], + 'uniqueItems' => true, + ], $type->toArray()); + } + + public function test_it_builds_nested_objects_and_marks_required_children(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'minLength' => 1], + 'age' => ['type' => 'integer', 'minimum' => 0], + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'city' => ['type' => 'string'], + ], + 'required' => ['city'], + ], + ], + 'required' => ['name'], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'minLength' => 1], + 'age' => ['type' => 'integer', 'minimum' => 0], + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'city' => ['type' => 'string'], + ], + 'required' => ['city'], + ], + ], + 'required' => ['name'], + ], $type->toArray()); + } + + public function test_it_preserves_numeric_string_property_names_when_marking_required(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + '1' => ['type' => 'string'], + '4' => ['type' => 'string'], + ], + 'required' => ['1', '4'], + ]); + + $array = $type->toArray(); + + $this->assertEquals(['1', '4'], $array['required']); + $this->assertIsString($array['required'][0]); + } + + public function test_it_disallows_additional_properties_when_false(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'additionalProperties' => false, + ]); + + $this->assertEquals([ + 'type' => 'object', + 'additionalProperties' => false, + ], $type->toArray()); + } + + public function test_it_normalizes_nullable_from_a_type_array(): void + { + $type = JsonSchema::fromArray([ + 'type' => ['string', 'null'], + 'minLength' => 1, + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertEquals([ + 'type' => ['string', 'null'], + 'minLength' => 1, + ], $type->toArray()); + } + + public function test_it_normalizes_nullable_from_an_any_of_null_branch(): void + { + $type = JsonSchema::fromArray([ + 'title' => 'Nickname', + 'anyOf' => [ + ['type' => 'string', 'minLength' => 1], + ['type' => 'null'], + ], + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertEquals([ + 'title' => 'Nickname', + 'minLength' => 1, + 'type' => ['string', 'null'], + ], $type->toArray()); + } + + public function test_it_normalizes_nullable_from_a_one_of_null_branch(): void + { + $type = JsonSchema::fromArray([ + 'oneOf' => [ + ['type' => 'null'], + ['type' => 'integer', 'minimum' => 0], + ], + ]); + + $this->assertInstanceOf(IntegerType::class, $type); + $this->assertEquals([ + 'minimum' => 0, + 'type' => ['integer', 'null'], + ], $type->toArray()); + } + + public function test_it_resolves_a_local_ref_against_defs(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'author' => ['$ref' => '#/$defs/User'], + ], + 'required' => ['author'], + '$defs' => [ + 'User' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + ], + ], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'author' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + ], + ], + 'required' => ['author'], + ], $type->toArray()); + } + + public function test_it_resolves_a_local_ref_against_definitions(): void + { + $type = JsonSchema::fromArray([ + '$ref' => '#/definitions/Tag', + 'definitions' => [ + 'Tag' => ['type' => 'string', 'minLength' => 1], + ], + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertEquals([ + 'type' => 'string', + 'minLength' => 1, + ], $type->toArray()); + } + + public function test_it_merges_sibling_keys_over_a_ref(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'handle' => [ + '$ref' => '#/$defs/Name', + 'description' => 'Overridden description', + ], + ], + '$defs' => [ + 'Name' => [ + 'type' => 'string', + 'description' => 'Original description', + 'minLength' => 1, + ], + ], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'handle' => [ + 'description' => 'Overridden description', + 'minLength' => 1, + 'type' => 'string', + ], + ], + ], $type->toArray()); + } + + public function test_it_throws_for_an_unresolvable_ref(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to resolve JSON Schema $ref [#/$defs/Missing].'); + + JsonSchema::fromArray([ + '$ref' => '#/$defs/Missing', + '$defs' => [], + ]); + } + + public function test_it_throws_for_a_remote_ref(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to resolve non-local JSON Schema $ref [https://example.com/user.json].'); + + JsonSchema::fromArray([ + '$ref' => 'https://example.com/user.json', + ]); + } + + public function test_it_infers_object_type_from_properties(): void + { + $type = JsonSchema::fromArray([ + 'properties' => [ + 'name' => ['type' => 'string'], + ], + ]); + + $this->assertInstanceOf(ObjectType::class, $type); + } + + public function test_it_infers_array_type_from_items(): void + { + $type = JsonSchema::fromArray([ + 'items' => ['type' => 'integer'], + ]); + + $this->assertInstanceOf(ArrayType::class, $type); + $this->assertEquals([ + 'type' => 'array', + 'items' => ['type' => 'integer'], + ], $type->toArray()); + } + + public function test_it_infers_scalar_type_from_a_homogeneous_enum(): void + { + $this->assertInstanceOf(StringType::class, JsonSchema::fromArray([ + 'enum' => ['draft', 'published'], + ])); + + $this->assertInstanceOf(IntegerType::class, JsonSchema::fromArray([ + 'enum' => [1, 2, 3], + ])); + + $this->assertInstanceOf(NumberType::class, JsonSchema::fromArray([ + 'enum' => [1, 2.5, 3], + ])); + + $this->assertInstanceOf(BooleanType::class, JsonSchema::fromArray([ + 'enum' => [true, false], + ])); + } + + public function test_it_applies_enum_and_default(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'string', + 'enum' => ['draft', 'published'], + 'default' => 'draft', + ]); + + $this->assertEquals([ + 'type' => 'string', + 'default' => 'draft', + 'enum' => ['draft', 'published'], + ], $type->toArray()); + } + + public function test_it_ignores_unknown_keywords(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'string', + 'minLength' => 1, + '$schema' => 'https://json-schema.org/draft/2020-12/schema', + '$comment' => 'ignore me', + 'readOnly' => true, + 'contentEncoding' => 'base64', + ]); + + $this->assertEquals([ + 'type' => 'string', + 'minLength' => 1, + ], $type->toArray()); + } + + public function test_it_throws_when_the_type_cannot_be_determined(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to determine the JSON Schema type for the given schema.'); + + JsonSchema::fromArray([ + 'title' => 'Mystery', + ]); + } + + public function test_it_detects_a_circular_ref_instead_of_recursing(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Circular JSON Schema $ref [#/$defs/node] detected.'); + + JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'children' => ['type' => 'array', 'items' => ['$ref' => '#/$defs/node']], + ], + '$defs' => [ + 'node' => [ + 'type' => 'object', + 'properties' => [ + 'children' => ['type' => 'array', 'items' => ['$ref' => '#/$defs/node']], + ], + ], + ], + ]); + } + + public function test_it_resolves_the_same_ref_used_in_sibling_positions(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'home' => ['$ref' => '#/$defs/address'], + 'work' => ['$ref' => '#/$defs/address'], + ], + '$defs' => [ + 'address' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]], + ], + ]); + + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'home' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]], + 'work' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]], + ], + ], $type->toArray()); + } + + public function test_it_throws_for_a_multi_type_union(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to represent a multi-type JSON Schema union [string, integer].'); + + JsonSchema::fromArray([ + 'type' => ['string', 'integer'], + ]); + } + + public function test_it_throws_for_a_boolean_property_schema(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to represent the schema for property [meta]; boolean schemas are not supported.'); + + JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'meta' => true, + ], + ]); + } + + public function test_it_throws_for_a_non_numeric_numeric_constraint(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The JSON Schema [minimum] constraint must be a number.'); + + JsonSchema::fromArray([ + 'type' => 'number', + 'minimum' => 'oops', + ]); + } + + public function test_it_throws_for_a_non_integer_integer_constraint(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The JSON Schema integer constraint [1.9] must be an integer.'); + + JsonSchema::fromArray([ + 'type' => 'integer', + 'minimum' => 1.9, + ]); + } + + public function test_it_throws_for_tuple_items(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Tuple and boolean JSON Schema "items" are not supported.'); + + JsonSchema::fromArray([ + 'type' => 'array', + 'items' => [ + ['type' => 'string'], + ['type' => 'integer'], + ], + ]); + } + + public function test_it_throws_when_a_union_branch_conflicts_with_sibling_keys(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Conflicting [type] between a "anyOf" branch and its sibling keys.'); + + JsonSchema::fromArray([ + 'type' => 'integer', + 'anyOf' => [ + ['type' => 'string', 'minLength' => 3], + ['type' => 'null'], + ], + ]); + } + + public function test_it_throws_for_an_unsupported_union(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only a nullable "anyOf" (a single schema plus a "null" branch) is supported.'); + + JsonSchema::fromArray([ + 'anyOf' => [ + ['type' => 'string'], + ['type' => 'integer'], + ], + ]); + } + + public function test_it_throws_for_a_null_default(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A null JSON Schema [default] is not supported.'); + + JsonSchema::fromArray([ + 'type' => 'string', + 'default' => null, + ]); + } + + public function test_it_resolves_the_root_ref_pointer(): void + { + // "#" resolves to the root, so a self-reference is detected as circular... + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Circular JSON Schema $ref [#] detected.'); + + JsonSchema::fromArray(['$ref' => '#']); + } +} diff --git a/tests/JsonSchema/ObjectTypeTest.php b/tests/JsonSchema/ObjectTypeTest.php index 362d20f32f2d..1551d858b3d7 100644 --- a/tests/JsonSchema/ObjectTypeTest.php +++ b/tests/JsonSchema/ObjectTypeTest.php @@ -78,6 +78,20 @@ public function test_it_may_be_initialized_with_a_closure_but_may_have_propertie ], $type->toArray()); } + public function test_numeric_string_property_names_remain_strings_in_required_array(): void + { + $type = JsonSchema::object([ + '1' => JsonSchema::string()->required(), + '4' => JsonSchema::string()->required(), + ]); + + $array = $type->toArray(); + + $this->assertSame(['1', '4'], $array['required']); + $this->assertIsString($array['required'][0]); + $this->assertIsString($array['required'][1]); + } + public function test_it_may_disable_additional_properties(): void { $type = JsonSchema::object()->default(['age' => 1])->withoutAdditionalProperties(); From d2d6a81e0a231e14e15f0b26e54489564bf2e4f8 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 4 Jun 2026 18:24:26 +0100 Subject: [PATCH 529/596] Update DebounceFor.php (#60388) Update DebounceFor.php Update DebounceFor.php --- src/Illuminate/Queue/Attributes/DebounceFor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Queue/Attributes/DebounceFor.php b/src/Illuminate/Queue/Attributes/DebounceFor.php index dbb04f816b08..3ec9f46818c4 100644 --- a/src/Illuminate/Queue/Attributes/DebounceFor.php +++ b/src/Illuminate/Queue/Attributes/DebounceFor.php @@ -9,6 +9,9 @@ class DebounceFor { /** * Create a new attribute instance. + * + * @param int $debounceFor Seconds to debounce the job for. + * @param int|null $maxWait The maximum number of seconds the job can be deferred before it is forced to run. */ public function __construct(public int $debounceFor, public ?int $maxWait = null) { From 8834caf9cd96302030405cd728f267853a8b60eb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Jun 2026 13:45:21 -0500 Subject: [PATCH 530/596] port fix --- src/Illuminate/Foundation/Bootstrap/HandleExceptions.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index a5588cf0694e..3f79291061c0 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -92,6 +92,10 @@ public function handleDeprecationError($message, $file, $line, $level = E_DEPREC return; } + if (! static::$app->bound('config')) { + return; + } + try { $logger = static::$app->make(LogManager::class); } catch (Exception) { From e60b1c817a9ef7da319e4007de6cfda5301a58c0 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:46:35 +0000 Subject: [PATCH 531/596] Update version to v13.14.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index ca983e8296be..9717abe1abb0 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 = '13.13.0'; + const VERSION = '13.14.0'; /** * The base path for the Laravel installation. From 7ed0854743ecfcf2969af84b7554aab736749332 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:48:22 +0000 Subject: [PATCH 532/596] Update CHANGELOG --- CHANGELOG.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be342260e9b1..9f0b89f8a218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.13.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.14.0...13.x) + +## [v13.14.0](https://github.com/laravel/framework/compare/v13.13.0...v13.14.0) - 2026-06-04 + +* [13.x] Register the lazy refresh hook on all connections by [@tontonsb](https://github.com/tontonsb) in https://github.com/laravel/framework/pull/60359 +* [13.x] Cache falsy JSON payloads in HTTP client responses by [@Button99](https://github.com/Button99) in https://github.com/laravel/framework/pull/60357 +* GitHub Actions hardening by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/60363 +* Bump the github-actions group with 4 updates by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/60364 +* [13.x] Fix `Request::createFromBase()` compatibility with Symfony 8.1 by [@irabbi360](https://github.com/irabbi360) in https://github.com/laravel/framework/pull/60354 +* [13.x] Set default cooldown for GitHub Actions updates by [@JurianArie](https://github.com/JurianArie) in https://github.com/laravel/framework/pull/60368 +* [13.x] Fix `Message::embed` data attachment handling by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/60361 +* [13.x] Namespace the cloud logging formatter by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60362 +* Grant contents: read to pull requests workflow by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/60370 +* [13.x] Respect child queue properties over inherited attributes by [@mattiasgeniar](https://github.com/mattiasgeniar) in https://github.com/laravel/framework/pull/60369 +* Grant contents: read and pull-requests: write to issues workflow by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/60371 +* Use least-privilege permissions for pull requests and issues workflows by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/60372 +* Pin pull requests and issues workflows to latest laravel/.github by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/60373 +* Enable Dependabot auto-merge by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/60383 +* [13.x] Foundation\Cloud\Events: remove an unused import and fix docblock by [@oliverquynh](https://github.com/oliverquynh) in https://github.com/laravel/framework/pull/60378 +* [13.x] Fix StartSession docblock by [@oliverquynh](https://github.com/oliverquynh) in https://github.com/laravel/framework/pull/60375 +* [13.x] Add JSON Schema array deserializer by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/framework/pull/60384 +* [13.x] Add queue to InspectedJob by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60374 +* [13.x] Fix treatment of null headers by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/60386 +* [13.x] Add units to DebounceFor by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60388 ## [v13.13.0](https://github.com/laravel/framework/compare/v13.12.0...v13.13.0) - 2026-06-02 From 36ca50e41eeaf3766b4cc388b04ab3032c803cab Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 5 Jun 2026 00:52:44 +0200 Subject: [PATCH 533/596] # [13.x] Fix validation bypass in date_equals rule due to loose comparison (#60393) * Fix loose comparison bug in date_equals validation rule * Trigger CI --- src/Illuminate/Validation/Concerns/ValidatesAttributes.php | 2 +- tests/Validation/ValidationValidatorTest.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 28efc707ce17..a655206ba691 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -2846,7 +2846,7 @@ protected function compare($first, $second, $operator) '>' => $first > $second, '<=' => $first <= $second, '>=' => $first >= $second, - '=' => $first == $second, + '=' => ($first === $second) || ($first == $second && ! is_null($first) && ! is_null($second)), default => throw new InvalidArgumentException, }; } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 805aea4394db..ff1cf8d937c4 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -6613,6 +6613,12 @@ public function testDateEquals() $v = new Validator($trans, ['x' => '17:44'], ['x' => 'date_format:H:i|date_equals:17:45']); $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['x' => 'invalid-date'], ['x' => 'date_equals:1970-01-01 00:00:00']); + $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['x' => '1970-01-01 00:00:00'], ['x' => 'date_equals:invalid-date']); + $this->assertTrue($v->fails()); } public function testDateEqualsRespectsCarbonTestNowWhenParameterIsRelative() From 99e5cb42efb11d630d814d7e11c20efd9e9d7054 Mon Sep 17 00:00:00 2001 From: Yoeri Boven Date: Fri, 5 Jun 2026 00:53:30 +0200 Subject: [PATCH 534/596] [13.x] Add Macroable to InvokedProcess (#60392) * Add getProcess method to InvokedProcess class * Remove getProcess method from InvokedProcess class Removed the getProcess method and its documentation. * Remove unnecessary blank line in InvokedProcess.php --- src/Illuminate/Process/InvokedProcess.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Process/InvokedProcess.php b/src/Illuminate/Process/InvokedProcess.php index 0316cb995998..d51a873f2258 100644 --- a/src/Illuminate/Process/InvokedProcess.php +++ b/src/Illuminate/Process/InvokedProcess.php @@ -4,11 +4,14 @@ use Illuminate\Contracts\Process\InvokedProcess as InvokedProcessContract; use Illuminate\Process\Exceptions\ProcessTimedOutException; +use Illuminate\Support\Traits\Macroable; use Symfony\Component\Process\Exception\ProcessTimedOutException as SymfonyTimeoutException; use Symfony\Component\Process\Process; class InvokedProcess implements InvokedProcessContract { + use Macroable; + /** * The underlying process instance. * From cf7e5070ed959fede9ccd106e30483a4f29bdac4 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 5 Jun 2026 01:04:18 +0200 Subject: [PATCH 535/596] [13.x] Restrict allowed classes in routing unserialization (#60391) * Restrict allowed classes in unserialize within routing to mitigate PHP Object Injection * Use closure variable in RoutingRouteTest to satisfy StyleCI * Remove extra newline at EOF to satisfy StyleCI --- src/Illuminate/Routing/Route.php | 16 +++++++- .../Routing/RouteSignatureParameters.php | 8 +++- tests/Routing/RoutingRouteTest.php | 37 +++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index df460dc03545..13fd57b1a164 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -241,7 +241,13 @@ protected function runCallable() $callable = $this->action['uses']; if ($this->isSerializedClosure()) { - $callable = unserialize($this->action['uses'])->getClosure(); + $callable = unserialize($this->action['uses'], ['allowed_classes' => [ + SerializableClosure::class, + \Laravel\SerializableClosure\UnsignedSerializableClosure::class, + \Laravel\SerializableClosure\Serializers\Native::class, + \Laravel\SerializableClosure\Serializers\Signed::class, + \Laravel\SerializableClosure\Support\SelfReference::class, + ]])->getClosure(); } return $this->container[CallableDispatcher::class]->dispatch($this, $callable); @@ -1029,7 +1035,13 @@ public function getMissing() Str::startsWith($missing, [ 'O:47:"Laravel\\SerializableClosure\\SerializableClosure', 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure', - ]) ? unserialize($missing) : $missing; + ]) ? unserialize($missing, ['allowed_classes' => [ + SerializableClosure::class, + \Laravel\SerializableClosure\UnsignedSerializableClosure::class, + \Laravel\SerializableClosure\Serializers\Native::class, + \Laravel\SerializableClosure\Serializers\Signed::class, + \Laravel\SerializableClosure\Support\SelfReference::class, + ]]) : $missing; } /** diff --git a/src/Illuminate/Routing/RouteSignatureParameters.php b/src/Illuminate/Routing/RouteSignatureParameters.php index 872709758314..9fb54b12913b 100644 --- a/src/Illuminate/Routing/RouteSignatureParameters.php +++ b/src/Illuminate/Routing/RouteSignatureParameters.php @@ -19,7 +19,13 @@ class RouteSignatureParameters public static function fromAction(array $action, $conditions = []) { $callback = RouteAction::containsSerializedClosure($action) - ? unserialize($action['uses'])->getClosure() + ? unserialize($action['uses'], ['allowed_classes' => [ + \Laravel\SerializableClosure\SerializableClosure::class, + \Laravel\SerializableClosure\UnsignedSerializableClosure::class, + \Laravel\SerializableClosure\Serializers\Native::class, + \Laravel\SerializableClosure\Serializers\Signed::class, + \Laravel\SerializableClosure\Support\SelfReference::class, + ]])->getClosure() : $action['uses']; $parameters = is_string($callback) diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index b653ce430420..8c2bfb8b8d58 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -2277,6 +2277,33 @@ protected function getRouter($container = null) return $router; } + + public function testRouteDeserializationAllowedClasses() + { + $badObject = new RouteTestInsecureDeserializationStub; + $closureWithUse = function () use ($badObject) { + return $badObject; + }; + + $serializedClosure = serialize(\Laravel\SerializableClosure\SerializableClosure::unsigned($closureWithUse)); + + RouteTestInsecureDeserializationStub::$instantiated = false; + unserialize($serializedClosure); + $this->assertTrue(RouteTestInsecureDeserializationStub::$instantiated); + $route = new Route(['GET'], 'foo', [ + 'uses' => $serializedClosure, + ]); + + RouteTestInsecureDeserializationStub::$instantiated = false; + + try { + $route->run(); + } catch (\Throwable $e) { + // + } + + $this->assertFalse(RouteTestInsecureDeserializationStub::$instantiated); + } } class RouteTestControllerStub extends Controller @@ -2708,3 +2735,13 @@ public function onTenant(RoutingTestTenant $tenant): void $this->tenant = $tenant; } } + +class RouteTestInsecureDeserializationStub +{ + public static $instantiated = false; + + public function __wakeup() + { + self::$instantiated = true; + } +} From 8f6c56bd118a6248950ca97f88236c6bfe089a7b Mon Sep 17 00:00:00 2001 From: Amirhf Date: Fri, 5 Jun 2026 02:34:49 +0330 Subject: [PATCH 536/596] [13.x] Extract flexible cache created-key prefix into a named constant (#60390) --- src/Illuminate/Cache/DatabaseStore.php | 6 +++--- src/Illuminate/Cache/FileStore.php | 2 +- src/Illuminate/Cache/Repository.php | 17 ++++++++++++----- src/Illuminate/Cache/StorageStore.php | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 2222d465ce94..85cb0cf1de16 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -411,7 +411,7 @@ protected function forgetMany(array $keys) { $this->table()->whereIn('key', (new Collection($keys))->flatMap(fn ($key) => [ $this->prefix.$key, - "{$this->prefix}illuminate:cache:flexible:created:{$key}", + $this->prefix.Repository::FLEXIBLE_CREATED_KEY_PREFIX.$key, ])->all())->delete(); return true; @@ -429,10 +429,10 @@ protected function forgetManyIfExpired(array $keys, bool $prefixed = false) $this->table() ->whereIn('key', (new Collection($keys))->flatMap(fn ($key) => $prefixed ? [ $key, - $this->prefix.'illuminate:cache:flexible:created:'.Str::chopStart($key, $this->prefix), + $this->prefix.Repository::FLEXIBLE_CREATED_KEY_PREFIX.Str::chopStart($key, $this->prefix), ] : [ "{$this->prefix}{$key}", - "{$this->prefix}illuminate:cache:flexible:created:{$key}", + $this->prefix.Repository::FLEXIBLE_CREATED_KEY_PREFIX.$key, ])->all()) ->where('expiration', '<=', $this->getTime()) ->delete(); diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index 4ac753de0cb0..6c9336b290f7 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -277,7 +277,7 @@ public function forget($key) { if ($this->files->exists($file = $this->path($key))) { return tap($this->files->delete($file), function ($forgotten) use ($key) { - if ($forgotten && $this->files->exists($file = $this->path("illuminate:cache:flexible:created:{$key}"))) { + if ($forgotten && $this->files->exists($file = $this->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.$key))) { $this->files->delete($file); } }); diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 1d3458a3f360..19b351c86f08 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -47,6 +47,13 @@ class Repository implements ArrayAccess, CacheContract __call as macroCall; } + /** + * The cache key prefix used to track when a flexible cache value was last refreshed. + * + * @var string + */ + const FLEXIBLE_CREATED_KEY_PREFIX = 'illuminate:cache:flexible:created:'; + /** * The cache store implementation. * @@ -623,13 +630,13 @@ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = fal [ $key => $value, - "illuminate:cache:flexible:created:{$key}" => $created, - ] = $this->many([$key, "illuminate:cache:flexible:created:{$key}"]); + self::FLEXIBLE_CREATED_KEY_PREFIX.$key => $created, + ] = $this->many([$key, self::FLEXIBLE_CREATED_KEY_PREFIX.$key]); if (in_array(null, [$value, $created], true)) { return tap(value($callback), fn ($value) => $this->putMany([ $key => $value, - "illuminate:cache:flexible:created:{$key}" => Carbon::now()->getTimestamp(), + self::FLEXIBLE_CREATED_KEY_PREFIX.$key => Carbon::now()->getTimestamp(), ], $ttl[1])); } @@ -643,13 +650,13 @@ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = fal $lock['seconds'] ?? 0, $lock['owner'] ?? null, )->get(function () use ($key, $callback, $created, $ttl) { - if ($created !== $this->get("illuminate:cache:flexible:created:{$key}")) { + if ($created !== $this->get(self::FLEXIBLE_CREATED_KEY_PREFIX.$key)) { return; } $this->putMany([ $key => value($callback), - "illuminate:cache:flexible:created:{$key}" => Carbon::now()->getTimestamp(), + self::FLEXIBLE_CREATED_KEY_PREFIX.$key => Carbon::now()->getTimestamp(), ], $ttl[1]); }); }; diff --git a/src/Illuminate/Cache/StorageStore.php b/src/Illuminate/Cache/StorageStore.php index 956121d55985..194cb5a557f5 100644 --- a/src/Illuminate/Cache/StorageStore.php +++ b/src/Illuminate/Cache/StorageStore.php @@ -167,7 +167,7 @@ public function forget($key) $forgotten = $this->disk->delete($this->path($key)); if ($forgotten) { - $this->disk->delete($this->path("illuminate:cache:flexible:created:{$key}")); + $this->disk->delete($this->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.$key)); } return $forgotten; From 9370033841457750ca60239f553b6ce0f2b175b0 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Fri, 5 Jun 2026 02:36:08 +0330 Subject: [PATCH 537/596] Add missing type hints to WorkerIdle and listenForSignals (#60389) --- src/Illuminate/Queue/Events/WorkerIdle.php | 8 +++++--- src/Illuminate/Queue/Worker.php | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Queue/Events/WorkerIdle.php b/src/Illuminate/Queue/Events/WorkerIdle.php index 7d244127d0ed..29a64de3290e 100644 --- a/src/Illuminate/Queue/Events/WorkerIdle.php +++ b/src/Illuminate/Queue/Events/WorkerIdle.php @@ -2,6 +2,8 @@ namespace Illuminate\Queue\Events; +use Illuminate\Queue\WorkerOptions; + class WorkerIdle { /** @@ -12,9 +14,9 @@ class WorkerIdle * @param \Illuminate\Queue\WorkerOptions $workerOptions */ public function __construct( - public $connectionName, - public $queue, - public $workerOptions, + public string $connectionName, + public string $queue, + public WorkerOptions $workerOptions, ) { } } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 0ff2cb3b1ff5..8051e2d438b6 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -855,9 +855,10 @@ protected function getTimestampOfLastQueueRestart() * * @param string|null $connectionName * @param string|null $queue + * @param \Illuminate\Queue\WorkerOptions|null $options * @return void */ - protected function listenForSignals($connectionName = null, $queue = null, $options = null) + protected function listenForSignals($connectionName = null, $queue = null, ?WorkerOptions $options = null) { pcntl_async_signals(true); From 58f015a8fc4114f444e64ad23c850e41158e0bc6 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Fri, 5 Jun 2026 15:42:14 +0100 Subject: [PATCH 538/596] [13.x] Allow enums in Queue::route (#60402) * [13.x] Allow enums in Queue::route * can be one test --- src/Illuminate/Queue/QueueManager.php | 4 ++-- src/Illuminate/Queue/QueueRoutes.php | 10 +++++++--- tests/Queue/QueueRoutesTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Queue/QueueManager.php b/src/Illuminate/Queue/QueueManager.php index c7af54eaa3d6..29a683715c0c 100755 --- a/src/Illuminate/Queue/QueueManager.php +++ b/src/Illuminate/Queue/QueueManager.php @@ -129,8 +129,8 @@ public function stopping($callback) * Set the queue route for the given class. * * @param array|class-string $class - * @param string|null $queue - * @param string|null $connection + * @param \UnitEnum|string|null $queue + * @param \UnitEnum|string|null $connection * @return void */ public function route(array|string $class, $queue = null, $connection = null) diff --git a/src/Illuminate/Queue/QueueRoutes.php b/src/Illuminate/Queue/QueueRoutes.php index 8c73fed0f8e5..6360072c0b35 100644 --- a/src/Illuminate/Queue/QueueRoutes.php +++ b/src/Illuminate/Queue/QueueRoutes.php @@ -2,6 +2,8 @@ namespace Illuminate\Queue; +use function Illuminate\Support\enum_value; + class QueueRoutes { /** @@ -81,8 +83,8 @@ class_uses_recursive($queueable) * Register the queue route for the given class. * * @param array|class-string $class - * @param string|null $queue - * @param string|null $connection + * @param \UnitEnum|string|null $queue + * @param \UnitEnum|string|null $connection * @return void */ public function set(array|string $class, $queue = null, $connection = null) @@ -90,7 +92,9 @@ public function set(array|string $class, $queue = null, $connection = null) $routes = is_array($class) ? $class : [$class => [$connection, $queue]]; foreach ($routes as $from => $to) { - $this->routes[$from] = $to; + $this->routes[$from] = is_array($to) + ? array_map(enum_value(...), $to) + : enum_value($to); } } diff --git a/tests/Queue/QueueRoutesTest.php b/tests/Queue/QueueRoutesTest.php index 30aeda1a8fb4..86bbcf219ef7 100644 --- a/tests/Queue/QueueRoutesTest.php +++ b/tests/Queue/QueueRoutesTest.php @@ -78,6 +78,31 @@ public function testStringRouteDefaultsToQueueNotConnection() $this->assertSame('notifications', $defaults->getQueue(new FinanceNotification)); $this->assertNull($defaults->getConnection(new FinanceNotification)); } + + public function testEnumsAreResolved() + { + $defaults = new QueueRoutes(); + + $defaults->set(SomeJob::class, QueueName::payments, ConnectionName::redis); + + $this->assertSame('payments', $defaults->getQueue(new SomeJob)); + $this->assertSame('redis', $defaults->getConnection(new SomeJob)); + + $defaults->set([SomeJob::class => [ConnectionName::redis, QueueName::payments]]); + + $this->assertSame('payments', $defaults->getQueue(new SomeJob)); + $this->assertSame('redis', $defaults->getConnection(new SomeJob)); + } +} + +enum QueueName: string +{ + case payments = 'payments'; +} + +enum ConnectionName: string +{ + case redis = 'redis'; } trait CustomTrait From dcb26c89eeb1e8b597730770f999db692373603b Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:42:50 +0000 Subject: [PATCH 539/596] Update facade docblocks --- src/Illuminate/Support/Facades/Queue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Queue.php b/src/Illuminate/Support/Facades/Queue.php index 5fa7f3d9fc03..d11d16d044b5 100755 --- a/src/Illuminate/Support/Facades/Queue.php +++ b/src/Illuminate/Support/Facades/Queue.php @@ -13,7 +13,7 @@ * @method static void failing(mixed $callback) * @method static void starting(mixed $callback) * @method static void stopping(mixed $callback) - * @method static void route(array|string $class, string|null $queue = null, string|null $connection = null) + * @method static void route(array|string $class, \UnitEnum|string|null $queue = null, \UnitEnum|string|null $connection = null) * @method static bool connected(\UnitEnum|string|null $name = null) * @method static \Illuminate\Contracts\Queue\Queue connection(\UnitEnum|string|null $name = null) * @method static void pause(string $connection, string $queue) From 8c231d07e8d8e640538ae28dd2276d3866abcdb9 Mon Sep 17 00:00:00 2001 From: Daniel Sandnes Date: Fri, 5 Jun 2026 16:48:46 +0200 Subject: [PATCH 540/596] Refresh unchanged compiled Blade views (#60401) --- .../View/Compilers/BladeCompiler.php | 8 ++++ tests/View/ViewBladeCompilerTest.php | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 7ea1224a6d82..ba6b6b0de09f 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -205,6 +205,14 @@ public function compile($path = null) if ($compiledHash !== hash('xxh128', $contents)) { $this->files->replace($compiledPath, $contents); + + return; + } + + $lastModified = $this->files->lastModified($this->getPath()); + + if ($lastModified >= $this->files->lastModified($compiledPath)) { + touch($compiledPath, $lastModified + 1); } } } diff --git a/tests/View/ViewBladeCompilerTest.php b/tests/View/ViewBladeCompilerTest.php index 75cc5f8a3f13..76b4017124e8 100644 --- a/tests/View/ViewBladeCompilerTest.php +++ b/tests/View/ViewBladeCompilerTest.php @@ -104,9 +104,50 @@ public function testCompileKeepsCacheIfUnchanged() $files->shouldReceive('makeDirectory')->once()->with(__DIR__, 0777, true, true); $files->shouldReceive('exists')->once()->with($compiledPath)->andReturn(true); $files->shouldReceive('hash')->once()->with($compiledPath, 'xxh128')->andReturn(hash('xxh128', 'Hello World')); + $files->shouldReceive('lastModified')->once()->with('foo')->andReturn(100); + $files->shouldReceive('lastModified')->once()->with($compiledPath)->andReturn(200); $compiler->compile('foo'); } + public function testCompileRefreshesCacheTimestampIfUnchangedButExpired() + { + $files = new Filesystem; + $directory = sys_get_temp_dir().'/laravel-blade-compiler-test-'.uniqid(); + $source = $directory.'/source.blade.php'; + $cache = $directory.'/cache'; + + try { + $files->ensureDirectoryExists($cache); + $files->put($source, 'Hello World'); + + $compiler = new BladeCompiler($files, $cache); + $compiler->compile($source); + + $compiled = $compiler->getCompiledPath($source); + + $compiledModified = time(); + $sourceModified = $compiledModified + 10; + + touch($source, $sourceModified); + touch($compiled, $compiledModified); + + clearstatcache(true, $source); + clearstatcache(true, $compiled); + + $this->assertTrue($compiler->isExpired($source)); + + $compiler->compile($source); + + clearstatcache(true, $source); + clearstatcache(true, $compiled); + + $this->assertGreaterThan($files->lastModified($source), $files->lastModified($compiled)); + $this->assertFalse($compiler->isExpired($source)); + } finally { + $files->deleteDirectory($directory); + } + } + public function testCompileCompilesAndGetThePath() { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); From cf6681c426d73435046c69ce7cf04c31ae17e88b Mon Sep 17 00:00:00 2001 From: Allen McCabe Date: Fri, 5 Jun 2026 08:54:13 -0700 Subject: [PATCH 541/596] [12.x] Skip pg_collation lookup in compileColumns() on PostgreSQL servers before 9.1 (#60400) The pg_catalog.pg_collation catalog was introduced in PostgreSQL 9.1. Servers reporting an older version (e.g. AWS Redshift, which reports 8.0.2) fail with "relation pg_catalog.pg_collation does not exist" when Schema::getColumns() is called. Version-gate the collation subquery the same way the attgenerated column already is for servers before 12.0. Co-authored-by: Claude Opus 4.8 (1M context) --- .../Database/Schema/Grammars/PostgresGrammar.php | 8 ++++++-- .../DatabasePostgresSchemaGrammarTest.php | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index f29a8457bdc7..11190cbd2b81 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -156,12 +156,16 @@ protected function compileSchemaWhereClause($schema, $column) */ public function compileColumns($schema, $table) { + $serverVersion = $this->connection->getServerVersion(); + return sprintf( 'select a.attname as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, ' - .'(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, ' + .(version_compare($serverVersion, '9.1', '<') + ? 'null as collation, ' + : '(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, ') .'not a.attnotnull as nullable, ' .'(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, ' - .(version_compare($this->connection->getServerVersion(), '12.0', '<') ? "'' as generated, " : 'a.attgenerated as generated, ') + .(version_compare($serverVersion, '12.0', '<') ? "'' as generated, " : 'a.attgenerated as generated, ') .'col_description(c.oid, a.attnum) as comment ' .'from pg_attribute a, pg_class c, pg_type t, pg_namespace n ' .'where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace ' diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 71bbddedd034..316096882ae2 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -1348,6 +1348,22 @@ public function testCompileColumns() $statement = $connection->getSchemaGrammar()->compileColumns('public', 'table'); $this->assertStringContainsString("where c.relname = 'table' and n.nspname = 'public'", $statement); + $this->assertStringContainsString('pg_catalog.pg_collation', $statement); + $this->assertStringContainsString('a.attgenerated as generated', $statement); + } + + public function testCompileColumnsOnLegacyServer() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getServerVersion')->once()->andReturn('8.0.2'); + + $statement = $connection->getSchemaGrammar()->compileColumns('public', 'table'); + + $this->assertStringContainsString("where c.relname = 'table' and n.nspname = 'public'", $statement); + $this->assertStringContainsString('null as collation', $statement); + $this->assertStringContainsString("'' as generated", $statement); + $this->assertStringNotContainsString('pg_catalog.pg_collation', $statement); + $this->assertStringNotContainsString('a.attgenerated', $statement); } protected function getConnection( From 1f6a84bceffaf4f6dd0a19af5d88829a7a577285 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 7 Jun 2026 16:42:16 +0100 Subject: [PATCH 542/596] 13.x-add-unit-docblock-to-attrs (#60431) --- src/Illuminate/Queue/Attributes/Backoff.php | 2 +- src/Illuminate/Queue/Attributes/Delay.php | 2 +- src/Illuminate/Queue/Attributes/Timeout.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Queue/Attributes/Backoff.php b/src/Illuminate/Queue/Attributes/Backoff.php index baabce4fe3e3..d9f15a723317 100644 --- a/src/Illuminate/Queue/Attributes/Backoff.php +++ b/src/Illuminate/Queue/Attributes/Backoff.php @@ -17,7 +17,7 @@ class Backoff /** * Create a new attribute instance. * - * @param array|int ...$backoff + * @param array|int ...$backoff Seconds to wait before retrying the job. */ public function __construct(array|int ...$backoff) { diff --git a/src/Illuminate/Queue/Attributes/Delay.php b/src/Illuminate/Queue/Attributes/Delay.php index 6aa7dd4b12e7..ec9b6657bdba 100644 --- a/src/Illuminate/Queue/Attributes/Delay.php +++ b/src/Illuminate/Queue/Attributes/Delay.php @@ -10,7 +10,7 @@ class Delay /** * Create a new attribute instance. * - * @param int $delay + * @param int $delay Seconds to delay the job for. */ public function __construct(public int $delay) { diff --git a/src/Illuminate/Queue/Attributes/Timeout.php b/src/Illuminate/Queue/Attributes/Timeout.php index 2d01907b35c4..d2ada2cde276 100644 --- a/src/Illuminate/Queue/Attributes/Timeout.php +++ b/src/Illuminate/Queue/Attributes/Timeout.php @@ -10,7 +10,7 @@ class Timeout /** * Create a new attribute instance. * - * @param int $timeout + * @param int $timeout Seconds before the job is considered timed out. */ public function __construct(public int $timeout) { From 0b133092aa96f5583a1ff01d2fbd2af0b1b02c1f Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Sun, 7 Jun 2026 16:42:33 +0100 Subject: [PATCH 543/596] 13.x-add-prohibited-to-more (#60430) --- src/Illuminate/Cache/Console/ClearCommand.php | 7 +++++++ src/Illuminate/Queue/Console/FlushFailedCommand.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/Illuminate/Cache/Console/ClearCommand.php b/src/Illuminate/Cache/Console/ClearCommand.php index 6a9d8103dafd..496c43878281 100755 --- a/src/Illuminate/Cache/Console/ClearCommand.php +++ b/src/Illuminate/Cache/Console/ClearCommand.php @@ -5,6 +5,7 @@ use BadMethodCallException; use Illuminate\Cache\CacheManager; use Illuminate\Console\Command; +use Illuminate\Console\Prohibitable; use Illuminate\Filesystem\Filesystem; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -13,6 +14,8 @@ #[AsCommand(name: 'cache:clear')] class ClearCommand extends Command { + use Prohibitable; + /** * The console command name. * @@ -62,6 +65,10 @@ public function __construct(CacheManager $cache, Filesystem $files) */ public function handle() { + if ($this->isProhibited()) { + return self::FAILURE; + } + if ($this->option('locks')) { return $this->clearLocks(); } diff --git a/src/Illuminate/Queue/Console/FlushFailedCommand.php b/src/Illuminate/Queue/Console/FlushFailedCommand.php index 80cc2006d968..7f24ea11c626 100644 --- a/src/Illuminate/Queue/Console/FlushFailedCommand.php +++ b/src/Illuminate/Queue/Console/FlushFailedCommand.php @@ -3,11 +3,14 @@ namespace Illuminate\Queue\Console; use Illuminate\Console\Command; +use Illuminate\Console\Prohibitable; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'queue:flush')] class FlushFailedCommand extends Command { + use Prohibitable; + /** * The console command name. * @@ -29,6 +32,10 @@ class FlushFailedCommand extends Command */ public function handle() { + if ($this->isProhibited()) { + return; + } + $this->laravel['queue.failer']->flush($this->option('hours')); if ($this->option('hours')) { From 998524a0f8e4661bfdb2bc31a7741636aebbbba9 Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Sun, 7 Jun 2026 19:16:14 +0330 Subject: [PATCH 544/596] [13.x] Refactor: use `Repository::FLEXIBLE_CREATED_KEY_PREFIX` in test (#60424) * refactor: use Repository::FLEXIBLE_CREATED_KEY_PREFIX * refactor: test * refactor: test --- tests/Cache/CacheStorageStoreTest.php | 7 ++-- tests/Integration/Cache/RepositoryTest.php | 47 +++++++++++----------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/tests/Cache/CacheStorageStoreTest.php b/tests/Cache/CacheStorageStoreTest.php index 045cb2088141..c9da25c9922e 100644 --- a/tests/Cache/CacheStorageStoreTest.php +++ b/tests/Cache/CacheStorageStoreTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Cache; +use Illuminate\Cache\Repository; use Illuminate\Cache\StorageStore; use Illuminate\Support\Carbon; use Illuminate\Tests\Cache\Fixtures\ArrayFilesystem; @@ -86,16 +87,16 @@ public function testForgetRemovesFlexibleCreatedKeyOnlyWhenParentIsForgotten() $disk = new ArrayFilesystem; $store = new StorageStore($disk, 'cache'); - $store->put('illuminate:cache:flexible:created:foo', true, 60); + $store->put(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo', true, 60); $this->assertFalse($store->forget('foo')); - $this->assertTrue($disk->exists($store->path('illuminate:cache:flexible:created:foo'))); + $this->assertTrue($disk->exists($store->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo'))); $store->put('foo', 'bar', 60); $this->assertTrue($store->forget('foo')); $this->assertFalse($disk->exists($store->path('foo'))); - $this->assertFalse($disk->exists($store->path('illuminate:cache:flexible:created:foo'))); + $this->assertFalse($disk->exists($store->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo'))); } public function testFlushRemovesScopedDirectory() diff --git a/tests/Integration/Cache/RepositoryTest.php b/tests/Integration/Cache/RepositoryTest.php index 2a415c22fe7f..ebc072d6c928 100644 --- a/tests/Integration/Cache/RepositoryTest.php +++ b/tests/Integration/Cache/RepositoryTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Cache; use Illuminate\Cache\Events\KeyWritten; +use Illuminate\Cache\Repository; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; @@ -29,7 +30,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(1, $value); $this->assertCount(0, defer()); $this->assertSame(1, $cache->get('foo')); - $this->assertSame(946684800, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Cache is fresh. The value should be retrieved from the cache and used... $value = $cache->flexible('foo', [10, 20], function () use (&$count) { @@ -38,7 +39,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(1, $value); $this->assertCount(0, defer()); $this->assertSame(1, $cache->get('foo')); - $this->assertSame(946684800, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); Carbon::setTestNow(Carbon::now()->addSeconds(11)); @@ -50,7 +51,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(1, $value); $this->assertCount(1, defer()); $this->assertSame(1, $cache->get('foo')); - $this->assertSame(946684800, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // We will hit it again within the same request. This should not queue // up an additional deferred callback as only one can be registered at @@ -61,14 +62,14 @@ public function testStaleWhileRevalidate(): void $this->assertSame(1, $value); $this->assertCount(1, defer()); $this->assertSame(1, $cache->get('foo')); - $this->assertSame(946684800, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // We will now simulate the end of the request lifecycle by executing the // deferred callback. This should refresh the cache. defer()->invoke(); $this->assertCount(0, defer()); $this->assertSame(2, $cache->get('foo')); // this has been updated! - $this->assertSame(946684811, $cache->get('illuminate:cache:flexible:created:foo')); // this has been updated! + $this->assertSame(946684811, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // this has been updated! // Now the cache is fresh again... $value = $cache->flexible('foo', [10, 20], function () use (&$count) { @@ -77,7 +78,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(2, $value); $this->assertCount(0, defer()); $this->assertSame(2, $cache->get('foo')); - $this->assertSame(946684811, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684811, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Let's now progress time beyond the stale TTL... Carbon::setTestNow(Carbon::now()->addSeconds(21)); @@ -89,7 +90,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(3, $value); $this->assertCount(0, defer()); $this->assertSame(3, $cache->get('foo')); - $this->assertSame(946684832, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684832, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Now lets see what happens when another request, job, or command is // also trying to refresh the same key at the same time. Will push past @@ -101,7 +102,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(3, $value); $this->assertCount(1, defer()); $this->assertSame(3, $cache->get('foo')); - $this->assertSame(946684832, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684832, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Now we will execute the deferred callback but we will first acquire // our own lock. This means that the value should not be refreshed by @@ -114,7 +115,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(3, $value); $this->assertCount(1, defer()); $this->assertSame(3, $cache->get('foo')); - $this->assertSame(946684832, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684832, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); $this->assertTrue($lock->release()); // Now we have cleared the lock we will, one last time, confirm that @@ -122,7 +123,7 @@ public function testStaleWhileRevalidate(): void defer()->invoke(); $this->assertCount(0, defer()); $this->assertSame(4, $cache->get('foo')); - $this->assertSame(946684843, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684843, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // The last thing is to check that we don't refresh the cache in the // deferred callback if another thread has already done the work for us. @@ -134,13 +135,13 @@ public function testStaleWhileRevalidate(): void $this->assertSame(4, $value); $this->assertCount(1, defer()); $this->assertSame(4, $cache->get('foo')); - $this->assertSame(946684843, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684843, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // There is now a deferred callback ready to refresh the cache. We will // simulate another thread updating the value. $cache->putMany([ 'foo' => 99, - 'illuminate:cache:flexible:created:foo' => 946684863, + Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo' => 946684863, ]); // then we will run the refresh callback @@ -151,7 +152,7 @@ public function testStaleWhileRevalidate(): void $this->assertSame(99, $value); $this->assertCount(0, defer()); $this->assertSame(99, $cache->get('foo')); - $this->assertSame(946684863, $cache->get('illuminate:cache:flexible:created:foo')); + $this->assertSame(946684863, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); } public function testItHandlesStrayTtlKeyAfterMainKeyIsForgotten() @@ -187,25 +188,25 @@ public function testItImplicitlyClearsTtlKeysFromDatabaseCache() $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); - $this->assertTrue($cache->has('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->forget('count'); $this->assertEmpty($cache->getConnection()->table('cache')->get()); $this->assertTrue($cache->missing('count')); - $this->assertTrue($cache->missing('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); - $this->assertTrue($cache->has('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $this->travel(20)->seconds(); $cache->forgetIfExpired('count'); $this->assertEmpty($cache->getConnection()->table('cache')->get()); $this->assertTrue($cache->missing('count')); - $this->assertTrue($cache->missing('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); } public function testItImplicitlyClearsTtlKeysFromFileDriver() @@ -216,26 +217,26 @@ public function testItImplicitlyClearsTtlKeysFromFileDriver() $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); - $this->assertTrue($cache->has('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->forget('count'); $this->assertFalse($cache->getFilesystem()->exists($cache->path('count'))); - $this->assertFalse($cache->getFilesystem()->exists($cache->path('illuminate:cache:flexible:created:count'))); + $this->assertFalse($cache->getFilesystem()->exists($cache->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count'))); $this->assertTrue($cache->missing('count')); - $this->assertTrue($cache->missing('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); - $this->assertTrue($cache->has('illuminate:cache:flexible:created:count')); + $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $this->travel(20)->seconds(); $this->assertTrue($cache->missing('count')); $this->assertFalse($cache->getFilesystem()->exists($cache->path('count'))); - $this->assertFalse($cache->getFilesystem()->exists($cache->path('illuminate:cache:flexible:created:count'))); - $this->assertTrue($cache->missing('illuminate:cache:flexible:created:count')); + $this->assertFalse($cache->getFilesystem()->exists($cache->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count'))); + $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); } public function testItCanAlwaysDefer() From 20064c858e7b84efad446c706baa2e5c919e1531 Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Sun, 7 Jun 2026 19:16:41 +0330 Subject: [PATCH 545/596] refactor: add JsonException (#60423) --- src/Illuminate/Database/Query/Builder.php | 6 ++++++ src/Illuminate/Testing/TestResponse.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 9fcf09fd4328..236b7b7907d6 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -495,6 +495,8 @@ public function addSelect($column) * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector * @param string|null $as * @return $this + * + * @throws \JsonException */ public function selectVectorDistance($column, $vector, $as = null) { @@ -1240,6 +1242,8 @@ public function whereVectorSimilarTo($column, $vector, $minSimilarity = 0.6, $or * @param float $maxDistance * @param string $boolean * @return $this + * + * @throws \JsonException */ public function whereVectorDistanceLessThan($column, $vector, $maxDistance, $boolean = 'and') { @@ -3035,6 +3039,8 @@ public function oldest($column = 'created_at') * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array $vector * @return $this + * + * @throws \JsonException */ public function orderByVectorDistance($column, $vector) { diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 214f5f45095c..c22b5a9b5de2 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -688,6 +688,8 @@ public function assertStreamedContent($value) * * @param array $value * @return $this + * + * @throws \JsonException */ public function assertStreamedJsonContent($value) { From 451486fe844b5ec4727b7051c2d175de0b569952 Mon Sep 17 00:00:00 2001 From: Denys Finchenko Date: Sun, 7 Jun 2026 17:47:33 +0200 Subject: [PATCH 546/596] Add generics to DatabaseTransactionsManager transaction getters (#60420) --- src/Illuminate/Database/DatabaseTransactionsManager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index 9713c66d82f4..a2b9932e72ee 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -248,7 +248,7 @@ public function afterCommitCallbacksShouldBeExecuted($level) /** * Get all of the pending transactions. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function getPendingTransactions() { @@ -258,7 +258,7 @@ public function getPendingTransactions() /** * Get all of the committed transactions. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function getCommittedTransactions() { From 7b03da7f78456e11a85e28679272210e7c0b288d Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Mon, 8 Jun 2026 00:22:00 +0330 Subject: [PATCH 547/596] fix: add @throws \ReflectionException (#60436) --- src/Illuminate/Foundation/Console/RouteListCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index 2c5f07c2ed24..fa86f4a329c6 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -236,6 +236,8 @@ protected function getMiddleware($route) * * @param \Illuminate\Routing\Route $route * @return string|null + * + * @throws \ReflectionException */ protected function getClosurePath(Route $route) { From 9c5b976b7380df6f92f2963641d57e49e4bffb29 Mon Sep 17 00:00:00 2001 From: Ali Khosrojerdi Date: Mon, 8 Jun 2026 00:22:21 +0330 Subject: [PATCH 548/596] fix: add |null in doc blocks (#60435) --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- src/Illuminate/Database/Eloquent/Concerns/HasUniqueIds.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index bb03c3b98907..075113972995 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1486,7 +1486,7 @@ public static function currentEncrypter() * * @param string $key * @param mixed $value - * @return string + * @return string|null * * @throws \RuntimeException */ diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasUniqueIds.php b/src/Illuminate/Database/Eloquent/Concerns/HasUniqueIds.php index b67343ac8c8c..5b7f43565be9 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasUniqueIds.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasUniqueIds.php @@ -38,7 +38,7 @@ public function setUniqueIds() /** * Generate a new key for the model. * - * @return string + * @return string|null */ public function newUniqueId() { From e31f9bdd630b555e0020ee0467a213cfff6142c4 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 8 Jun 2026 06:04:00 +0000 Subject: [PATCH 549/596] Apply fixes from StyleCI --- src/Illuminate/Foundation/Cloud.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 6b907ebdc6d7..7f9b98b5d5d8 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -8,7 +8,6 @@ use Illuminate\Foundation\Bootstrap\LoadConfiguration; use Illuminate\Foundation\Cloud\Events; use Illuminate\Foundation\Cloud\FailedJobProvider; -use Illuminate\Foundation\Cloud\JsonFormatter; use Illuminate\Foundation\Cloud\QueueConnector; use Illuminate\Queue\Connectors\SqsConnector; use Monolog\Handler\SocketHandler; From 4f91e08bd4f4d38b62274ce42e485e4d0cbe0826 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 8 Jun 2026 14:23:34 +0800 Subject: [PATCH 550/596] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Queue/Worker.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index c90e1bcf1218..8051e2d438b6 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -144,8 +144,7 @@ class Worker * * @var bool */ - public static - = true; + public static $stopOnLostConnection = true; /** * Indicates if the worker should check for the restart signal in the cache. From 278fcd781ea9a82c7a2ef224417018388486b871 Mon Sep 17 00:00:00 2001 From: Clem Blanco Date: Mon, 8 Jun 2026 15:43:48 +0200 Subject: [PATCH 551/596] [13.x] fix model:prune Command options validation typo (#60444) Fix typo when using both `model` and `except` options. --- src/Illuminate/Database/Console/PruneCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Console/PruneCommand.php b/src/Illuminate/Database/Console/PruneCommand.php index 527ab70c6f04..62b7bc86bcce 100644 --- a/src/Illuminate/Database/Console/PruneCommand.php +++ b/src/Illuminate/Database/Console/PruneCommand.php @@ -121,7 +121,7 @@ protected function models() $except = $this->option('except'); if ($models && $except) { - throw new InvalidArgumentException('The --models and --except options cannot be combined.'); + throw new InvalidArgumentException('The --model and --except options cannot be combined.'); } if ($models) { From e57caffa4283d1d516a3c8a857f97bfda7ab193b Mon Sep 17 00:00:00 2001 From: Daniel Sandnes Date: Mon, 8 Jun 2026 15:46:48 +0200 Subject: [PATCH 552/596] Add typed translation accessors (#60443) --- src/Illuminate/Translation/Translator.php | 38 +++++++++++++++++++ .../Translation/TranslationTranslatorTest.php | 38 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index 12f18dd868ef..1641f68ce866 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -189,6 +189,44 @@ public function get($key, array $replace = [], $locale = null, $fallback = true) return $this->makeReplacements($line ?: $key, $replace); } + /** + * Get the specified string translation value. + * + * @throws \InvalidArgumentException + */ + public function string(string $key, array $replace = [], ?string $locale = null, bool $fallback = true): string + { + $value = $this->get($key, $replace, $locale, $fallback); + + if (! is_string($value)) { + throw new InvalidArgumentException( + sprintf('Translation value for key [%s] must be a string, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + + /** + * Get the specified array translation value. + * + * @return array + * + * @throws \InvalidArgumentException + */ + public function array(string $key, array $replace = [], ?string $locale = null, bool $fallback = true): array + { + $value = $this->get($key, $replace, $locale, $fallback); + + if (! is_array($value)) { + throw new InvalidArgumentException( + sprintf('Translation value for key [%s] must be an array, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + /** * Get a translation according to an integer value. * diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php index d965e873aed2..0d1d868b3e0d 100755 --- a/tests/Translation/TranslationTranslatorTest.php +++ b/tests/Translation/TranslationTranslatorTest.php @@ -64,6 +64,44 @@ public function testGetMethodProperlyLoadsAndRetrievesArrayItem() $this->assertSame('foo', $t->get('foo::bar.foo')); } + public function testStringMethodProperlyLoadsAndRetrievesStringItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['baz' => 'breeze :foo']); + $this->assertSame('breeze bar', $t->string('foo::bar.baz', ['foo' => 'bar'], 'en')); + } + + public function testStringMethodThrowsExceptionForArrayItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['baz' => ['breeze']]); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Translation value for key [foo::bar.baz] must be a string, array given.'); + + $t->string('foo::bar.baz', [], 'en'); + } + + public function testArrayMethodProperlyLoadsAndRetrievesArrayItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['baz' => ['breeze :foo']]); + $this->assertSame(['breeze bar'], $t->array('foo::bar.baz', ['foo' => 'bar'], 'en')); + } + + public function testArrayMethodThrowsExceptionForStringItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['baz' => 'breeze']); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Translation value for key [foo::bar.baz] must be an array, string given.'); + + $t->array('foo::bar.baz', [], 'en'); + } + public function testGetMethodForNonExistingReturnsSameKey() { $t = new Translator($this->getLoader(), 'en'); From 9ff66a3ca36f68b85766d9b3613b2ec86b06cdc2 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:47:24 +0000 Subject: [PATCH 553/596] Update facade docblocks --- src/Illuminate/Support/Facades/Lang.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Support/Facades/Lang.php b/src/Illuminate/Support/Facades/Lang.php index 7dc08da5197d..7b8f3ec5dc19 100755 --- a/src/Illuminate/Support/Facades/Lang.php +++ b/src/Illuminate/Support/Facades/Lang.php @@ -6,6 +6,8 @@ * @method static bool hasForLocale(string $key, string|null $locale = null) * @method static bool has(string $key, string|null $locale = null, bool $fallback = true) * @method static string|array get(string $key, array $replace = [], string|null $locale = null, bool $fallback = true) + * @method static string string(string $key, array $replace = [], string|null $locale = null, bool $fallback = true) + * @method static array array(string $key, array $replace = [], string|null $locale = null, bool $fallback = true) * @method static string choice(string $key, \Countable|int|float|array $number, array $replace = [], string|null $locale = null) * @method static void addLines(array $lines, string $locale, string $namespace = '*') * @method static void load(string $namespace, string $group, string $locale) From 1b1d6126660dc7d906f7e74a80a9811ac9acea20 Mon Sep 17 00:00:00 2001 From: Majid Feizi Date: Mon, 8 Jun 2026 17:17:33 +0330 Subject: [PATCH 554/596] fix: guard against null $app in HandleExceptions when Octane resets state (#60439) --- .../Foundation/Bootstrap/HandleExceptions.php | 1 + .../Bootstrap/HandleExceptionsTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index 3f79291061c0..3608d962d305 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -125,6 +125,7 @@ public function handleDeprecationError($message, $file, $line, $level = E_DEPREC protected function shouldIgnoreDeprecationErrors() { return ! class_exists(LogManager::class) + || is_null(static::$app) || ! static::$app->hasBeenBootstrapped() || (static::$app->runningUnitTests() && ! Env::get('LOG_DEPRECATIONS_WHILE_TESTING')); } diff --git a/tests/Foundation/Bootstrap/HandleExceptionsTest.php b/tests/Foundation/Bootstrap/HandleExceptionsTest.php index 9de7ee5103ba..38289dbb747e 100644 --- a/tests/Foundation/Bootstrap/HandleExceptionsTest.php +++ b/tests/Foundation/Bootstrap/HandleExceptionsTest.php @@ -403,6 +403,23 @@ public function testHandlerForgetsPreviousApp() $this->assertNotSame($this->app, $appResolver()); $this->assertSame($newApp, $appResolver()); } + + public function testDeprecationErrorsAreIgnoredWhenAppIsNull() + { + $instance = $this->handleExceptions(); + + HandleExceptions::forgetApp(); + + // Should not throw when static::$app is null (e.g., during Octane request marshaling) + $instance->handleError( + E_USER_DEPRECATED, + 'Directly setting property "request" of "Illuminate\Http\Request" is deprecated', + '/vendor/symfony/http-foundation/Request.php', + 100 + ); + + $this->assertTrue(true); + } } class CustomNullHandler extends NullHandler From 1399c96360d5ceb0815ad46238f3807a9cd1fb42 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Mon, 8 Jun 2026 16:19:43 +0100 Subject: [PATCH 555/596] Update PruneCommandTest.php (#60445) --- tests/Database/PruneCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Database/PruneCommandTest.php b/tests/Database/PruneCommandTest.php index 5707fdea5d62..e2eea8a5f736 100644 --- a/tests/Database/PruneCommandTest.php +++ b/tests/Database/PruneCommandTest.php @@ -42,7 +42,7 @@ protected function setUp(): void public function testPrunableModelAndExceptWithEachOther(): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The --models and --except options cannot be combined.'); + $this->expectExceptionMessage('The --model and --except options cannot be combined.'); $this->artisan([ '--model' => Pruning\Models\PrunableTestModelWithPrunableRecords::class, From f3a7e5018730ddcb117d88fb7e407ec49ec69a96 Mon Sep 17 00:00:00 2001 From: Denys Finchenko Date: Mon, 8 Jun 2026 23:50:23 +0200 Subject: [PATCH 556/596] Add generic to QueueRoutes::all() return type (#60447) --- src/Illuminate/Queue/QueueRoutes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/QueueRoutes.php b/src/Illuminate/Queue/QueueRoutes.php index 6360072c0b35..c98f7e0e3b32 100644 --- a/src/Illuminate/Queue/QueueRoutes.php +++ b/src/Illuminate/Queue/QueueRoutes.php @@ -101,7 +101,7 @@ public function set(array|string $class, $queue = null, $connection = null) /** * Get all registered queue routes. * - * @return array + * @return array */ public function all() { From 85d1ce9f56267fe7954ee32907a18463fd2adac5 Mon Sep 17 00:00:00 2001 From: Sander Visser Date: Tue, 9 Jun 2026 15:10:14 +0200 Subject: [PATCH 557/596] Pass calling parameter to resolve method of ContextualAttribute" (#60457) --- src/Illuminate/Container/BoundMethod.php | 2 +- src/Illuminate/Container/Container.php | 6 +-- .../Routing/ResolvesRouteDependencies.php | 2 +- .../ContextualAttributeBindingTest.php | 41 +++++++++++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Container/BoundMethod.php b/src/Illuminate/Container/BoundMethod.php index 6bedef02da6e..806cfbceb1b0 100644 --- a/src/Illuminate/Container/BoundMethod.php +++ b/src/Illuminate/Container/BoundMethod.php @@ -175,7 +175,7 @@ protected static function addDependencyForCallParameter( unset($parameters[$paramName]); } elseif ($attribute = Util::getContextualAttributeFromDependency($parameter)) { - $pendingDependencies[] = $container->resolveFromAttribute($attribute); + $pendingDependencies[] = $container->resolveFromAttribute($attribute, $parameter); } elseif (! is_null($className = Util::getParameterClassName($parameter))) { if (array_key_exists($className, $parameters)) { $pendingDependencies[] = $parameters[$className]; diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index d46bd98957db..5633559a6859 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1229,7 +1229,7 @@ protected function resolveDependencies(array $dependencies) $result = null; if (! is_null($attribute = Util::getContextualAttributeFromDependency($dependency))) { - $result = $this->resolveFromAttribute($attribute); + $result = $this->resolveFromAttribute($attribute, $dependency); } // If the class is null, it means the dependency is a string or some other @@ -1378,7 +1378,7 @@ protected function resolveVariadicClass(ReflectionParameter $parameter) * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function resolveFromAttribute(ReflectionAttribute $attribute) + public function resolveFromAttribute(ReflectionAttribute $attribute, ReflectionParameter $parameter) { $handler = $this->contextualAttributes[$attribute->getName()] ?? null; @@ -1392,7 +1392,7 @@ public function resolveFromAttribute(ReflectionAttribute $attribute) throw new BindingResolutionException("Contextual binding attribute [{$attribute->getName()}] has no registered handler."); } - return $handler($instance, $this); + return $handler($instance, $this, $parameter); } /** diff --git a/src/Illuminate/Routing/ResolvesRouteDependencies.php b/src/Illuminate/Routing/ResolvesRouteDependencies.php index bd3139fc7691..ee0da613c677 100644 --- a/src/Illuminate/Routing/ResolvesRouteDependencies.php +++ b/src/Illuminate/Routing/ResolvesRouteDependencies.php @@ -76,7 +76,7 @@ public function resolveMethodDependencies(array $parameters, ReflectionFunctionA protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue) { if ($attribute = Util::getContextualAttributeFromDependency($parameter)) { - return $this->container->resolveFromAttribute($attribute); + return $this->container->resolveFromAttribute($attribute, $parameter); } $className = Reflector::getParameterClassName($parameter); diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index 88da22176e07..db799a6b2ce8 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -35,6 +35,7 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use ReflectionParameter; class ContextualAttributeBindingTest extends TestCase { @@ -369,6 +370,28 @@ public function testTagAttribute() $this->assertEquals([1, 2], iterator_to_array($value)); } + + public function testParameterIsPassedToContextualAttributeResolver() + { + $container = new Container; + + $value = $container->make(HasParameterAwareAttribute::class); + + $this->assertSame('name', $value->name); + } + + public function testParameterIsPassedToContextualAttributeResolverOnAppCall() + { + $container = new Container; + + $value = $container->call(function ( + #[ContainerTestParameterAwareAttribute] ?string $name + ) { + return $name; + }); + + $this->assertSame('name', $value); + } } #[Attribute(Attribute::TARGET_PARAMETER)] @@ -495,6 +518,15 @@ public function after(self $attribute, object $value, Container $container): voi } } +#[Attribute(Attribute::TARGET_PARAMETER)] +final class ContainerTestParameterAwareAttribute implements ContextualAttribute +{ + public function resolve(self $attribute, Container $container, ReflectionParameter $parameter): string + { + return $parameter->getName(); + } +} + final class ContainerTestHasConfigValueWithResolvePropertyAndAfterCallback { public function __construct( @@ -638,3 +670,12 @@ public function __construct( // } } + +final class HasParameterAwareAttribute +{ + public function __construct( + #[ContainerTestParameterAwareAttribute] public readonly ?string $name, + ) { + // + } +} From 898112eaee26ac275da69d0f64e379828e6749f8 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:10:48 +0000 Subject: [PATCH 558/596] Update facade docblocks --- src/Illuminate/Support/Facades/App.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 5bad0f492df4..2aaef3a9dba2 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -124,7 +124,7 @@ * @method static object|mixed makeWith(string|callable $abstract, array $parameters = []) * @method static object|mixed get(string $id) * @method static object build(\Closure|string $concrete) - * @method static mixed resolveFromAttribute(\ReflectionAttribute $attribute) + * @method static mixed resolveFromAttribute(\ReflectionAttribute $attribute, \ReflectionParameter $parameter) * @method static void beforeResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void resolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void afterResolving(\Closure|string $abstract, \Closure|null $callback = null) From 6e2db3491d9e4d17dba1aff237776203809c6a48 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 9 Jun 2026 18:41:07 +0530 Subject: [PATCH 559/596] Add multi-type union support to Illuminate JsonSchema (#60455) * Add multi-type union support to JsonSchema component * Validate union members and reject lossy union keywords * Formatting * Formatting * Update UnionType.php --- .../Contracts/JsonSchema/JsonSchema.php | 8 ++ src/Illuminate/JsonSchema/Deserializer.php | 59 +++++++--- src/Illuminate/JsonSchema/JsonSchema.php | 1 + .../JsonSchema/JsonSchemaTypeFactory.php | 10 ++ src/Illuminate/JsonSchema/Serializer.php | 7 +- src/Illuminate/JsonSchema/Types/UnionType.php | 58 ++++++++++ tests/JsonSchema/DeserializerTest.php | 102 ++++++++++++++++- tests/JsonSchema/TypeTest.php | 16 +++ tests/JsonSchema/UnionTypeTest.php | 107 ++++++++++++++++++ 9 files changed, 349 insertions(+), 19 deletions(-) create mode 100644 src/Illuminate/JsonSchema/Types/UnionType.php create mode 100644 tests/JsonSchema/UnionTypeTest.php diff --git a/src/Illuminate/Contracts/JsonSchema/JsonSchema.php b/src/Illuminate/Contracts/JsonSchema/JsonSchema.php index 60e7d20f05c3..3c327d01809c 100644 --- a/src/Illuminate/Contracts/JsonSchema/JsonSchema.php +++ b/src/Illuminate/Contracts/JsonSchema/JsonSchema.php @@ -48,4 +48,12 @@ public function number(); * @return \Illuminate\JsonSchema\Types\BooleanType */ public function boolean(); + + /** + * Create a new multi-type union instance. + * + * @param array $types + * @return \Illuminate\JsonSchema\Types\UnionType + */ + public function union(array $types); } diff --git a/src/Illuminate/JsonSchema/Deserializer.php b/src/Illuminate/JsonSchema/Deserializer.php index dab25e9ce89e..e9db387cb754 100644 --- a/src/Illuminate/JsonSchema/Deserializer.php +++ b/src/Illuminate/JsonSchema/Deserializer.php @@ -51,15 +51,21 @@ protected function build(array $schema, array $refs = []): Types\Type [$name, $nullableFromType] = $this->resolveType($schema); - $type = match ($name) { - 'object' => $this->buildObject($schema, $refs), - 'array' => $this->buildArray($schema, $refs), - 'string' => $this->buildString($schema), - 'integer' => $this->buildInteger($schema), - 'number' => $this->buildNumber($schema), - 'boolean' => new Types\BooleanType, - default => throw new InvalidArgumentException("Unsupported JSON Schema type [{$name}]."), - }; + if (is_array($name)) { + $this->ensureUnionConstraintsAreSupported($schema); + + $type = new Types\UnionType($name); + } else { + $type = match ($name) { + 'object' => $this->buildObject($schema, $refs), + 'array' => $this->buildArray($schema, $refs), + 'string' => $this->buildString($schema), + 'integer' => $this->buildInteger($schema), + 'number' => $this->buildNumber($schema), + 'boolean' => new Types\BooleanType, + default => throw new InvalidArgumentException("Unsupported JSON Schema type [{$name}]."), + }; + } $this->applyCommon($type, $schema); @@ -262,7 +268,7 @@ protected function applyCommon(Types\Type $type, array $schema): void * Resolve the base type name and whether the schema is nullable. * * @param array $schema - * @return array{0: string, 1: bool} + * @return array{0: string|array, 1: bool} * * @throws \InvalidArgumentException */ @@ -274,15 +280,13 @@ protected function resolveType(array $schema): array if (is_array($type)) { $nullable = in_array('null', $type, true); - $names = array_values(array_unique(array_filter( + $names = array_values(array_unique(array_map('strval', array_filter( $type, static fn ($value) => $value !== 'null', - ))); + )))); if (count($names) > 1) { - throw new InvalidArgumentException( - 'Unable to represent a multi-type JSON Schema union ['.implode(', ', array_map('strval', $names)).'].' - ); + return [$names, $nullable]; } $type = $names[0] ?? null; @@ -297,6 +301,31 @@ protected function resolveType(array $schema): array return [$type, $nullable]; } + /** + * Ensure a multi-type union carries no type-specific constraint keywords. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + protected function ensureUnionConstraintsAreSupported(array $schema): void + { + $keywords = [ + 'minLength', 'maxLength', 'pattern', 'format', + 'minimum', 'maximum', 'multipleOf', + 'items', 'minItems', 'maxItems', 'uniqueItems', + 'properties', 'required', 'additionalProperties', + ]; + + $unsupported = array_values(array_intersect($keywords, array_keys($schema))); + + if ($unsupported !== []) { + throw new InvalidArgumentException( + 'Type-specific keywords ['.implode(', ', $unsupported).'] are not supported on a multi-type JSON Schema union.' + ); + } + } + /** * Infer the type name when "type" is absent but the shape is unambiguous. * diff --git a/src/Illuminate/JsonSchema/JsonSchema.php b/src/Illuminate/JsonSchema/JsonSchema.php index 5ca003061ce6..d08f4b17aa2c 100644 --- a/src/Illuminate/JsonSchema/JsonSchema.php +++ b/src/Illuminate/JsonSchema/JsonSchema.php @@ -12,6 +12,7 @@ * @method static Types\StringType string() * @method static Types\BooleanType boolean() * @method static Types\ArrayType array() + * @method static Types\UnionType union(array $types) */ class JsonSchema { diff --git a/src/Illuminate/JsonSchema/JsonSchemaTypeFactory.php b/src/Illuminate/JsonSchema/JsonSchemaTypeFactory.php index 8729e3c31da0..5891e3faa2b1 100644 --- a/src/Illuminate/JsonSchema/JsonSchemaTypeFactory.php +++ b/src/Illuminate/JsonSchema/JsonSchemaTypeFactory.php @@ -60,4 +60,14 @@ public function boolean(): Types\BooleanType { return new Types\BooleanType; } + + /** + * Create a new multi-type union instance. + * + * @param array $types + */ + public function union(array $types): Types\UnionType + { + return new Types\UnionType($types); + } } diff --git a/src/Illuminate/JsonSchema/Serializer.php b/src/Illuminate/JsonSchema/Serializer.php index 99811c8f96c9..199f05814bb5 100644 --- a/src/Illuminate/JsonSchema/Serializer.php +++ b/src/Illuminate/JsonSchema/Serializer.php @@ -32,13 +32,18 @@ public static function serialize(Types\Type $type): array Types\NumberType::class => 'number', Types\ObjectType::class => 'object', Types\StringType::class => 'string', + Types\UnionType::class => $attributes['types'], default => throw new RuntimeException('Unsupported ['.get_class($type).'] type.'), }; + unset($attributes['types']); + $nullable = static::isNullable($type); if ($nullable) { - $attributes['type'] = [$attributes['type'], 'null']; + $attributes['type'] = is_array($attributes['type']) + ? [...$attributes['type'], 'null'] + : [$attributes['type'], 'null']; } $attributes = array_filter($attributes, static function (mixed $value, string $key) { diff --git a/src/Illuminate/JsonSchema/Types/UnionType.php b/src/Illuminate/JsonSchema/Types/UnionType.php new file mode 100644 index 000000000000..a5142577b9ef --- /dev/null +++ b/src/Illuminate/JsonSchema/Types/UnionType.php @@ -0,0 +1,58 @@ + + */ + public const SUPPORTED = ['string', 'integer', 'number', 'boolean', 'object', 'array']; + + /** + * The union's member type names. + * + * @var array + */ + protected array $types; + + /** + * Create a new union type instance. + * + * @param array $types + * + * @throws \InvalidArgumentException + */ + public function __construct(array $types) + { + $names = array_map('strval', $types); + + if (in_array('null', $names, true)) { + $this->nullable(); + + $names = array_filter($names, static fn (string $name) => $name !== 'null'); + } + + foreach ($names as $name) { + if (! in_array($name, self::SUPPORTED, true)) { + throw new InvalidArgumentException("Unsupported JSON Schema type [{$name}] in a multi-type union."); + } + } + + $this->types = array_values(array_unique($names)); + } + + /** + * Get the union's member type names. + * + * @return array + */ + public function types(): array + { + return $this->types; + } +} diff --git a/tests/JsonSchema/DeserializerTest.php b/tests/JsonSchema/DeserializerTest.php index d0b1ed5db4d7..8afe331b8127 100644 --- a/tests/JsonSchema/DeserializerTest.php +++ b/tests/JsonSchema/DeserializerTest.php @@ -10,6 +10,7 @@ use Illuminate\JsonSchema\Types\NumberType; use Illuminate\JsonSchema\Types\ObjectType; use Illuminate\JsonSchema\Types\StringType; +use Illuminate\JsonSchema\Types\UnionType; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -469,13 +470,108 @@ public function test_it_resolves_the_same_ref_used_in_sibling_positions(): void ], $type->toArray()); } - public function test_it_throws_for_a_multi_type_union(): void + public function test_it_deserializes_a_multi_type_union(): void + { + $type = JsonSchema::fromArray([ + 'type' => ['string', 'number', 'boolean'], + ]); + + $this->assertInstanceOf(UnionType::class, $type); + $this->assertSame(['string', 'number', 'boolean'], $type->types()); + $this->assertSame(['type' => ['string', 'number', 'boolean']], $type->toArray()); + } + + public function test_it_deserializes_a_nullable_multi_type_union(): void + { + $type = JsonSchema::fromArray([ + 'type' => ['string', 'number', 'null'], + ]); + + $this->assertInstanceOf(UnionType::class, $type); + $this->assertSame(['string', 'number'], $type->types()); + $this->assertSame(['type' => ['string', 'number', 'null']], $type->toArray()); + } + + public function test_it_does_not_treat_a_single_type_plus_null_as_a_union(): void + { + $type = JsonSchema::fromArray([ + 'type' => ['string', 'null'], + ]); + + $this->assertInstanceOf(StringType::class, $type); + $this->assertSame(['type' => ['string', 'null']], $type->toArray()); + } + + public function test_it_dedupes_and_preserves_order_of_union_members(): void + { + $type = JsonSchema::fromArray([ + 'type' => ['number', 'string', 'number', 'boolean', 'string'], + ]); + + $this->assertInstanceOf(UnionType::class, $type); + $this->assertSame(['number', 'string', 'boolean'], $type->types()); + } + + public function test_it_deserializes_a_union_nested_in_an_object_property(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'object', + 'properties' => [ + 'value' => ['type' => ['string', 'number']], + ], + ]); + + $this->assertInstanceOf(ObjectType::class, $type); + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'value' => ['type' => ['string', 'number']], + ], + ], $type->toArray()); + } + + public function test_it_deserializes_a_union_nested_in_array_items(): void + { + $type = JsonSchema::fromArray([ + 'type' => 'array', + 'items' => ['type' => ['string', 'integer', 'null']], + ]); + + $this->assertInstanceOf(ArrayType::class, $type); + $this->assertEquals([ + 'type' => 'array', + 'items' => ['type' => ['string', 'integer', 'null']], + ], $type->toArray()); + } + + public function test_it_throws_for_an_unsupported_union_member(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported JSON Schema type [wat] in a multi-type union.'); + + JsonSchema::fromArray([ + 'type' => ['string', 'wat'], + ]); + } + + public function test_it_throws_for_a_non_string_union_member(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unable to represent a multi-type JSON Schema union [string, integer].'); + $this->expectExceptionMessage('Unsupported JSON Schema type [123] in a multi-type union.'); JsonSchema::fromArray([ - 'type' => ['string', 'integer'], + 'type' => ['string', 123], + ]); + } + + public function test_it_throws_when_a_union_carries_type_specific_keywords(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Type-specific keywords [items] are not supported on a multi-type JSON Schema union.'); + + JsonSchema::fromArray([ + 'type' => ['array', 'string'], + 'items' => ['type' => 'integer'], ]); } diff --git a/tests/JsonSchema/TypeTest.php b/tests/JsonSchema/TypeTest.php index 7e897b8f9844..fe54a2a31cc3 100644 --- a/tests/JsonSchema/TypeTest.php +++ b/tests/JsonSchema/TypeTest.php @@ -319,6 +319,15 @@ public static function validSchemasProvider(): array [JsonSchema::array()->enum([[]]), []], [JsonSchema::array()->nullable(), null], [JsonSchema::array()->nullable(false), []], + + // UnionType + [JsonSchema::union(['string', 'number']), 'hello'], + [JsonSchema::union(['string', 'number']), 42], + [JsonSchema::union(['string', 'number']), 3.14], + [JsonSchema::union(['integer', 'boolean']), true], + [JsonSchema::union(['string', 'number'])->enum(['draft', 5]), 'draft'], + [JsonSchema::union(['string', 'number'])->nullable(), null], + [JsonSchema::union(['string', 'number'])->nullable(), 'still valid'], ]; } @@ -413,6 +422,13 @@ public static function invalidSchemasProvider(): array [JsonSchema::array()->enum([['a'], ['b']]), ['a', 'b']], // not equal to any enum member [JsonSchema::array()->items(JsonSchema::string()->max(1)), ['ab']], // item too long [JsonSchema::array()->nullable(false), null], // not nullable + + // UnionType + [JsonSchema::union(['string', 'number']), true], // boolean not in union + [JsonSchema::union(['string', 'number']), []], // array not in union + [JsonSchema::union(['string', 'number']), null], // null not allowed unless nullable + [JsonSchema::union(['integer', 'boolean']), 'nope'], // string not in union + [JsonSchema::union(['string', 'number'])->enum(['draft', 5]), 'archived'], // not in enum ]; } diff --git a/tests/JsonSchema/UnionTypeTest.php b/tests/JsonSchema/UnionTypeTest.php new file mode 100644 index 000000000000..a92ab4ff059b --- /dev/null +++ b/tests/JsonSchema/UnionTypeTest.php @@ -0,0 +1,107 @@ +assertEquals([ + 'type' => ['string', 'number', 'boolean'], + ], $type->toArray()); + } + + public function test_serializes_with_metadata(): void + { + $type = JsonSchema::union(['string', 'number']) + ->title('Value') + ->description('A string or a number'); + + $this->assertEquals([ + 'type' => ['string', 'number'], + 'title' => 'Value', + 'description' => 'A string or a number', + ], $type->toArray()); + } + + public function test_dedupes_and_preserves_member_order(): void + { + $type = JsonSchema::union(['number', 'string', 'number', 'boolean', 'string']); + + $this->assertSame(['number', 'string', 'boolean'], $type->types()); + $this->assertSame(['type' => ['number', 'string', 'boolean']], $type->toArray()); + } + + public function test_appends_null_when_nullable(): void + { + $type = JsonSchema::union(['string', 'number'])->nullable(); + + $this->assertEquals([ + 'type' => ['string', 'number', 'null'], + ], $type->toArray()); + } + + public function test_it_normalizes_a_null_member_into_nullability(): void + { + $type = JsonSchema::union(['string', 'number', 'null']); + + $this->assertSame(['string', 'number'], $type->types()); + $this->assertEquals([ + 'type' => ['string', 'number', 'null'], + ], $type->toArray()); + } + + public function test_it_does_not_duplicate_null_when_already_nullable(): void + { + $type = JsonSchema::union(['string', 'null'])->nullable(); + + $this->assertEquals([ + 'type' => ['string', 'null'], + ], $type->toArray()); + } + + public function test_it_rejects_an_unsupported_member_name(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported JSON Schema type [wat] in a multi-type union.'); + + JsonSchema::union(['string', 'wat']); + } + + public function test_it_rejects_a_non_string_member(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported JSON Schema type [123] in a multi-type union.'); + + JsonSchema::union(['string', 123]); + } + + public function test_it_round_trips_a_union(): void + { + $schema = ['type' => ['string', 'number', 'boolean']]; + + $type = JsonSchema::fromArray($schema); + + $this->assertInstanceOf(UnionType::class, $type); + $this->assertSame($schema, Serializer::serialize($type)); + $this->assertEquals($type, JsonSchema::fromArray(Serializer::serialize($type))); + } + + public function test_it_round_trips_a_nullable_union(): void + { + $schema = ['type' => ['string', 'number', 'null']]; + + $type = JsonSchema::fromArray($schema); + + $this->assertInstanceOf(UnionType::class, $type); + $this->assertSame($schema, Serializer::serialize($type)); + } +} From 8fa44517cf70d22b64cc195f28a20bb2f6a39a4b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Jun 2026 08:12:13 -0500 Subject: [PATCH 560/596] formatting --- src/Illuminate/JsonSchema/Deserializer.php | 50 +++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Illuminate/JsonSchema/Deserializer.php b/src/Illuminate/JsonSchema/Deserializer.php index e9db387cb754..790a3c1b4461 100644 --- a/src/Illuminate/JsonSchema/Deserializer.php +++ b/src/Illuminate/JsonSchema/Deserializer.php @@ -301,31 +301,6 @@ protected function resolveType(array $schema): array return [$type, $nullable]; } - /** - * Ensure a multi-type union carries no type-specific constraint keywords. - * - * @param array $schema - * - * @throws \InvalidArgumentException - */ - protected function ensureUnionConstraintsAreSupported(array $schema): void - { - $keywords = [ - 'minLength', 'maxLength', 'pattern', 'format', - 'minimum', 'maximum', 'multipleOf', - 'items', 'minItems', 'maxItems', 'uniqueItems', - 'properties', 'required', 'additionalProperties', - ]; - - $unsupported = array_values(array_intersect($keywords, array_keys($schema))); - - if ($unsupported !== []) { - throw new InvalidArgumentException( - 'Type-specific keywords ['.implode(', ', $unsupported).'] are not supported on a multi-type JSON Schema union.' - ); - } - } - /** * Infer the type name when "type" is absent but the shape is unambiguous. * @@ -384,6 +359,31 @@ protected function inferEnumType(array $enum): ?string return $resolved; } + /** + * Ensure a multi-type union carries no type-specific constraint keywords. + * + * @param array $schema + * + * @throws \InvalidArgumentException + */ + protected function ensureUnionConstraintsAreSupported(array $schema): void + { + $keywords = [ + 'minLength', 'maxLength', 'pattern', 'format', + 'minimum', 'maximum', 'multipleOf', + 'items', 'minItems', 'maxItems', 'uniqueItems', + 'properties', 'required', 'additionalProperties', + ]; + + $unsupported = array_values(array_intersect($keywords, array_keys($schema))); + + if ($unsupported !== []) { + throw new InvalidArgumentException( + 'Type-specific keywords ['.implode(', ', $unsupported).'] are not supported on a multi-type JSON Schema union.' + ); + } + } + /** * Collapse "anyOf" / "oneOf" null branches into a single effective schema. * From 1ad604862a9f028e2defcf3e6a0bda1464fce863 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:43:04 -0400 Subject: [PATCH 561/596] [13.x] Cache `rememberWithState()` (#60385) * rememberWithState * Tim's suggestion * tests * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Cache/Repository.php | 19 +++++++++++++-- src/Illuminate/Support/Facades/Cache.php | 1 + tests/Cache/CacheRepositoryTest.php | 30 ++++++++++++++++++++++++ types/Cache/Repository.php | 3 +++ types/Support/Facades/Cache.php | 3 +++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 19b351c86f08..785a7592b1ba 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -556,6 +556,21 @@ public function forever($key, $value) * @return TCacheValue */ public function remember($key, $ttl, Closure $callback) + { + return $this->rememberWithWarmth($key, $ttl, $callback)[0]; + } + + /** + * Get an item from the cache, or execute the given Closure and store the result. + * + * @template TCacheValue + * + * @param \UnitEnum|string $key + * @param \Closure|\DateTimeInterface|\DateInterval|int|null $ttl + * @param \Closure(): TCacheValue $callback + * @return array{TCacheValue, bool} The cached value and whether it was warm. + */ + public function rememberWithWarmth($key, $ttl, Closure $callback): array { $value = $this->get($key); @@ -563,14 +578,14 @@ public function remember($key, $ttl, Closure $callback) // not we will execute the given Closure and cache the result of that for a // given number of seconds so it's available for all subsequent requests. if (! is_null($value)) { - return $value; + return [$value, true]; } $value = $callback(); $this->put($key, $value, value($ttl, $value)); - return $value; + return [$value, false]; } /** diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 128d6b50fa3d..64a42b9548a6 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -39,6 +39,7 @@ * @method static int|bool decrement(\UnitEnum|string $key, mixed $value = 1) * @method static bool forever(\UnitEnum|string $key, mixed $value) * @method static mixed remember(\UnitEnum|string $key, \Closure|\DateTimeInterface|\DateInterval|int|null $ttl, \Closure $callback) + * @method static array rememberWithWarmth(\UnitEnum|string $key, \Closure|\DateTimeInterface|\DateInterval|int|null $ttl, \Closure $callback) * @method static mixed sear(\UnitEnum|string $key, \Closure $callback) * @method static mixed rememberForever(\UnitEnum|string $key, \Closure $callback) * @method static mixed flexible(\UnitEnum|string $key, array $ttl, callable $callback, array|null $lock = null, bool $alwaysDefer = false) diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index d76071a58b7e..09d395641c38 100755 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -147,6 +147,36 @@ public function testRememberMethodCallsPutAndReturnsDefault() $this->assertSame('bar', $result); } + public function testRememberWithWarmthReturnsCachedValue() + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('foo')->andReturn('bar'); + $repo->getStore()->shouldReceive('put')->never(); + + $result = $repo->rememberWithWarmth('foo', 10, function () { + $this->fail('The cache callback should not be called.'); + }); + + $this->assertSame(['bar', true], $result); + } + + public function testRememberWithWarmthCallsPutAndReturnsDefault() + { + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->once()->with('foo')->andReturn(null); + $repo->getStore()->shouldReceive('put')->once()->with('foo', 'bar', 10); + + $result = $repo->rememberWithWarmth('foo', function ($value) { + $this->assertSame('bar', $value); + + return 10; + }, function () { + return 'bar'; + }); + + $this->assertSame(['bar', false], $result); + } + public function testRememberForeverMethodCallsForeverAndReturnsDefault() { $repo = $this->getRepository(); diff --git a/types/Cache/Repository.php b/types/Cache/Repository.php index aaba868b4247..f0c97289f688 100644 --- a/types/Cache/Repository.php +++ b/types/Cache/Repository.php @@ -25,6 +25,9 @@ assertType('36', $cache->remember('cache', Carbon::now(), function (): int { return 36; })); +assertType('array{36, bool}', $cache->rememberWithWarmth('cache', Carbon::now(), function (): int { + return 36; +})); assertType('36', $cache->rememberForever('cache', function (): int { return 36; })); diff --git a/types/Support/Facades/Cache.php b/types/Support/Facades/Cache.php index a29c13d6797f..913815d36d11 100644 --- a/types/Support/Facades/Cache.php +++ b/types/Support/Facades/Cache.php @@ -22,6 +22,9 @@ assertType('mixed', Cache::remember('cache', Carbon::now(), function (): int { return 36; })); +assertType('array', Cache::rememberWithWarmth('cache', Carbon::now(), function (): int { + return 36; +})); assertType('mixed', Cache::rememberForever('cache', function (): int { return 36; })); From 7e23b2aa4e1133a43835c93a810b4bedc40e425b Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:45:51 +0000 Subject: [PATCH 562/596] Update version to v13.15.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 9717abe1abb0..1f27e35c7360 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 = '13.14.0'; + const VERSION = '13.15.0'; /** * The base path for the Laravel installation. From 85261cf7d0c116a9309a75eeee9832ba79166915 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:47:42 +0000 Subject: [PATCH 563/596] Update CHANGELOG --- CHANGELOG.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0b89f8a218..ea01daa45a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,105 @@ # Release Notes for 13.x -## [Unreleased](https://github.com/laravel/framework/compare/v13.14.0...13.x) +## [Unreleased](https://github.com/laravel/framework/compare/v13.15.0...13.x) + +## [v13.15.0](https://github.com/laravel/framework/compare/v13.14.0...v13.15.0) - 2026-06-09 + +* [12.x] Fix infinite recursion when defining model scope with attribute as private by [@noefleury](https://github.com/noefleury) in https://github.com/laravel/framework/pull/59958 + +* [12.x] Fix infinite recursion when middleware group referencing itself by [@noefleury](https://github.com/noefleury) in https://github.com/laravel/framework/pull/60002 + +* [12.x] Backport #60000 to 12.x by [@iWader](https://github.com/iWader) in https://github.com/laravel/framework/pull/60006 + +* [12.x] Narrow attachment url scheme by [@benbjurstrom](https://github.com/benbjurstrom) in https://github.com/laravel/framework/pull/60035 + +* [12.x] backport #60045 to 12.x by [@levikl](https://github.com/levikl) in https://github.com/laravel/framework/pull/60052 + +* [12.x] Back port cloud queues by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/60122 + +* [12.x] Fix Number::fileSize() handling of negative byte values by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60147 + +* [12.x] Remove stale PHPStan ignore comments from type tests by [@jradtilbrook](https://github.com/jradtilbrook) in https://github.com/laravel/framework/pull/60167 + +* [12.x] Output cloud request ID in logs by [@jradtilbrook](https://github.com/jradtilbrook) in https://github.com/laravel/framework/pull/60166 + +* [12.x] Dedicated Cloud Queue by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60181 + +* [12.x] Rename X-Request-ID header to Cloud-Request-ID by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60189 + +* [12.x] Boot managed queues before service providers boot by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60199 + +* [12.x] Accept Symfony's new control-characters exception message in mailer test by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60203 + +* [12.x] Fix queue:failed command to show real class name by [@clementmas](https://github.com/clementmas) in https://github.com/laravel/framework/pull/60279 + +* [12.x] Throw ManagedQueueNotFoundException when a managed queue is missing by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60276 + +* [12.x] Preserve empty HTTP attach contents by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/60291 + +* Fix [@params](https://github.com/params) typo in Fluent and MessageBag toPrettyJson() docblocks by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60313 + +* [12.x] Fix regex typo in Env::addVariableToEnvContents that prevented quotin… by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60312 + +* [12.x] Fix Number::trim() returning null for INF and NAN values by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60322 + +* [12.x] Fix FIFO queue name normalization in Cloud managed queues by [@kieranbrown](https://github.com/kieranbrown) in https://github.com/laravel/framework/pull/60316 + +* [12.x] Fix Number::pairs() infinite loop when $by is zero or negative by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60324 + +* [12.x] Ensure path seperators aren't encoded in LocalFilesystemAdapter by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60350 + +* [12.x] Ensure `config` is bound before trying to log deprecation notice by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60376 + +* [12.x] Add JSON Schema array deserializer by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/framework/pull/60387 + +* ### [13.x] Fix validation bypass in date_equals rule due to loose comparison by [@gr8man](https://github.com/gr8man) in https://github.com/laravel/framework/pull/60393 + +* [13.x] Add Macroable to InvokedProcess by [@yoeriboven](https://github.com/yoeriboven) in https://github.com/laravel/framework/pull/60392 + +* [13.x] Restrict allowed classes in routing unserialization by [@gr8man](https://github.com/gr8man) in https://github.com/laravel/framework/pull/60391 + +* [13.x] Extract flexible cache created-key prefix into a named constant by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60390 + +* [13.x] Add missing type hints to WorkerIdle and listenForSignals to match sibling events by [@Amirhf1](https://github.com/Amirhf1) in https://github.com/laravel/framework/pull/60389 + +* [13.x] Allow enums in Queue::route by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60402 + +* [13.x] Ensure unchanged compiled Blade views are not left expired by [@dansan92](https://github.com/dansan92) in https://github.com/laravel/framework/pull/60401 + +* [12.x] Skip pg_collation lookup in compileColumns() on PostgreSQL servers before 9.1 by [@fissible](https://github.com/fissible) in https://github.com/laravel/framework/pull/60400 + +* [13.x] Add units to queue attributes by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60431 + +* [13.x] Add Prohibitable to `cache:clear` and `queue:flush` by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60430 + +* [13.x] Refactor: use `Repository::FLEXIBLE_CREATED_KEY_PREFIX` in test by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/60424 + +* [13.x] Refactor: add `\JsonException` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/60423 + +* [13.x] Add generics to DatabaseTransactionsManager transaction getters by [@dfinchenko](https://github.com/dfinchenko) in https://github.com/laravel/framework/pull/60420 + +* [13.x] Fix: add `@throws \ReflectionException` by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/60436 + +* [13.x] Fix: add `|null` in doc blocks by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/60435 + +* Merge branch 12.x by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/60441 + +* [13.x] fix model:prune Command options validation typo by [@clemblanco](https://github.com/clemblanco) in https://github.com/laravel/framework/pull/60444 + +* [13.x] Add typed translation accessors by [@dansan92](https://github.com/dansan92) in https://github.com/laravel/framework/pull/60443 + +* [13.x] Fix HandleExceptions fatal when static::$app is null during Octane request marshaling by [@majidfeiz](https://github.com/majidfeiz) in https://github.com/laravel/framework/pull/60439 + +* [13.x] Adjust PruneCommandTest by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/60445 + +* [13.x] Add generic to QueueRoutes::all() return type by [@dfinchenko](https://github.com/dfinchenko) in https://github.com/laravel/framework/pull/60447 + +* [13.x] Pass reflection parameter to contextual attribute resolve method by [@SanderSander](https://github.com/SanderSander) in https://github.com/laravel/framework/pull/60457 + +* Add multi-type union support to Illuminate JsonSchema by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/framework/pull/60455 + +* [13.x] Cache `rememberWithState()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/60385 + ## [v13.14.0](https://github.com/laravel/framework/compare/v13.13.0...v13.14.0) - 2026-06-04 From 8fcdf4a4ff28293c74301c092135a49d4c5afa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20W=C3=BCnsch?= Date: Wed, 10 Jun 2026 14:55:11 +0200 Subject: [PATCH 564/596] Fix case of StdClass to stdClass in SupportHelpersTest (#60479) --- tests/Support/SupportHelpersTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 8f291d2c64f7..c8ff7b0a7e46 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -136,7 +136,7 @@ public function testWhen() $this->assertNull(when(0, fn () => null)); $this->assertSame('True', when([1, 2, 3, 4], 'True')); // Array $this->assertNull(when([], 'True')); // Empty Array = Falsy - $this->assertSame('True', when(new StdClass, fn () => 'True')); // Object + $this->assertSame('True', when(new stdClass, fn () => 'True')); // Object $this->assertSame('World', when(false, 'Hello', 'World')); $this->assertSame('World', when(1 === 0, 'Hello', 'World')); // strict types $this->assertSame('World', when(1 == '0', 'Hello', 'World')); // loose types From d1bebe9dbfc83f836d37263038bd50fbf90a821f Mon Sep 17 00:00:00 2001 From: igorlealantunes Date: Wed, 10 Jun 2026 10:06:15 -0300 Subject: [PATCH 565/596] Fix shell quoting when scheduled commands run as another user (#60469) --- .../Console/Scheduling/CommandBuilder.php | 4 +- tests/Console/Scheduling/EventTest.php | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Console/Scheduling/CommandBuilder.php b/src/Illuminate/Console/Scheduling/CommandBuilder.php index 17ad5522da92..02f71341253f 100644 --- a/src/Illuminate/Console/Scheduling/CommandBuilder.php +++ b/src/Illuminate/Console/Scheduling/CommandBuilder.php @@ -70,6 +70,8 @@ protected function buildBackgroundCommand(Event $event) */ protected function ensureCorrectUser(Event $event, $command) { - return $event->user && ! windows_os() ? 'sudo -u '.$event->user.' -- sh -c \''.$command.'\'' : $command; + return $event->user && ! windows_os() + ? 'sudo -u '.$event->user.' -- sh -c '.ProcessUtils::escapeArgument($command) + : $command; } } diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index df6df6a22f9a..7bb95aaac24c 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -5,6 +5,7 @@ use Illuminate\Console\Scheduling\Event; use Illuminate\Console\Scheduling\EventMutex; use Illuminate\Container\Container; +use Illuminate\Support\ProcessUtils; use Illuminate\Support\Str; use Illuminate\Support\Stringable; use Mockery as m; @@ -53,6 +54,46 @@ public function testBuildCommandInBackgroundUsingWindows() $this->assertSame('start /b cmd /v:on /c "(php -i & '.php_binary().' artisan schedule:finish '.$scheduleId.' ^!ERRORLEVEL^!) > "NUL" 2>&1"', $event->buildCommand()); } + #[RequiresOperatingSystem('Linux|Darwin')] + public function testBuildCommandWithUserUsingUnix() + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + $event->user('forge'); + + $this->assertSame("sudo -u forge -- sh -c 'php -i > '\''/dev/null'\'' 2>&1'", $event->buildCommand()); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testBuildCommandWithUserAndSpacesInOutputPathUsingUnix() + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + $event->user('forge')->sendOutputTo('/my folder/foo.log'); + + $this->assertSame("sudo -u forge -- sh -c 'php -i > '\''/my folder/foo.log'\'' 2>&1'", $event->buildCommand()); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testBuildCommandWithUserAndSingleQuotesInOutputPathUsingUnix() + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + $event->user('forge')->sendOutputTo("/tmp/o'brien.log"); + + $this->assertSame("sudo -u forge -- sh -c 'php -i > '\''/tmp/o'\''\'\'''\''brien.log'\'' 2>&1'", $event->buildCommand()); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testBuildCommandInBackgroundWithUserUsingUnix() + { + $event = new Event(m::mock(EventMutex::class), 'php -i'); + $event->user('forge')->runInBackground(); + + $scheduleId = '"framework'.DIRECTORY_SEPARATOR.'schedule-eeb46c93d45e928d62aaf684d727e213b7094822"'; + + $background = "(php -i > '/dev/null' 2>&1 ; '".php_binary()."' 'artisan' schedule:finish {$scheduleId} \"$?\") > '/dev/null' 2>&1 &"; + + $this->assertSame('sudo -u forge -- sh -c '.ProcessUtils::escapeArgument($background), $event->buildCommand()); + } + public function testBuildCommandSendOutputTo() { $quote = (DIRECTORY_SEPARATOR === '\\') ? '"' : "'"; From 017dafa2c3bcf0fded10b736073b9e5b023dc602 Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:08:19 +0200 Subject: [PATCH 566/596] Improve return type for `Localizable::withLocale()` (#60466) --- src/Illuminate/Support/Traits/Localizable.php | 6 ++++-- types/Support/Traits.php | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 types/Support/Traits.php diff --git a/src/Illuminate/Support/Traits/Localizable.php b/src/Illuminate/Support/Traits/Localizable.php index 1e9fa58c90bf..aa1f3309e7b8 100644 --- a/src/Illuminate/Support/Traits/Localizable.php +++ b/src/Illuminate/Support/Traits/Localizable.php @@ -9,9 +9,11 @@ trait Localizable /** * Run the callback with the given locale. * + * @template TReturn + * * @param string $locale - * @param \Closure $callback - * @return mixed + * @param \Closure(): TReturn $callback + * @return TReturn */ public function withLocale($locale, $callback) { diff --git a/types/Support/Traits.php b/types/Support/Traits.php new file mode 100644 index 000000000000..871e13faeb07 --- /dev/null +++ b/types/Support/Traits.php @@ -0,0 +1,15 @@ +withLocale('en', fn () => 'foo')); + } +}; From a27df39926d4cebbab4b20ea43ee137efbbedd96 Mon Sep 17 00:00:00 2001 From: Sander Visser Date: Wed, 10 Jun 2026 15:09:02 +0200 Subject: [PATCH 567/596] Improve RouteParameter attribute, use property name as route key (#60465) --- .../Container/Attributes/RouteParameter.php | 7 ++++--- .../ContextualAttributeBindingTest.php | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Container/Attributes/RouteParameter.php b/src/Illuminate/Container/Attributes/RouteParameter.php index 32afced0ecc4..3b430f845cae 100644 --- a/src/Illuminate/Container/Attributes/RouteParameter.php +++ b/src/Illuminate/Container/Attributes/RouteParameter.php @@ -5,6 +5,7 @@ use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; +use ReflectionParameter; #[Attribute(Attribute::TARGET_PARAMETER)] class RouteParameter implements ContextualAttribute @@ -12,7 +13,7 @@ class RouteParameter implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public string $parameter) + public function __construct(public ?string $parameter = null) { } @@ -23,8 +24,8 @@ public function __construct(public string $parameter) * @param \Illuminate\Contracts\Container\Container $container * @return mixed */ - public static function resolve(self $attribute, Container $container) + public static function resolve(self $attribute, Container $container, ReflectionParameter $parameter) { - return $container->make('request')->route($attribute->parameter); + return $container->make('request')->route($attribute->parameter ?? $parameter->getName()); } } diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index db799a6b2ce8..c97186fbdfc6 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -255,6 +255,20 @@ public function testRouteParameterAttribute() $container->make(RouteParameterTest::class); } + public function testRouteParameterAttributeWithouthParameterName() + { + $container = new Container; + $container->singleton('request', function () { + $request = m::mock(Request::class); + $request->shouldReceive('route')->with('foo')->andReturn(m::mock(Model::class)); + $request->shouldReceive('route')->with('bar')->andReturn('bar'); + + return $request; + }); + + $container->make(RouteParameterTestWithoutParameterName::class); + } + public function testContextAttribute(): void { $container = new Container; @@ -624,6 +638,13 @@ public function __construct(#[RouteParameter('foo')] Model $foo, #[RouteParamete } } +final class RouteParameterTestWithoutParameterName +{ + public function __construct(#[RouteParameter] Model $foo, #[RouteParameter] string $bar) + { + } +} + final class StorageTest { public function __construct( From a37716e6409cbb02219dab6f299e74015ab6353a Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Wed, 10 Jun 2026 14:09:30 +0100 Subject: [PATCH 568/596] Update CHANGELOG.md (#60464) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea01daa45a20..8befa118d390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,7 @@ * [12.x] Add JSON Schema array deserializer by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/laravel/framework/pull/60387 -* ### [13.x] Fix validation bypass in date_equals rule due to loose comparison by [@gr8man](https://github.com/gr8man) in https://github.com/laravel/framework/pull/60393 +* [13.x] Fix validation bypass in date_equals rule due to loose comparison by [@gr8man](https://github.com/gr8man) in https://github.com/laravel/framework/pull/60393 * [13.x] Add Macroable to InvokedProcess by [@yoeriboven](https://github.com/yoeriboven) in https://github.com/laravel/framework/pull/60392 From 2aaf7549c5ba60853396df006dd3eec54d8452ed Mon Sep 17 00:00:00 2001 From: Denys Finchenko Date: Wed, 10 Jun 2026 15:09:48 +0200 Subject: [PATCH 569/596] Add generic to HasEvents::dispatchesEvents() return type (#60463) --- src/Illuminate/Database/Eloquent/Concerns/HasEvents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php index cc0368e67da0..0b7fb41c1b39 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php @@ -398,7 +398,7 @@ public static function flushEventListeners() /** * Get the event map for the model. * - * @return array + * @return array */ public function dispatchesEvents() { From 9d7b9a953dd69579d3587e5083cb5a2d9667f4c5 Mon Sep 17 00:00:00 2001 From: Denys Finchenko Date: Wed, 10 Jun 2026 16:08:21 +0200 Subject: [PATCH 570/596] Improve return types for model callback-scope helpers (#60481) --- src/Illuminate/Database/Eloquent/Concerns/HasEvents.php | 6 ++++-- src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php | 6 ++++-- src/Illuminate/Database/Eloquent/Model.php | 6 ++++-- types/Database/Eloquent/Model.php | 4 ++++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php index 0b7fb41c1b39..e449d50169c7 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php @@ -439,8 +439,10 @@ public static function unsetEventDispatcher() /** * Execute a callback without firing any model events for any model type. * - * @param callable $callback - * @return mixed + * @template TReturn + * + * @param callable(): TReturn $callback + * @return TReturn */ public static function withoutEvents(callable $callback) { diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php index e612cf5db94f..198027424ed4 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php @@ -216,9 +216,11 @@ public static function withoutTimestamps(callable $callback) /** * Disable timestamps for the given model classes during the given callback scope. * + * @template TReturn + * * @param array $models - * @param callable $callback - * @return mixed + * @param callable(): TReturn $callback + * @return TReturn */ public static function withoutTimestampsOn($models, $callback) { diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index cb36dd411b36..fa5d1422cc68 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -651,8 +651,10 @@ public static function handleMissingAttributeViolationUsing(?callable $callback) /** * Execute a callback without broadcasting any model events for all model types. * - * @param callable $callback - * @return mixed + * @template TReturn + * + * @param callable(): TReturn $callback + * @return TReturn */ public static function withoutBroadcasting(callable $callback) { diff --git a/types/Database/Eloquent/Model.php b/types/Database/Eloquent/Model.php index e2f81f0eace1..c2966a9f0b0b 100644 --- a/types/Database/Eloquent/Model.php +++ b/types/Database/Eloquent/Model.php @@ -49,6 +49,10 @@ function test(User $user, Post $post, Comment $comment, Article $article): void assertType('bool', $user->restore()); assertType('User', $user->restoreOrCreate()); assertType('User', $user->createOrRestore()); + + assertType("'foo'", User::withoutEvents(fn () => 'foo')); + assertType("'foo'", User::withoutBroadcasting(fn () => 'foo')); + assertType("'foo'", User::withoutTimestampsOn([], fn () => 'foo')); } class Post extends Model From c69bf137e372dcf08b69f94d6d00d935743fb84f Mon Sep 17 00:00:00 2001 From: Alex Bowers Date: Thu, 11 Jun 2026 02:27:05 +0100 Subject: [PATCH 571/596] [13.x] Support enum for broadcastAs (#60483) * Support enum for broadcastAs [13.x] * StyleCI fix --- .../Broadcasting/BroadcastEvent.php | 4 +- tests/Broadcasting/BroadcastEventTest.php | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Broadcasting/BroadcastEvent.php b/src/Illuminate/Broadcasting/BroadcastEvent.php index 5c018e5a557f..1ac336899501 100644 --- a/src/Illuminate/Broadcasting/BroadcastEvent.php +++ b/src/Illuminate/Broadcasting/BroadcastEvent.php @@ -17,6 +17,8 @@ use ReflectionProperty; use Throwable; +use function Illuminate\Support\enum_value; + class BroadcastEvent implements ShouldQueue { use Queueable, ReadsQueueAttributes; @@ -88,7 +90,7 @@ public function __construct($event) public function handle(BroadcastingFactory $manager) { $name = method_exists($this->event, 'broadcastAs') - ? $this->event->broadcastAs() + ? enum_value($this->event->broadcastAs()) : get_class($this->event); $channels = Arr::wrap($this->event->broadcastOn()); diff --git a/tests/Broadcasting/BroadcastEventTest.php b/tests/Broadcasting/BroadcastEventTest.php index bfecbb7e44cb..a14abf957232 100644 --- a/tests/Broadcasting/BroadcastEventTest.php +++ b/tests/Broadcasting/BroadcastEventTest.php @@ -84,6 +84,40 @@ public function testSpecificChannelsPerConnection() (new BroadcastEvent($event))->handle($manager); } + public function testBroadcastAsStringIsUsedAsEventName() + { + $broadcaster = m::mock(Broadcaster::class); + + $broadcaster->shouldReceive('broadcast')->once()->with( + ['test-channel'], 'custom-name', ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] + ); + + $manager = m::mock(BroadcastingFactory::class); + + $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + + $event = new TestBroadcastEventWithStringName; + + (new BroadcastEvent($event))->handle($manager); + } + + public function testBroadcastAsBackedEnumResolvesToValue() + { + $broadcaster = m::mock(Broadcaster::class); + + $broadcaster->shouldReceive('broadcast')->once()->with( + ['test-channel'], 'custom-enum-name', ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] + ); + + $manager = m::mock(BroadcastingFactory::class); + + $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + + $event = new TestBroadcastEventWithEnumName; + + (new BroadcastEvent($event))->handle($manager); + } + public function testMiddlewareProxiesMiddlewareFromUnderlyingEvent() { $event = new class @@ -136,6 +170,27 @@ public function broadcastOn() } } +class TestBroadcastEventWithStringName extends TestBroadcastEvent +{ + public function broadcastAs() + { + return 'custom-name'; + } +} + +class TestBroadcastEventWithEnumName extends TestBroadcastEvent +{ + public function broadcastAs() + { + return TestBroadcastEventName::Custom; + } +} + +enum TestBroadcastEventName: string +{ + case Custom = 'custom-enum-name'; +} + class TestBroadcastEventWithManualData extends TestBroadcastEvent { public function broadcastWith() From fc155464710c0c10cfe96d966805383eb0275e7d Mon Sep 17 00:00:00 2001 From: Denys Finchenko Date: Thu, 11 Jun 2026 03:29:31 +0200 Subject: [PATCH 572/596] Improve return types for database connection callback wrappers (#60484) --- src/Illuminate/Database/Connection.php | 12 ++++++++---- src/Illuminate/Database/DatabaseManager.php | 6 ++++-- src/Illuminate/Database/Migrations/Migrator.php | 2 +- types/Database/Connection.php | 11 +++++++++++ types/Database/DatabaseManager.php | 10 ++++++++++ types/Database/Migrations/Migrator.php | 10 ++++++++++ 6 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 types/Database/Connection.php create mode 100644 types/Database/DatabaseManager.php create mode 100644 types/Database/Migrations/Migrator.php diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index 7b62c355946a..49a0ccfae7e2 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -679,8 +679,10 @@ public function pretend(Closure $callback) /** * Execute the given callback without "pretending". * - * @param \Closure $callback - * @return mixed + * @template TReturn + * + * @param \Closure(): TReturn $callback + * @return TReturn */ public function withoutPretending(Closure $callback) { @@ -1738,8 +1740,10 @@ public function setTablePrefix($prefix) /** * Execute the given callback without table prefix. * - * @param \Closure $callback - * @return mixed + * @template TReturn + * + * @param (\Closure($this): TReturn) $callback + * @return TReturn */ public function withoutTablePrefix(Closure $callback): mixed { diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index 0afeb7d41fd8..b6cf31dfd54b 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -343,9 +343,11 @@ public function reconnect($name = null) /** * Set the default database connection for the callback execution. * + * @template TReturn + * * @param \UnitEnum|string $name - * @param callable $callback - * @return mixed + * @param (callable(): TReturn) $callback + * @return TReturn */ public function usingConnection($name, callable $callback) { diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 2f89c0ca10cf..25413e1bdd65 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -659,7 +659,7 @@ public function getConnection() * * @param string $name * @param (callable(): TReturn) $callback - * @return mixed + * @return TReturn */ public function usingConnection($name, callable $callback) { diff --git a/types/Database/Connection.php b/types/Database/Connection.php new file mode 100644 index 000000000000..b0cc0adb2cdb --- /dev/null +++ b/types/Database/Connection.php @@ -0,0 +1,11 @@ +withoutPretending(fn () => 'foo')); +assertType("'foo'", $connection->withoutTablePrefix(fn () => 'foo')); diff --git a/types/Database/DatabaseManager.php b/types/Database/DatabaseManager.php new file mode 100644 index 000000000000..fdcf666ac1e1 --- /dev/null +++ b/types/Database/DatabaseManager.php @@ -0,0 +1,10 @@ +usingConnection('mysql', fn () => 'foo')); diff --git a/types/Database/Migrations/Migrator.php b/types/Database/Migrations/Migrator.php new file mode 100644 index 000000000000..d09c241c7d51 --- /dev/null +++ b/types/Database/Migrations/Migrator.php @@ -0,0 +1,10 @@ +usingConnection('mysql', fn () => 'foo')); From 715eb7b6448db167d9633a9f7cb9ceaaab32184c Mon Sep 17 00:00:00 2001 From: Jamie York Date: Thu, 11 Jun 2026 15:14:35 +0100 Subject: [PATCH 573/596] [13.x] Add `array` maintenance mode driver for parallel testing (#60489) * wip (array maintenance mode) * wip --- .../Foundation/ArrayMaintenanceMode.php | 65 +++++++++++++++++++ .../Foundation/MaintenanceModeManager.php | 10 +++ .../FoundationArrayMaintenanceModeTest.php | 48 ++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 src/Illuminate/Foundation/ArrayMaintenanceMode.php create mode 100644 tests/Foundation/FoundationArrayMaintenanceModeTest.php diff --git a/src/Illuminate/Foundation/ArrayMaintenanceMode.php b/src/Illuminate/Foundation/ArrayMaintenanceMode.php new file mode 100644 index 000000000000..b9f9065a45fa --- /dev/null +++ b/src/Illuminate/Foundation/ArrayMaintenanceMode.php @@ -0,0 +1,65 @@ +active = true; + $this->payload = $payload; + } + + /** + * Take the application out of maintenance. + * + * @return void + */ + public function deactivate(): void + { + $this->active = false; + $this->payload = []; + } + + /** + * Determine if the application is currently down for maintenance. + * + * @return bool + */ + public function active(): bool + { + return $this->active; + } + + /** + * Get the data array which was provided when the application was placed into maintenance. + * + * @return array + */ + public function data(): array + { + return $this->payload; + } +} diff --git a/src/Illuminate/Foundation/MaintenanceModeManager.php b/src/Illuminate/Foundation/MaintenanceModeManager.php index 4d233f44f3e3..bd859f353ed1 100644 --- a/src/Illuminate/Foundation/MaintenanceModeManager.php +++ b/src/Illuminate/Foundation/MaintenanceModeManager.php @@ -16,6 +16,16 @@ protected function createFileDriver(): FileBasedMaintenanceMode return new FileBasedMaintenanceMode(); } + /** + * Create an instance of the array based maintenance driver. + * + * @return \Illuminate\Foundation\ArrayMaintenanceMode + */ + protected function createArrayDriver(): ArrayMaintenanceMode + { + return new ArrayMaintenanceMode(); + } + /** * Create an instance of the cache based maintenance driver. * diff --git a/tests/Foundation/FoundationArrayMaintenanceModeTest.php b/tests/Foundation/FoundationArrayMaintenanceModeTest.php new file mode 100644 index 000000000000..9f423b866374 --- /dev/null +++ b/tests/Foundation/FoundationArrayMaintenanceModeTest.php @@ -0,0 +1,48 @@ +assertFalse($manager->active()); + + $manager->activate(['payload']); + $this->assertTrue($manager->active()); + } + + public function test_it_retrieves_payload() + { + $manager = new ArrayMaintenanceMode(); + + $manager->activate(['payload']); + $this->assertSame(['payload'], $manager->data()); + } + + public function test_it_stores_payload() + { + $manager = new ArrayMaintenanceMode(); + + $manager->activate(['payload']); + + $this->assertTrue($manager->active()); + $this->assertSame(['payload'], $manager->data()); + } + + public function test_it_removes_payload() + { + $manager = new ArrayMaintenanceMode(); + + $manager->activate(['payload']); + $manager->deactivate(); + + $this->assertFalse($manager->active()); + $this->assertSame([], $manager->data()); + } +} From 4b4486b762aa43ef4c1a971657a3e907a20ab071 Mon Sep 17 00:00:00 2001 From: "ast." <118503951+astandkaya@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:28:04 +0900 Subject: [PATCH 574/596] [13.x] Add `whenFilledEnum` method to `InteractsWithData` (#60486) * [13.x] Add `whenFilledEnum` method to `InteractsWithData` * formatting --------- Co-authored-by: Taylor Otwell --- .../Support/Traits/InteractsWithData.php | 26 ++++++++++++++++ tests/Http/HttpRequestTest.php | 31 +++++++++++++++++++ tests/Support/ValidatedInputTest.php | 31 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/src/Illuminate/Support/Traits/InteractsWithData.php b/src/Illuminate/Support/Traits/InteractsWithData.php index 99616e6c9058..168390e22d26 100644 --- a/src/Illuminate/Support/Traits/InteractsWithData.php +++ b/src/Illuminate/Support/Traits/InteractsWithData.php @@ -178,6 +178,32 @@ public function whenFilled($key, callable $callback, ?callable $default = null) return $this; } + /** + * Apply the callback if the instance contains a valid enum value for the given key. + * + * @param string $key + * @param class-string<\BackedEnum> $enumClass + * @param callable $callback + * @param callable|null $default + * @return $this|mixed + */ + public function whenEnum($key, string $enumClass, callable $callback, ?callable $default = null) + { + if ($this->filled($key) && $this->isBackedEnum($enumClass)) { + $value = $enumClass::tryFrom(data_get($this->all(), $key)); + + if ($value !== null) { + return $callback($value) ?: $this; + } + } + + if ($default) { + return $default(); + } + + return $this; + } + /** * Determine if the instance is missing a given key. * diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index d26dfc4f4eab..77ee7bd4237a 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -453,6 +453,37 @@ public function testWhenFilledMethod() $this->assertTrue($bar); } + public function testWhenEnumMethod() + { + $request = Request::create('/', 'GET', ['status' => 'test', 'invalid' => 'invalid', 'empty' => '']); + + $status = $invalid = $empty = $missing = $default = false; + + $request->whenEnum('status', TestEnumBacked::class, function ($value) use (&$status) { + $status = $value; + }); + + $request->whenEnum('invalid', TestEnumBacked::class, function ($value) use (&$invalid) { + $invalid = $value; + }); + + $request->whenEnum('empty', TestEnumBacked::class, function ($value) use (&$empty) { + $empty = $value; + }); + + $request->whenEnum('missing', TestEnumBacked::class, function ($value) use (&$missing) { + $missing = $value; + }, function () use (&$default) { + $default = true; + }); + + $this->assertSame(TestEnumBacked::test, $status); + $this->assertFalse($invalid); + $this->assertFalse($empty); + $this->assertFalse($missing); + $this->assertTrue($default); + } + public function testMissingMethod() { $request = Request::create('/', 'GET', ['name' => 'Taylor', 'age' => '', 'city' => null]); diff --git a/tests/Support/ValidatedInputTest.php b/tests/Support/ValidatedInputTest.php index d6688d41f9be..2a29a476ab2f 100644 --- a/tests/Support/ValidatedInputTest.php +++ b/tests/Support/ValidatedInputTest.php @@ -231,6 +231,37 @@ public function test_when_filled_method() $this->assertFalse($bar); } + public function test_when_enum_method() + { + $input = new ValidatedInput(['status' => 'Hello world', 'invalid' => 'invalid', 'age' => '']); + + $status = $invalid = $age = $missing = $default = false; + + $input->whenEnum('status', StringBackedEnum::class, function ($value) use (&$status) { + $status = $value; + }); + + $input->whenEnum('invalid', StringBackedEnum::class, function ($value) use (&$invalid) { + $invalid = $value; + }); + + $input->whenEnum('age', StringBackedEnum::class, function ($value) use (&$age) { + $age = $value; + }); + + $input->whenEnum('missing', StringBackedEnum::class, function ($value) use (&$missing) { + $missing = $value; + }, function () use (&$default) { + $default = true; + }); + + $this->assertSame(StringBackedEnum::HELLO_WORLD, $status); + $this->assertFalse($invalid); + $this->assertFalse($age); + $this->assertFalse($missing); + $this->assertTrue($default); + } + public function test_missing_method() { $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); From 47335d4339d36c395fa49b506ac059b02d5c865c Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:28:42 +0000 Subject: [PATCH 575/596] Update facade docblocks --- src/Illuminate/Support/Facades/Request.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 411ad9739f31..bf878d5d60c6 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -165,6 +165,7 @@ * @method static bool isNotFilled(string|array $key) * @method static bool anyFilled(string|array $keys) * @method static \Illuminate\Http\Request|mixed whenFilled(string $key, callable $callback, callable|null $default = null) + * @method static \Illuminate\Http\Request|mixed whenEnum(string $key, string $enumClass, callable $callback, callable|null $default = null) * @method static bool missing(string|array $key) * @method static \Illuminate\Http\Request|mixed whenMissing(string $key, callable $callback, callable|null $default = null) * @method static \Illuminate\Support\Stringable str(string $key, mixed $default = null) From 7c851385bceb7c02ac5cf23c7836cd82669b0ec9 Mon Sep 17 00:00:00 2001 From: Jack Bayliss Date: Thu, 11 Jun 2026 21:17:02 +0100 Subject: [PATCH 576/596] [13.x] Add array to supported maintainance mode drivers doc (#60490) * Update app.php * swap --- config/app.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/app.php b/config/app.php index 1ced8bef0a14..f2d907ecff96 100644 --- a/config/app.php +++ b/config/app.php @@ -143,7 +143,7 @@ | manage Laravel's "maintenance mode" status. The "cache" driver will | allow maintenance mode to be controlled across multiple machines. | - | Supported drivers: "file", "cache" + | Supported drivers: "file", "cache", "array" | */ From 3da7d837ba9f59873b7e847ba51821eb53b83f0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:40:54 -0500 Subject: [PATCH 577/596] Bump esbuild, @tailwindcss/vite and vite in /src/Illuminate/Foundation/resources/exceptions/renderer (#60501) * Bump esbuild, @tailwindcss/vite and vite Removes [esbuild](https://github.com/evanw/esbuild). It's no longer used after updating ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together. Removes `esbuild` Updates `@tailwindcss/vite` from 4.1.18 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite) Updates `vite` from 7.3.2 to 8.0.16 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite) --- updated-dependencies: - dependency-name: esbuild dependency-version: dependency-type: indirect - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.1 dependency-type: direct:development - dependency-name: vite dependency-version: 8.0.16 dependency-type: direct:development ... Signed-off-by: dependabot[bot] * Update Assets --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../exceptions/renderer/dist/scripts.js | 52 +- .../exceptions/renderer/dist/styles.css | 3 +- .../exceptions/renderer/package-lock.json | 1286 ++++++----------- .../exceptions/renderer/package.json | 4 +- 4 files changed, 456 insertions(+), 889 deletions(-) diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/dist/scripts.js b/src/Illuminate/Foundation/resources/exceptions/renderer/dist/scripts.js index 7569eff73a33..0c9853507a37 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/dist/scripts.js +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/dist/scripts.js @@ -1,21 +1,21 @@ -var lr=!1,ur=!1,ot=[],pr=-1,Wr=!1;function Ql(e){tu(e)}function Jl(){Wr=!0}function eu(){Wr=!1,io()}function tu(e){ot.includes(e)||ot.push(e),io()}function nu(e){let t=ot.indexOf(e);t!==-1&&t>pr&&ot.splice(t,1)}function io(){if(!ur&&!lr){if(Wr)return;lr=!0,queueMicrotask(au)}}function au(){lr=!1,ur=!0;for(let e=0;ee.effect(t,{scheduler:n=>{dr?Ql(n):n()}}),so=e.raw}function Mi(e){_t=e}function su(e){let t=()=>{};return[a=>{let r=_t(a);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(i=>i())}),e._x_effects.add(r),t=()=>{r!==void 0&&(e._x_effects.delete(r),Bt(r))},r},()=>{t()}]}function oo(e,t){let n=!0,a,r=_t(()=>{let i=e();if(JSON.stringify(i),!n&&(typeof i=="object"||i!==a)){let s=a;queueMicrotask(()=>{t(i,s)})}a=i,n=!1});return()=>Bt(r)}async function ou(e){Jl();try{await e(),await Promise.resolve()}finally{eu()}}var co=[],lo=[],uo=[];function cu(e){uo.push(e)}function Vr(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,lo.push(t))}function po(e){co.push(e)}function mo(e,t,n){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(n)}function ho(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([n,a])=>{(t===void 0||t.includes(n))&&(a.forEach(r=>r()),delete e._x_attributeCleanups[n])})}function lu(e){for(e._x_effects?.forEach(nu);e._x_cleanups?.length;)e._x_cleanups.pop()()}var Zr=new MutationObserver(Qr),Yr=!1;function Xr(){Zr.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),Yr=!0}function go(){uu(),Zr.disconnect(),Yr=!1}var tn=[];function uu(){let e=Zr.takeRecords();tn.push(()=>e.length>0&&Qr(e));let t=tn.length;queueMicrotask(()=>{if(tn.length===t)for(;tn.length>0;)tn.shift()()})}function z(e){if(!Yr)return e();go();let t=e();return Xr(),t}var Kr=!1,la=[];function pu(){Kr=!0}function du(){Kr=!1,Qr(la),la=[]}function Qr(e){if(Kr){la=la.concat(e);return}let t=[],n=new Set,a=new Map,r=new Map;for(let i=0;i{s.nodeType===1&&s._x_marker&&n.add(s)}),e[i].addedNodes.forEach(s=>{if(s.nodeType===1){if(n.has(s)){n.delete(s);return}s._x_marker||t.push(s)}})),e[i].type==="attributes")){let s=e[i].target,o=e[i].attributeName,c=e[i].oldValue,l=()=>{a.has(s)||a.set(s,[]),a.get(s).push({name:o,value:s.getAttribute(o)})},u=()=>{r.has(s)||r.set(s,[]),r.get(s).push(o)};s.hasAttribute(o)&&c===null?l():s.hasAttribute(o)?(u(),l()):u()}r.forEach((i,s)=>{ho(s,i)}),a.forEach((i,s)=>{co.forEach(o=>o(s,i))});for(let i of n)t.some(s=>s.contains(i))||lo.forEach(s=>s(i));for(let i of t)i.isConnected&&uo.forEach(s=>s(i));t=null,n=null,a=null,r=null}function fo(e){return mt(dt(e))}function Fn(e,t,n){return e._x_dataStack=[t,...dt(n||e)],()=>{e._x_dataStack=e._x_dataStack.filter(a=>a!==t)}}function dt(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?dt(e.host):e.parentNode?dt(e.parentNode):[]}function mt(e){return new Proxy({objects:e},mu)}var mu={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(n=>Object.prototype.hasOwnProperty.call(n,t)||Reflect.has(n,t))},get({objects:e},t,n){return t=="toJSON"?hu:Reflect.get(e.find(a=>Reflect.has(a,t))||{},t,n)},set({objects:e},t,n,a){const r=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],i=Object.getOwnPropertyDescriptor(r,t);return i?.set&&i?.get?i.set.call(a,n)||!0:Reflect.set(r,t,n)}};function hu(){return Reflect.ownKeys(this).reduce((t,n)=>(t[n]=Reflect.get(this,n),t),{})}function Jr(e){let t=a=>typeof a=="object"&&!Array.isArray(a)&&a!==null,n=(a,r="")=>{Object.entries(Object.getOwnPropertyDescriptors(a)).forEach(([i,{value:s,enumerable:o}])=>{if(o===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=r===""?i:`${r}.${i}`;typeof s=="object"&&s!==null&&s._x_interceptor?a[i]=s.initialize(e,c,i):t(s)&&s!==a&&!(s instanceof Element)&&n(s,c)})};return n(e)}function bo(e,t=()=>{}){let n={initialValue:void 0,_x_interceptor:!0,initialize(a,r,i){return e(this.initialValue,()=>gu(a,r),s=>mr(a,r,s),r,i)}};return t(n),a=>{if(typeof a=="object"&&a!==null&&a._x_interceptor){let r=n.initialize.bind(n);n.initialize=(i,s,o)=>{let c=a.initialize(i,s,o);return n.initialValue=c,r(i,s,o)}}else n.initialValue=a;return n}}function gu(e,t){return t.split(".").reduce((n,a)=>n[a],e)}function mr(e,t,n){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=n;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mr(e[t[0]],t.slice(1),n)}}var _o={};function ve(e,t){_o[e]=t}function fn(e,t){let n=fu(t);return Object.entries(_o).forEach(([a,r])=>{Object.defineProperty(e,`$${a}`,{get(){return r(t,n)},enumerable:!1})}),e}function fu(e){let[t,n]=Fo(e),a={interceptor:bo,...t};return Vr(e,n),a}function bu(e,t,n,...a){try{return n(...a)}catch(r){bn(r,e,t)}}function bn(...e){return yo(...e)}var yo=yu;function _u(e){yo=e}function yu(e,t,n=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:n}),console.warn(`Alpine Expression Error: ${e.message} +var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r},n=!1,r=!1,i=[],a=-1,o=!1;function s(e){u(e)}function c(){o=!0}function l(){o=!1,f()}function u(e){i.includes(e)||i.push(e),f()}function d(e){let t=i.indexOf(e);t!==-1&&t>a&&i.splice(t,1)}function f(){if(!r&&!n){if(o)return;n=!0,queueMicrotask(p)}}function p(){n=!1,r=!0;for(let e=0;ee.effect(t,{scheduler:e=>{v?s(e):e()}}),_=e.raw}function x(e){h=e}function S(e){let t=()=>{};return[n=>{let r=h(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(e=>e())}),e._x_effects.add(r),t=()=>{r!==void 0&&(e._x_effects.delete(r),g(r))},r},()=>{t()}]}function C(e,t){let n=!0,r,i=h(()=>{let i=e();if(JSON.stringify(i),!n&&(typeof i==`object`||i!==r)){let e=r;queueMicrotask(()=>{t(i,e)})}r=i,n=!1});return()=>g(i)}async function w(e){c();try{await e(),await Promise.resolve()}finally{l()}}var T=[],E=[],D=[];function O(e){D.push(e)}function k(e,t){typeof t==`function`?(e._x_cleanups||=[],e._x_cleanups.push(t)):(t=e,E.push(t))}function ee(e){T.push(e)}function A(e,t,n){e._x_attributeCleanups||={},e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(n)}function j(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([n,r])=>{(t===void 0||t.includes(n))&&(r.forEach(e=>e()),delete e._x_attributeCleanups[n])})}function M(e){for(e._x_effects?.forEach(d);e._x_cleanups?.length;)e._x_cleanups.pop()()}var te=new MutationObserver(de),ne=!1;function re(){te.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ne=!0}function ie(){oe(),te.disconnect(),ne=!1}var ae=[];function oe(){let e=te.takeRecords();ae.push(()=>e.length>0&&de(e));let t=ae.length;queueMicrotask(()=>{if(ae.length===t)for(;ae.length>0;)ae.shift()()})}function N(e){if(!ne)return e();ie();let t=e();return re(),t}var se=!1,ce=[];function le(){se=!0}function ue(){se=!1,de(ce),ce=[]}function de(e){if(se){ce=ce.concat(e);return}let t=[],n=new Set,r=new Map,i=new Map;for(let a=0;a{e.nodeType===1&&e._x_marker&&n.add(e)}),e[a].addedNodes.forEach(e=>{if(e.nodeType===1){if(n.has(e)){n.delete(e);return}e._x_marker||t.push(e)}})),e[a].type===`attributes`)){let t=e[a].target,n=e[a].attributeName,o=e[a].oldValue,s=()=>{r.has(t)||r.set(t,[]),r.get(t).push({name:n,value:t.getAttribute(n)})},c=()=>{i.has(t)||i.set(t,[]),i.get(t).push(n)};t.hasAttribute(n)&&o===null?s():t.hasAttribute(n)?(c(),s()):c()}i.forEach((e,t)=>{j(t,e)}),r.forEach((e,t)=>{T.forEach(n=>n(t,e))});for(let e of n)t.some(t=>t.contains(e))||E.forEach(t=>t(e));for(let e of t)e.isConnected&&D.forEach(t=>t(e));t=null,n=null,r=null,i=null}function fe(e){return he(me(e))}function pe(e,t,n){return e._x_dataStack=[t,...me(n||e)],()=>{e._x_dataStack=e._x_dataStack.filter(e=>e!==t)}}function me(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot==`function`&&e instanceof ShadowRoot?me(e.host):e.parentNode?me(e.parentNode):[]}function he(e){return new Proxy({objects:e},ge)}var ge={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(e=>Object.keys(e))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(e=>Object.prototype.hasOwnProperty.call(e,t)||Reflect.has(e,t))},get({objects:e},t,n){return t==`toJSON`?_e:Reflect.get(e.find(e=>Reflect.has(e,t))||{},t,n)},set({objects:e},t,n,r){let i=e.find(e=>Object.prototype.hasOwnProperty.call(e,t))||e[e.length-1],a=Object.getOwnPropertyDescriptor(i,t);return a?.set&&a?.get?a.set.call(r,n)||!0:Reflect.set(i,t,n)}};function _e(){return Reflect.ownKeys(this).reduce((e,t)=>(e[t]=Reflect.get(this,t),e),{})}function ve(e){let t=e=>typeof e==`object`&&!Array.isArray(e)&&e!==null,n=(r,i=``)=>{Object.entries(Object.getOwnPropertyDescriptors(r)).forEach(([a,{value:o,enumerable:s}])=>{if(s===!1||o===void 0||typeof o==`object`&&o&&o.__v_skip)return;let c=i===``?a:`${i}.${a}`;typeof o==`object`&&o&&o._x_interceptor?r[a]=o.initialize(e,c,a):t(o)&&o!==r&&!(o instanceof Element)&&n(o,c)})};return n(e)}function ye(e,t=()=>{}){let n={initialValue:void 0,_x_interceptor:!0,initialize(t,n,r){return e(this.initialValue,()=>be(t,n),e=>xe(t,n,e),n,r)}};return t(n),e=>{if(typeof e==`object`&&e&&e._x_interceptor){let t=n.initialize.bind(n);n.initialize=(r,i,a)=>(n.initialValue=e.initialize(r,i,a),t(r,i,a))}else n.initialValue=e;return n}}function be(e,t){return t.split(`.`).reduce((e,t)=>e[t],e)}function xe(e,t,n){if(typeof t==`string`&&(t=t.split(`.`)),t.length===1)e[t[0]]=n;else if(t.length===0)throw error;else if(e[t[0]])return xe(e[t[0]],t.slice(1),n);else return e[t[0]]={},xe(e[t[0]],t.slice(1),n)}var Se={};function P(e,t){Se[e]=t}function F(e,t){let n=Ce(t);return Object.entries(Se).forEach(([r,i])=>{Object.defineProperty(e,`$${r}`,{get(){return i(t,n)},enumerable:!1})}),e}function Ce(e){let[t,n]=tt(e),r={interceptor:ye,...t};return k(e,n),r}function we(e,t,n,...r){try{return n(...r)}catch(n){Te(n,e,t)}}function Te(...e){return Ee(...e)}var Ee=Oe;function De(e){Ee=e}function Oe(e,t,n=void 0){e=Object.assign(e??{message:`No error message given.`},{el:t,expression:n}),console.warn(`Alpine Expression Error: ${e.message} -${n?'Expression: "'+n+`" +${n?`Expression: "`+n+`" -`:""}`,t),setTimeout(()=>{throw e},0)}var Rt=!0;function vo(e){let t=Rt;Rt=!1;let n=e();return Rt=t,n}function ct(e,t,n={}){let a;return ne(e,t)(r=>a=r,n),a}function ne(...e){return wo(...e)}var wo=ko;function vu(e){wo=e}var xo;function wu(e){xo=e}function ko(e,t){let n={};fn(n,e);let a=[n,...dt(e)],r=typeof t=="function"?xu(a,t):Cu(a,t,e);return bu.bind(null,e,t,r)}function xu(e,t){return(n=()=>{},{scope:a={},params:r=[],context:i}={})=>{if(!Rt){_n(n,t,mt([a,...e]),r);return}let s=t.apply(mt([a,...e]),r);_n(n,s)}}var za={};function ku(e,t){if(za[e])return za[e];let n=Object.getPrototypeOf(async function(){}).constructor,a=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,i=(()=>{try{let s=new n(["__self","scope"],`with (scope) { __self.result = ${a} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return bn(s,t,e),Promise.resolve()}})();return za[e]=i,i}function Cu(e,t,n){let a=ku(t,n);return(r=()=>{},{scope:i={},params:s=[],context:o}={})=>{a.result=void 0,a.finished=!1;let c=mt([i,...e]);if(typeof a=="function"){let l=a.call(o,a,c).catch(u=>bn(u,n,t));a.finished?(_n(r,a.result,c,s,n),a.result=void 0):l.then(u=>{_n(r,u,c,s,n)}).catch(u=>bn(u,n,t)).finally(()=>a.result=void 0)}}}function _n(e,t,n,a,r){if(Rt&&typeof t=="function"){let i=t.apply(n,a);i instanceof Promise?i.then(s=>_n(e,s,n,a)).catch(s=>bn(s,r,t)):e(i)}else typeof t=="object"&&t instanceof Promise?t.then(i=>e(i)):e(t)}function Eu(...e){return xo(...e)}function Fu(e,t,n={}){let a={};fn(a,e);let r=[a,...dt(e)],i=mt([n.scope??{},...r]),s=n.params??[];if(t.includes("await")){let o=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t;return new o(["scope"],`with (scope) { let __result = ${c}; return __result }`).call(n.context,i)}else{let o=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(()=>{ ${t} })()`:t,l=new Function(["scope"],`with (scope) { let __result = ${o}; return __result }`).call(n.context,i);return typeof l=="function"&&Rt?l.apply(i,s):l}}var ei="x-";function Ut(e=""){return ei+e}function $u(e){ei=e}var ua={};function W(e,t){return ua[e]=t,{before(n){if(!ua[n]){console.warn(String.raw`Cannot find directive \`${n}\`. \`${e}\` will use the default order of execution`);return}const a=rt.indexOf(n);rt.splice(a>=0?a:rt.indexOf("DEFAULT"),0,e)}}}function ju(e){return Object.keys(ua).includes(e)}function ti(e,t,n){if(t=Array.from(t),e._x_virtualDirectives){let i=Object.entries(e._x_virtualDirectives).map(([o,c])=>({name:o,value:c})),s=Co(i);i=i.map(o=>s.find(c=>c.name===o.name)?{name:`x-bind:${o.name}`,value:`"${o.value}"`}:o),t=t.concat(i)}let a={};return t.map(So((i,s)=>a[i]=s)).filter(To).map(Tu(a,n)).sort(Ru).map(i=>Au(e,i))}function Co(e){return Array.from(e).map(So()).filter(t=>!To(t))}var hr=!1,on=new Map,Eo=Symbol();function Su(e){hr=!0;let t=Symbol();Eo=t,on.set(t,[]);let n=()=>{for(;on.get(t).length;)on.get(t).shift()();on.delete(t)},a=()=>{hr=!1,n()};e(n),a()}function Fo(e){let t=[],n=o=>t.push(o),[a,r]=su(e);return t.push(r),[{Alpine:Ht,effect:a,cleanup:n,evaluateLater:ne.bind(ne,e),evaluate:ct.bind(ct,e)},()=>t.forEach(o=>o())]}function Au(e,t){let n=()=>{},a=ua[t.type]||n,[r,i]=Fo(e);mo(e,t.original,i);let s=()=>{e._x_ignore||e._x_ignoreSelf||(a.inline&&a.inline(e,t,r),a=a.bind(a,e,t,r),hr?on.get(Eo).push(a):a())};return s.runCleanups=i,s}var $o=(e,t)=>({name:n,value:a})=>(n.startsWith(e)&&(n=n.replace(e,t)),{name:n,value:a}),jo=e=>e;function So(e=()=>{}){return({name:t,value:n})=>{let{name:a,value:r}=Ao.reduce((i,s)=>s(i),{name:t,value:n});return a!==t&&e(a,t),{name:a,value:r}}}var Ao=[];function ni(e){Ao.push(e)}function To({name:e}){return Ro().test(e)}var Ro=()=>new RegExp(`^${ei}([^:^.]+)\\b`);function Tu(e,t){return({name:n,value:a})=>{n===a&&(a="");let r=n.match(Ro()),i=n.match(/:([a-zA-Z0-9\-_:]+)/),s=n.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],o=t||e[n]||n;return{type:r?r[1]:null,value:i?i[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:a,original:o}}}var gr="DEFAULT",rt=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",gr,"teleport"];function Ru(e,t){let n=rt.indexOf(e.type)===-1?gr:e.type,a=rt.indexOf(t.type)===-1?gr:t.type;return rt.indexOf(n)-rt.indexOf(a)}function ln(e,t,n={}){e.dispatchEvent(new CustomEvent(t,{detail:n,bubbles:!0,composed:!0,cancelable:!0}))}function ht(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(r=>ht(r,t));return}let n=!1;if(t(e,()=>n=!0),n)return;let a=e.firstElementChild;for(;a;)ht(a,t),a=a.nextElementSibling}function pe(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var qi=!1;function Ou(){qi&&pe("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),qi=!0,document.body||pe("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `