From 4e19d4e8faf501cedef0bc83afbea1b207e5e375 Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Tue, 20 Feb 2024 17:20:15 +0200 Subject: [PATCH 01/25] [10.x] Added getQualifiedMorphTypeName to MorphToMany (#50153) It is in addition to qualified names for MorphToMany relations. --- .../Database/Eloquent/Relations/MorphToMany.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 87b8e7816f9f..8cf113bd0f34 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -189,6 +189,16 @@ public function getMorphType() return $this->morphType; } + /** + * Get the fully qualified morph type for the relation. + * + * @return string + */ + public function getQualifiedMorphTypeName() + { + return $this->qualifyPivotColumn($this->morphType); + } + /** * Get the class name of the parent model. * From 8b08d8cd79f8093eb51a8c59e21647bedfbf05f2 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Tue, 20 Feb 2024 15:32:48 +0000 Subject: [PATCH 02/25] Update version to v10.45.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 3cb07986767a..a23f0a4a9272 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -40,7 +40,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.44.0'; + const VERSION = '10.45.0'; /** * The base path for the Laravel installation. From 465668622caef586f047e86247984deedb893314 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 20 Feb 2024 16:10:57 +0000 Subject: [PATCH 03/25] Update CHANGELOG --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 033086916d46..d20dd33a86fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.44.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.45.0...10.x) + +## [v10.45.0](https://github.com/laravel/framework/compare/v10.44.0...v10.45.0) - 2024-02-20 + +* [10.x] Update `Stringable` phpdoc by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/50075 +* [10.x] Allow `Collection::select()` to work on `ArrayAccess` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/50072 +* [10.x] Add `before` to the `PendingBatch` by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/50058 +* [10.x] Adjust rules call sequence by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50084 +* [10.x] Fixes `Illuminate\Support\Str::fromBase64()` return type by [@SamAsEnd](https://github.com/SamAsEnd) in https://github.com/laravel/framework/pull/50108 +* [10.x] Actually fix fromBase64 return type by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/50113 +* [10.x] Fix warning and deprecation for Str::api by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50114 +* [10.x] Mark model instanse as not exists on deleting MorphPivot relation. by [@dkulyk](https://github.com/dkulyk) in https://github.com/laravel/framework/pull/50135 +* [10.x] Adds Tappable and Conditionable to Relation class by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/50124 +* [10.x] Added getQualifiedMorphTypeName to MorphToMany by [@dkulyk](https://github.com/dkulyk) in https://github.com/laravel/framework/pull/50153 ## [v10.44.0](https://github.com/laravel/framework/compare/v10.43.0...v10.44.0) - 2024-02-13 From 51134d6802b6193ec4b0a24c45a03b2bae730327 Mon Sep 17 00:00:00 2001 From: Kuba Szymanowski Date: Tue, 20 Feb 2024 18:03:56 +0100 Subject: [PATCH 04/25] Fix typehint for ResetPassword::toMailUsing() (#50163) --- src/Illuminate/Auth/Notifications/ResetPassword.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Auth/Notifications/ResetPassword.php b/src/Illuminate/Auth/Notifications/ResetPassword.php index 1d8da41bd1a8..efb4573e8be2 100644 --- a/src/Illuminate/Auth/Notifications/ResetPassword.php +++ b/src/Illuminate/Auth/Notifications/ResetPassword.php @@ -25,7 +25,7 @@ class ResetPassword extends Notification /** * The callback that should be used to build the mail message. * - * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage)|null + * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable)|null */ public static $toMailCallback; @@ -114,7 +114,7 @@ public static function createUrlUsing($callback) /** * Set a callback that should be used when building the notification mail message. * - * @param \Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage $callback + * @param \Closure(mixed, string): (\Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable) $callback * @return void */ public static function toMailUsing($callback) From 08bf276de0efc9af130e10cb93ede827715543c3 Mon Sep 17 00:00:00 2001 From: Sjors Ottjes Date: Tue, 20 Feb 2024 21:17:36 +0100 Subject: [PATCH 05/25] fix Process::fake() not matching multi line commands (#50164) --- src/Illuminate/Process/PendingProcess.php | 2 +- tests/Process/ProcessTest.php | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 320172bd266c..810ae6e6ed17 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -348,7 +348,7 @@ public function withFakeHandlers(array $fakeHandlers) protected function fakeFor(string $command) { return collect($this->fakeHandlers) - ->first(fn ($handler, $pattern) => Str::is($pattern, $command)); + ->first(fn ($handler, $pattern) => $pattern === '*' || Str::is($pattern, $command)); } /** diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 9908b1a76de3..4557004e1d1a 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -148,6 +148,26 @@ public function testBasicProcessFake() $this->assertTrue($result->successful()); } + public function testBasicProcessFakeWithMultiLineCommand() + { + $factory = new Factory; + + $factory->preventStrayProcesses(); + + $factory->fake([ + '*' => 'The output', + ]); + + $result = $factory->run(<<<'COMMAND' + git clone --depth 1 \ + --single-branch \ + --branch main \ + git://some-url . + COMMAND); + + $this->assertSame(0, $result->exitCode()); + } + public function testProcessFakeExitCodes() { $factory = new Factory; From 6b98a716f9f66cca9ed43a3edb8fc3fd18997ec3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 21 Feb 2024 08:07:01 -0600 Subject: [PATCH 06/25] wip --- src/Illuminate/Database/Eloquent/Relations/Relation.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index cffb14bf5a25..7fea6b70d95b 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -10,14 +10,12 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\Query\Expression; -use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\Macroable; -use Illuminate\Support\Traits\Tappable; abstract class Relation implements BuilderContract { - use Conditionable, ForwardsCalls, Macroable, Tappable { + use ForwardsCalls, Macroable { Macroable::__call as macroCall; } From dcf5d1d722b84ad38a5e053289130b6962f830bd Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Wed, 21 Feb 2024 14:07:36 +0000 Subject: [PATCH 07/25] Update version to v10.45.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 a23f0a4a9272..ea5cbe2ca532 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -40,7 +40,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.45.0'; + const VERSION = '10.45.1'; /** * The base path for the Laravel installation. From abeec173e027cde01f9cd1ac5ca1c485cfbcf20a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Wed, 21 Feb 2024 15:12:51 +0100 Subject: [PATCH 08/25] add regression test (#50176) --- .../Database/EloquentMorphEagerLoadingTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Integration/Database/EloquentMorphEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphEagerLoadingTest.php index c251f0c105c7..8f6d4405bd63 100644 --- a/tests/Integration/Database/EloquentMorphEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphEagerLoadingTest.php @@ -95,6 +95,21 @@ public function testMorphLoadingMixedWithTrashedRelations() $this->assertTrue($action[1]->relationLoaded('target')); $this->assertInstanceOf(User::class, $action[1]->getRelation('target')); } + + public function testMorphWithTrashedRelationLazyLoading() + { + $deletedUser = User::forceCreate(['deleted_at' => now()]); + + $action = new Action; + $action->target()->associate($deletedUser)->save(); + + // model is already set via associate and not retrieved from the database + $this->assertInstanceOf(User::class, $action->target); + + $action->unsetRelation('target'); + + $this->assertInstanceOf(User::class, $action->target); + } } class Action extends Model From e708043384ab9887cfcb9aa20453f54c0d888855 Mon Sep 17 00:00:00 2001 From: Brenier Arnaud Date: Wed, 21 Feb 2024 15:17:53 +0100 Subject: [PATCH 09/25] [10.x] Arr::select not working when $keys is a string (#50169) * Arr::select not working when $keys is a string Adding a check about $keys type. If it is a string, transform it into an array. If not, it throws a foreach() error * add test --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Collections/Arr.php | 2 ++ tests/Support/SupportArrTest.php | 36 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index 291d3de39074..e75d87ae979e 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -516,6 +516,8 @@ public static function only($array, $keys) */ public static function select($array, $keys) { + $keys = static::wrap($keys); + return static::map($array, function ($item) use ($keys) { $result = []; diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index 05f6b743f3e1..119dd2a3bd65 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -1273,4 +1273,40 @@ public function testTake() 4, 5, 6, ], Arr::take($array, -3)); } + + public function testSelect() + { + $array = [ + [ + 'name' => 'Taylor', + 'role' => 'Developer', + 'age' => 1, + ], + [ + 'name' => 'Abigail', + 'role' => 'Infrastructure', + 'age' => 2, + ], + ]; + + $this->assertEquals([ + [ + 'name' => 'Taylor', + 'age' => 1, + ], + [ + 'name' => 'Abigail', + 'age' => 2, + ], + ], Arr::select($array, ['name', 'age'])); + + $this->assertEquals([ + [ + 'name' => 'Taylor', + ], + [ + 'name' => 'Abigail', + ], + ], Arr::select($array, 'name')); + } } From 500fb9fffcaecd7d8972e5e1f2a6023da7f578ac Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 21 Feb 2024 14:18:14 +0000 Subject: [PATCH 10/25] Apply fixes from StyleCI --- src/Illuminate/Collections/Arr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index e75d87ae979e..361808418ffa 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -517,7 +517,7 @@ public static function only($array, $keys) public static function select($array, $keys) { $keys = static::wrap($keys); - + return static::map($array, function ($item) use ($keys) { $result = []; From f7c57c47f677b3fcfa1c70c6c3c51d0f90866d31 Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Wed, 21 Feb 2024 17:19:17 +0200 Subject: [PATCH 11/25] Added passing loaded relationship to value callback (#50167) --- .../Http/Resources/ConditionallyLoadsAttributes.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php index 9940c3e0cea6..0f03ebda832b 100644 --- a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php +++ b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php @@ -266,15 +266,17 @@ protected function whenLoaded($relationship, $value = null, $default = null) return value($default); } + $loadedValue = $this->resource->{$relationship}; + if (func_num_args() === 1) { - return $this->resource->{$relationship}; + return $loadedValue; } - if ($this->resource->{$relationship} === null) { + if ($loadedValue === null) { return; } - return value($value); + return value($value, $loadedValue); } /** From e550f2bf06e4d2f44bf241d2861eea059904976f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 21 Feb 2024 19:58:47 +0000 Subject: [PATCH 12/25] [10.x] Fix optional charset and collation when creating database (#50168) * Fix optional charset and collation when creating database * Update MySqlGrammar.php * Re-use the var i made for this --- .../Database/Schema/Grammars/MySqlGrammar.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index d54e9a6fe218..26d97a7a5541 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -43,11 +43,21 @@ class MySqlGrammar extends Grammar */ public function compileCreateDatabase($name, $connection) { + $charset = $connection->getConfig('charset'); + $collation = $connection->getConfig('collation'); + + if (! $charset || ! $collation) { + return sprintf( + 'create database %s', + $this->wrapValue($name), + ); + } + return sprintf( 'create database %s default character set %s default collate %s', $this->wrapValue($name), - $this->wrapValue($connection->getConfig('charset')), - $this->wrapValue($connection->getConfig('collation')), + $this->wrapValue($charset), + $this->wrapValue($collation), ); } From ae606ae6004c1ae7dca922fe37ab3f9e517c197f Mon Sep 17 00:00:00 2001 From: "S.a Mahmoudzadeh" <36761585+saMahmoudzadeh@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:22:10 +0330 Subject: [PATCH 13/25] [10.x] update doc block in PendingProcess.php (#50198) * refactor(PendingProcess): update doc block for start method * refactor(PendingProcess): update doc block for resolveAsynchronousFake method --- src/Illuminate/Process/PendingProcess.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 810ae6e6ed17..53ffdd130def 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -266,8 +266,10 @@ public function run(array|string $command = null, callable $output = null) * Start the process in the background. * * @param array|string|null $command - * @param callable $output + * @param callable|null $output * @return \Illuminate\Process\InvokedProcess + * + * @throws \RuntimeException */ public function start(array|string $command = null, callable $output = null) { @@ -382,6 +384,8 @@ protected function resolveSynchronousFake(string $command, Closure $fake) * @param callable|null $output * @param \Closure $fake * @return \Illuminate\Process\FakeInvokedProcess + * + * @throw \LogicException */ protected function resolveAsynchronousFake(string $command, ?callable $output, Closure $fake) { From e0be50a7b770c46b522377cb46318e06e312014e Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Thu, 22 Feb 2024 14:52:40 +0000 Subject: [PATCH 14/25] Update facade docblocks --- src/Illuminate/Support/Facades/Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index 1df17ba7e7a9..43b5b93a6578 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -17,7 +17,7 @@ * @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 $output = null) + * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) * @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) * @method static \Illuminate\Process\PendingProcess|mixed unless(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) From 4ebce1e453d6f2c5c23905751f781ec2a6edf0b6 Mon Sep 17 00:00:00 2001 From: "S.a Mahmoudzadeh" <36761585+saMahmoudzadeh@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:08:25 +0330 Subject: [PATCH 15/25] [10.x] Fix Accepting nullable Parameters, updated doc block, and null pointer exception handling in batchable trait (#50209) * fix(Reachable Trait): fix parameters type and doc block in With withFakeBatch method * fix(Reachable Trait): fix null pointer exception * fix(Batchable Trait): fix parameters type and doc block in With withFakeBatch method * fix(Batchable Trait): fix null pointer exception * Update Batchable.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/Batchable.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Bus/Batchable.php b/src/Illuminate/Bus/Batchable.php index 0b082700f8a2..5cf5706070e9 100644 --- a/src/Illuminate/Bus/Batchable.php +++ b/src/Illuminate/Bus/Batchable.php @@ -35,7 +35,7 @@ public function batch() } if ($this->batchId) { - return Container::getInstance()->make(BatchRepository::class)->find($this->batchId); + return Container::getInstance()->make(BatchRepository::class)?->find($this->batchId); } } @@ -74,7 +74,7 @@ public function withBatchId(string $batchId) * @param int $failedJobs * @param array $failedJobIds * @param array $options - * @param \Carbon\CarbonImmutable $createdAt + * @param \Carbon\CarbonImmutable|null $createdAt * @param \Carbon\CarbonImmutable|null $cancelledAt * @param \Carbon\CarbonImmutable|null $finishedAt * @return array{0: $this, 1: \Illuminate\Support\Testing\Fakes\BatchFake} @@ -86,7 +86,7 @@ public function withFakeBatch(string $id = '', int $failedJobs = 0, array $failedJobIds = [], array $options = [], - CarbonImmutable $createdAt = null, + ?CarbonImmutable $createdAt = null, ?CarbonImmutable $cancelledAt = null, ?CarbonImmutable $finishedAt = null) { From dda69ba7208a3093390594ba2f6095041d2b18c2 Mon Sep 17 00:00:00 2001 From: driesvints Date: Fri, 23 Feb 2024 16:14:41 +0000 Subject: [PATCH 16/25] Update CHANGELOG --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d20dd33a86fe..c48b7d1f94ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.45.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.45.1...10.x) + +## [v10.45.1](https://github.com/laravel/framework/compare/v10.45.0...v10.45.1) - 2024-02-21 + +### What's Changed + +* Fix typehint for ResetPassword::toMailUsing() by [@KKSzymanowski](https://github.com/KKSzymanowski) in https://github.com/laravel/framework/pull/50163 +* [10.x] Fix Process::fake() never matching multi-line commands by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/50164 + +**Full Changelog**: https://github.com/laravel/framework/compare/v10.45.0...v10.45.1 ## [v10.45.0](https://github.com/laravel/framework/compare/v10.44.0...v10.45.0) - 2024-02-20 From d309d71f0102719be7e7984e4462889d2ab24f48 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Fri, 23 Feb 2024 17:16:58 +0100 Subject: [PATCH 17/25] Update CHANGELOG.md --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c48b7d1f94ab..6ba419c3be85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,9 @@ ## [v10.45.1](https://github.com/laravel/framework/compare/v10.45.0...v10.45.1) - 2024-02-21 -### What's Changed - * Fix typehint for ResetPassword::toMailUsing() by [@KKSzymanowski](https://github.com/KKSzymanowski) in https://github.com/laravel/framework/pull/50163 * [10.x] Fix Process::fake() never matching multi-line commands by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/50164 -**Full Changelog**: https://github.com/laravel/framework/compare/v10.45.0...v10.45.1 - ## [v10.45.0](https://github.com/laravel/framework/compare/v10.44.0...v10.45.0) - 2024-02-20 * [10.x] Update `Stringable` phpdoc by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/50075 From 56250738b43f0ff3e20a3596ac2caa0b589a1aac Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Fri, 23 Feb 2024 17:33:02 +0100 Subject: [PATCH 18/25] Fix release notes --- .github/workflows/releases.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 43c7f2611a4a..e6177b2222a8 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -51,8 +51,8 @@ jobs: - name: Cleanup release notes run: | - sed -i '/## What'"'"'s Changed/d' ${{ steps.notes.outputs.release-notes }} - sed -i '/## New Contributors/,//d' ${{ steps.notes.outputs.release-notes }} + sed -i '/## What/d' ${{ steps.notes.outputs.release-notes }} + sed -i '/## New Contributors/,$d' ${{ steps.notes.outputs.release-notes }} - name: Create release uses: softprops/action-gh-release@v1 From 30324cf06d1f34fe359c5e458bcbb8da5413db71 Mon Sep 17 00:00:00 2001 From: Liam Duckett <116881406+liamduckett@users.noreply.github.com> Date: Sun, 25 Feb 2024 14:18:43 +0000 Subject: [PATCH 19/25] Make GuardsAttributes fillable property DocBlock more specific (#50229) --- src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index b7e0d7dea8c0..f7d4c9ff538d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -7,7 +7,7 @@ trait GuardsAttributes /** * The attributes that are mass assignable. * - * @var array + * @var array */ protected $fillable = []; From 8d47be393e43ffeacd49556471110454f868da5f Mon Sep 17 00:00:00 2001 From: Anton5360 <72033639+Anton5360@users.noreply.github.com> Date: Sun, 25 Feb 2024 07:30:59 -0700 Subject: [PATCH 20/25] [10.x] Add only and except methods to Enum validation rule (#50226) * Implement only and except logic * Cover only and except logic with tests * Fix code styling * Fix code styling * Improve php doc * Fix code styling * Fix code styling * formatting * fix visibility * fix type hints * remove type hint * fix type hints --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Validation/Rules/Enum.php | 62 ++++++++++++++- tests/Validation/ValidationEnumRuleTest.php | 88 +++++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Enum.php b/src/Illuminate/Validation/Rules/Enum.php index d66a16d126bc..42d9e6f012d2 100644 --- a/src/Illuminate/Validation/Rules/Enum.php +++ b/src/Illuminate/Validation/Rules/Enum.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidatorAwareRule; +use Illuminate\Support\Arr; use TypeError; class Enum implements Rule, ValidatorAwareRule @@ -22,6 +23,20 @@ class Enum implements Rule, ValidatorAwareRule */ protected $validator; + /** + * The cases that should be considered valid. + * + * @var array + */ + protected $only = []; + + /** + * The cases that should be considered invalid. + * + * @var array + */ + protected $except = []; + /** * Create a new rule instance. * @@ -43,7 +58,7 @@ public function __construct($type) public function passes($attribute, $value) { if ($value instanceof $this->type) { - return true; + return $this->isDesirable($value); } if (is_null($value) || ! enum_exists($this->type) || ! method_exists($this->type, 'tryFrom')) { @@ -51,12 +66,55 @@ public function passes($attribute, $value) } try { - return ! is_null($this->type::tryFrom($value)); + $value = $this->type::tryFrom($value); + + return ! is_null($value) && $this->isDesirable($value); } catch (TypeError) { return false; } } + /** + * Specify the cases that should be considered valid. + * + * @param \UnitEnum[]|\UnitEnum $values + * @return $this + */ + public function only($values) + { + $this->only = Arr::wrap($values); + + return $this; + } + + /** + * Specify the cases that should be considered invalid. + * + * @param \UnitEnum[]|\UnitEnum $values + * @return $this + */ + public function except($values) + { + $this->except = Arr::wrap($values); + + return $this; + } + + /** + * Determine if the given case is a valid case based on the only / except values. + * + * @param mixed $value + * @return bool + */ + protected function isDesirable($value) + { + return match (true) { + ! empty($this->only) => in_array(needle: $value, haystack: $this->only, strict: true), + ! empty($this->except) => ! in_array(needle: $value, haystack: $this->except, strict: true), + default => true, + }; + } + /** * Get the validation error message. * diff --git a/tests/Validation/ValidationEnumRuleTest.php b/tests/Validation/ValidationEnumRuleTest.php index 1c14c766f262..beffc1b314b1 100644 --- a/tests/Validation/ValidationEnumRuleTest.php +++ b/tests/Validation/ValidationEnumRuleTest.php @@ -78,6 +78,84 @@ public function testValidationFailsWhenProvidingNoExistingCases() $this->assertEquals(['The selected status is invalid.'], $v->messages()->get('status')); } + public function testValidationPassesForAllCasesUntilEitherOnlyOrExceptIsPassed() + { + $v = new Validator( + resolve('translator'), + [ + 'status_1' => PureEnum::one, + 'status_2' => PureEnum::two, + 'status_3' => IntegerStatus::done->value, + ], + [ + 'status_1' => new Enum(PureEnum::class), + 'status_2' => (new Enum(PureEnum::class))->only([])->except([]), + 'status_3' => new Enum(IntegerStatus::class), + ], + ); + + $this->assertTrue($v->passes()); + } + + /** + * @dataProvider conditionalCasesDataProvider + */ + public function testValidationPassesWhenOnlyCasesProvided( + IntegerStatus|int $enum, + array|IntegerStatus $only, + bool $expected + ) { + $v = new Validator( + resolve('translator'), + [ + 'status' => $enum, + ], + [ + 'status' => (new Enum(IntegerStatus::class))->only($only), + ], + ); + + $this->assertSame($expected, $v->passes()); + } + + /** + * @dataProvider conditionalCasesDataProvider + */ + public function testValidationPassesWhenExceptCasesProvided( + int|IntegerStatus $enum, + array|IntegerStatus $except, + bool $expected + ) { + $v = new Validator( + resolve('translator'), + [ + 'status' => $enum, + ], + [ + 'status' => (new Enum(IntegerStatus::class))->except($except), + ], + ); + + $this->assertSame($expected, $v->fails()); + } + + public function testOnlyHasHigherOrderThanExcept() + { + $v = new Validator( + resolve('translator'), + [ + 'status' => PureEnum::one, + ], + [ + 'status' => (new Enum(PureEnum::class)) + ->only(PureEnum::one) + ->except(PureEnum::one), + ], + ); + + $this->assertTrue($v->passes()); + } + public function testValidationFailsWhenProvidingDifferentType() { $v = new Validator( @@ -171,6 +249,16 @@ public function testValidationFailsWhenProvidingStringToIntegerType() $this->assertEquals(['The selected status is invalid.'], $v->messages()->get('status')); } + public static function conditionalCasesDataProvider(): array + { + return [ + [IntegerStatus::done, IntegerStatus::done, true], + [IntegerStatus::done, [IntegerStatus::done, IntegerStatus::pending], true], + [IntegerStatus::pending->value, [IntegerStatus::done, IntegerStatus::pending], true], + [IntegerStatus::done->value, IntegerStatus::pending, false], + ]; + } + protected function setUp(): void { $container = Container::getInstance(); From 7af199446137b8eaba53a3bea2edd028279c6176 Mon Sep 17 00:00:00 2001 From: Guilhem-DELAITRE <89917125+Guilhem-DELAITRE@users.noreply.github.com> Date: Sun, 25 Feb 2024 15:40:35 +0100 Subject: [PATCH 21/25] [10.x] Fixes on nesting operations performed while applying scopes. (#50207) * Add tests to emphasize the issues with nesting due to scope (nesting "or" groups but not "or not" groups and doubling first where clause negation) * Fix issues : also nest on "or not" clause, and don't repeat first where clause negation when nesting --- src/Illuminate/Database/Eloquent/Builder.php | 4 +-- .../DatabaseEloquentLocalScopesTest.php | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 5563b5477a2b..185781580684 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -1454,9 +1454,9 @@ protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice) // Here we'll check if the given subset of where clauses contains any "or" // booleans and in this case create a nested where expression. That way // we don't add any unnecessary nesting thus keeping the query clean. - if ($whereBooleans->contains('or')) { + if ($whereBooleans->contains(fn ($logicalOperator) => str_contains($logicalOperator, 'or'))) { $query->wheres[] = $this->createNestedWhere( - $whereSlice, $whereBooleans->first() + $whereSlice, str_replace(' not', '', $whereBooleans->first()) ); } else { $query->wheres = array_merge($query->wheres, $whereSlice); diff --git a/tests/Database/DatabaseEloquentLocalScopesTest.php b/tests/Database/DatabaseEloquentLocalScopesTest.php index 1d71f6f57661..d34a510f1e5f 100644 --- a/tests/Database/DatabaseEloquentLocalScopesTest.php +++ b/tests/Database/DatabaseEloquentLocalScopesTest.php @@ -61,6 +61,32 @@ public function testLocalScopesCanChained() $this->assertSame('select * from "table" where "active" = ? and "type" = ?', $query->toSql()); $this->assertEquals([true, 'foo'], $query->getBindings()); } + + public function testLocalScopeNestingDoesntDoubleFirstWhereClauseNegation() + { + $model = new EloquentLocalScopesTestModel; + $query = $model + ->newQuery() + ->whereNot('firstWhere', true) + ->orWhere('secondWhere', true) + ->active(); + + $this->assertSame('select * from "table" where (not "firstWhere" = ? or "secondWhere" = ?) and "active" = ?', $query->toSql()); + $this->assertEquals([true, true, true], $query->getBindings()); + } + + public function testLocalScopeNestingGroupsOrNotWhereClause() + { + $model = new EloquentLocalScopesTestModel; + $query = $model + ->newQuery() + ->where('firstWhere', true) + ->orWhereNot('secondWhere', true) + ->active(); + + $this->assertSame('select * from "table" where ("firstWhere" = ? or not "secondWhere" = ?) and "active" = ?', $query->toSql()); + $this->assertEquals([true, true, true], $query->getBindings()); + } } class EloquentLocalScopesTestModel extends Model From e45d4248840b5e43d20eec0c7db448e68f386130 Mon Sep 17 00:00:00 2001 From: Sebastien Armand Date: Sun, 25 Feb 2024 06:45:05 -0800 Subject: [PATCH 22/25] [10.x] Custom RateLimiter increase (#50197) * Allow ratelimiter custom increments * add test * formatting * formatting * Revert formatting * Formatting * formatting --------- Co-authored-by: Tim MacDonald Co-authored-by: Taylor Otwell --- src/Illuminate/Cache/RateLimiter.php | 17 +++++++++++++++-- tests/Cache/CacheRateLimiterTest.php | 19 +++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index 5f5fac0659b6..afdb9b25a208 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -105,13 +105,26 @@ public function tooManyAttempts($key, $maxAttempts) } /** - * Increment the counter for a given key for a given decay time. + * Increment (by 1) the counter for a given key for a given decay time. * * @param string $key * @param int $decaySeconds * @return int */ public function hit($key, $decaySeconds = 60) + { + return $this->increment($key, $decaySeconds); + } + + /** + * Increment the counter for a given key for a given decay time by a given amount. + * + * @param string $key + * @param int $decaySeconds + * @param int $amount + * @return int + */ + public function increment($key, $decaySeconds = 60, $amount = 1) { $key = $this->cleanRateLimiterKey($key); @@ -121,7 +134,7 @@ public function hit($key, $decaySeconds = 60) $added = $this->cache->add($key, 0, $decaySeconds); - $hits = (int) $this->cache->increment($key); + $hits = (int) $this->cache->increment($key, $amount); if (! $added && $hits == 1) { $this->cache->put($key, 1, $decaySeconds); diff --git a/tests/Cache/CacheRateLimiterTest.php b/tests/Cache/CacheRateLimiterTest.php index 660179256c84..0805f92af092 100644 --- a/tests/Cache/CacheRateLimiterTest.php +++ b/tests/Cache/CacheRateLimiterTest.php @@ -30,18 +30,29 @@ public function testHitProperlyIncrementsAttemptCount() $cache = m::mock(Cache::class); $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); - $cache->shouldReceive('increment')->once()->with('key')->andReturn(1); + $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $rateLimiter = new RateLimiter($cache); $rateLimiter->hit('key', 1); } + public function testIncrementProperlyIncrementsAttemptCount() + { + $cache = m::mock(Cache::class); + $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); + $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); + $cache->shouldReceive('increment')->once()->with('key', 5)->andReturn(5); + $rateLimiter = new RateLimiter($cache); + + $rateLimiter->increment('key', 1, 5); + } + public function testHitHasNoMemoryLeak() { $cache = m::mock(Cache::class); $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(false); - $cache->shouldReceive('increment')->once()->with('key')->andReturn(1); + $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $cache->shouldReceive('put')->once()->with('key', 1, 1); $rateLimiter = new RateLimiter($cache); @@ -83,7 +94,7 @@ public function testAttemptsCallbackReturnsTrue() $cache->shouldReceive('get')->once()->with('key', 0)->andReturn(0); $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturns(1); - $cache->shouldReceive('increment')->once()->with('key')->andReturn(1); + $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $executed = false; @@ -101,7 +112,7 @@ public function testAttemptsCallbackReturnsCallbackReturn() $cache->shouldReceive('get')->times(6)->with('key', 0)->andReturn(0); $cache->shouldReceive('add')->times(6)->with('key:timer', m::type('int'), 1); $cache->shouldReceive('add')->times(6)->with('key', 0, 1)->andReturns(1); - $cache->shouldReceive('increment')->times(6)->with('key')->andReturn(1); + $cache->shouldReceive('increment')->times(6)->with('key', 1)->andReturn(1); $rateLimiter = new RateLimiter($cache); From ca2ce7c0924c325590b7506b3d830f5b845ab9a8 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Sun, 25 Feb 2024 14:45:37 +0000 Subject: [PATCH 23/25] Update facade docblocks --- src/Illuminate/Support/Facades/RateLimiter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/RateLimiter.php b/src/Illuminate/Support/Facades/RateLimiter.php index 5ac2e5b36dd0..e8b3ab3fe4f5 100644 --- a/src/Illuminate/Support/Facades/RateLimiter.php +++ b/src/Illuminate/Support/Facades/RateLimiter.php @@ -8,6 +8,7 @@ * @method static mixed attempt(string $key, int $maxAttempts, \Closure $callback, int $decaySeconds = 60) * @method static bool tooManyAttempts(string $key, int $maxAttempts) * @method static int hit(string $key, int $decaySeconds = 60) + * @method static int increment(string $key, int $decaySeconds = 60, int $amount = 1) * @method static mixed attempts(string $key) * @method static mixed resetAttempts(string $key) * @method static int remaining(string $key, int $maxAttempts) From 5f8684b3dde621a3fd6b523e9f48f232de38611c Mon Sep 17 00:00:00 2001 From: Magnus Hauge Bakke Date: Sun, 25 Feb 2024 16:34:09 +0100 Subject: [PATCH 24/25] [10.x] Add Lateral Join to Query Builder (#50050) * Add lateral join support to Query Builder * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Query/Builder.php | 46 +++++++ .../Database/Query/Grammars/Grammar.php | 19 +++ .../Database/Query/Grammars/MySqlGrammar.php | 13 ++ .../Query/Grammars/PostgresGrammar.php | 13 ++ .../Query/Grammars/SqlServerGrammar.php | 15 +++ .../Database/Query/JoinLateralClause.php | 8 ++ tests/Database/DatabaseQueryBuilderTest.php | 111 +++++++++++++++++ .../Database/MySql/JoinLateralTest.php | 117 ++++++++++++++++++ .../Database/Postgres/JoinLateralTest.php | 104 ++++++++++++++++ .../Database/SqlServer/JoinLateralTest.php | 100 +++++++++++++++ 10 files changed, 546 insertions(+) create mode 100644 src/Illuminate/Database/Query/JoinLateralClause.php create mode 100644 tests/Integration/Database/MySql/JoinLateralTest.php create mode 100644 tests/Integration/Database/Postgres/JoinLateralTest.php create mode 100644 tests/Integration/Database/SqlServer/JoinLateralTest.php diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index ebb833707b11..6e31dbb0d263 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -586,6 +586,39 @@ public function joinSub($query, $as, $first, $operator = null, $second = null, $ return $this->join(new Expression($expression), $first, $operator, $second, $type, $where); } + /** + * Add a lateral join clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @param string $type + * @return $this + */ + public function joinLateral($query, string $as, string $type = 'inner') + { + [$query, $bindings] = $this->createSub($query); + + $expression = '('.$query.') as '.$this->grammar->wrapTable($as); + + $this->addBinding($bindings, 'join'); + + $this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression)); + + return $this; + } + + /** + * Add a lateral left join to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @return $this + */ + public function leftJoinLateral($query, string $as) + { + return $this->joinLateral($query, $as, 'left'); + } + /** * Add a left join to the query. * @@ -725,6 +758,19 @@ protected function newJoinClause(self $parentQuery, $type, $table) return new JoinClause($parentQuery, $type, $table); } + /** + * Get a new join lateral clause. + * + * @param \Illuminate\Database\Query\Builder $parentQuery + * @param string $type + * @param string $table + * @return \Illuminate\Database\Query\JoinLateralClause + */ + protected function newJoinLateralClause(self $parentQuery, $type, $table) + { + return new JoinLateralClause($parentQuery, $type, $table); + } + /** * Merge an array of where clauses and bindings. * diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index a03cdcb03346..b8eed21e69fd 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -7,6 +7,7 @@ use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use RuntimeException; @@ -182,10 +183,28 @@ protected function compileJoins(Builder $query, $joins) $tableAndNestedJoins = is_null($join->joins) ? $table : '('.$table.$nestedJoins.')'; + if ($join instanceof JoinLateralClause) { + return $this->compileJoinLateral($join, $tableAndNestedJoins); + } + return trim("{$join->type} join {$tableAndNestedJoins} {$this->compileWheres($join)}"); })->implode(' '); } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + * + * @throws \RuntimeException + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + throw new RuntimeException('This database engine does not support lateral joins.'); + } + /** * Compile the "where" portions of the query. * diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 1365d8efe620..3d900eeb3c24 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Str; class MySqlGrammar extends Grammar @@ -267,6 +268,18 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar return $sql.$columns; } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + return trim("{$join->type} join lateral {$expression} on true"); + } + /** * Prepare a JSON column being updated using the JSON_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index b39a20a0a5d0..c22720a05c7c 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -409,6 +410,18 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar return $sql.$columns; } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + return trim("{$join->type} join lateral {$expression} on true"); + } + /** * Prepares a JSON column being updated using the JSONB_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index f68722a64bce..062041c37d30 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -444,6 +445,20 @@ public function prepareBindingsForUpdate(array $bindings, array $values) ); } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + $type = $join->type == 'left' ? 'outer' : 'cross'; + + return trim("{$type} apply {$expression}"); + } + /** * Compile the SQL statement to define a savepoint. * diff --git a/src/Illuminate/Database/Query/JoinLateralClause.php b/src/Illuminate/Database/Query/JoinLateralClause.php new file mode 100644 index 000000000000..1be31d29626a --- /dev/null +++ b/src/Illuminate/Database/Query/JoinLateralClause.php @@ -0,0 +1,8 @@ +from('users')->rightJoinSub(['foo'], 'sub', 'users.id', '=', 'sub.id'); } + public function testJoinLateral() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral('select * from `contacts` where `contracts`.`user_id` = `users`.`id`', 'sub'); + $this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $sub = $this->getMySqlBuilder(); + $sub->getConnection()->shouldReceive('getDatabaseName'); + $eloquentBuilder = new EloquentBuilder($sub->from('contacts')->whereColumn('contracts.user_id', 'users.id')); + $builder->from('users')->joinLateral($eloquentBuilder, 'sub'); + $this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $sub1 = $this->getMySqlBuilder(); + $sub1->getConnection()->shouldReceive('getDatabaseName'); + $sub1 = $sub1->from('contacts')->whereColumn('contracts.user_id', 'users.id')->where('name', 'foo'); + + $sub2 = $this->getMySqlBuilder(); + $sub2->getConnection()->shouldReceive('getDatabaseName'); + $sub2 = $sub2->from('contacts')->whereColumn('contracts.user_id', 'users.id')->where('name', 'bar'); + + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral($sub1, 'sub1')->joinLateral($sub2, 'sub2'); + + $expected = 'select * from `users` '; + $expected .= 'inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id` and `name` = ?) as `sub1` on true '; + $expected .= 'inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id` and `name` = ?) as `sub2` on true'; + + $this->assertEquals($expected, $builder->toSql()); + $this->assertEquals(['foo', 'bar'], $builder->getRawBindings()['join']); + + $this->expectException(InvalidArgumentException::class); + $builder = $this->getMySqlBuilder(); + $builder->from('users')->joinLateral(['foo'], 'sub'); + } + + public function testJoinLateralSQLite() + { + $this->expectException(RuntimeException::class); + $builder = $this->getSQLiteBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub')->toSql(); + } + + public function testJoinLateralPostgres() + { + $builder = $this->getPostgresBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from "users" inner join lateral (select * from "contacts" where "contracts"."user_id" = "users"."id") as "sub" on true', $builder->toSql()); + } + + public function testJoinLateralSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from [users] cross apply (select * from [contacts] where [contracts].[user_id] = [users].[id]) as [sub]', $builder->toSql()); + } + + public function testJoinLateralWithPrefix() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->getGrammar()->setTablePrefix('prefix_'); + $builder->from('users')->joinLateral('select * from `contacts` where `contracts`.`user_id` = `users`.`id`', 'sub'); + $this->assertSame('select * from `prefix_users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `prefix_sub` on true', $builder->toSql()); + } + + public function testLeftJoinLateral() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + + $sub = $this->getMySqlBuilder(); + $sub->getConnection()->shouldReceive('getDatabaseName'); + + $builder->from('users')->leftJoinLateral($sub->from('contacts')->whereColumn('contracts.user_id', 'users.id'), 'sub'); + $this->assertSame('select * from `users` left join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $this->expectException(InvalidArgumentException::class); + $builder = $this->getBuilder(); + $builder->from('users')->leftJoinLateral(['foo'], 'sub'); + } + + public function testLeftJoinLateralSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->leftJoinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from [users] outer apply (select * from [contacts] where [contracts].[user_id] = [users].[id]) as [sub]', $builder->toSql()); + } + public function testRawExpressionsInSelect() { $builder = $this->getBuilder(); diff --git a/tests/Integration/Database/MySql/JoinLateralTest.php b/tests/Integration/Database/MySql/JoinLateralTest.php new file mode 100644 index 000000000000..969308ff856c --- /dev/null +++ b/tests/Integration/Database/MySql/JoinLateralTest.php @@ -0,0 +1,117 @@ +id('id'); + $table->string('name'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id('id'); + $table->string('title'); + $table->integer('rating'); + $table->unsignedBigInteger('user_id'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('posts'); + Schema::drop('users'); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->checkMySqlVersion(); + + DB::table('users')->insert([ + ['name' => Str::random()], + ['name' => Str::random()], + ]); + + DB::table('posts')->insert([ + ['title' => Str::random(), 'rating' => 1, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 3, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 7, 'user_id' => 1], + ]); + } + + protected function checkMySqlVersion() + { + $mySqlVersion = DB::select('select version()')[0]->{'version()'} ?? ''; + + if (strpos($mySqlVersion, 'Maria') !== false) { + $this->markTestSkipped('Lateral joins are not supported on MariaDB'.__CLASS__); + } elseif ((float) $mySqlVersion < '8.0.14') { + $this->markTestSkipped('Lateral joins are not supported on MySQL < 8.0.14'.__CLASS__); + } + } + + public function testJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(0, $userWithoutPosts); + } + + public function testLeftJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(1, $userWithoutPosts); + $this->assertNull($userWithoutPosts[0]->best_post_title); + $this->assertNull($userWithoutPosts[0]->best_post_rating); + } +} diff --git a/tests/Integration/Database/Postgres/JoinLateralTest.php b/tests/Integration/Database/Postgres/JoinLateralTest.php new file mode 100644 index 000000000000..e17f5622efdd --- /dev/null +++ b/tests/Integration/Database/Postgres/JoinLateralTest.php @@ -0,0 +1,104 @@ +id('id'); + $table->string('name'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id('id'); + $table->string('title'); + $table->integer('rating'); + $table->unsignedBigInteger('user_id'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('posts'); + Schema::drop('users'); + } + + protected function setUp(): void + { + parent::setUp(); + + DB::table('users')->insert([ + ['name' => Str::random()], + ['name' => Str::random()], + ]); + + DB::table('posts')->insert([ + ['title' => Str::random(), 'rating' => 1, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 3, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 7, 'user_id' => 1], + ]); + } + + public function testJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(0, $userWithoutPosts); + } + + public function testLeftJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(1, $userWithoutPosts); + $this->assertNull($userWithoutPosts[0]->best_post_title); + $this->assertNull($userWithoutPosts[0]->best_post_rating); + } +} diff --git a/tests/Integration/Database/SqlServer/JoinLateralTest.php b/tests/Integration/Database/SqlServer/JoinLateralTest.php new file mode 100644 index 000000000000..df11c5517585 --- /dev/null +++ b/tests/Integration/Database/SqlServer/JoinLateralTest.php @@ -0,0 +1,100 @@ +id('id'); + $table->string('name'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id('id'); + $table->string('title'); + $table->integer('rating'); + $table->unsignedBigInteger('user_id'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('posts'); + Schema::drop('users'); + } + + protected function setUp(): void + { + parent::setUp(); + + DB::table('users')->insert([ + ['name' => Str::random()], + ['name' => Str::random()], + ]); + + DB::table('posts')->insert([ + ['title' => Str::random(), 'rating' => 1, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 3, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 7, 'user_id' => 1], + ]); + } + + public function testJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, (int) $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, (int) $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(0, $userWithoutPosts); + } + + public function testLeftJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, (int) $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, (int) $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(1, $userWithoutPosts); + $this->assertNull($userWithoutPosts[0]->best_post_title); + $this->assertNull($userWithoutPosts[0]->best_post_rating); + } +} From 778683570e1a9d0a8a0da86f43f710edbd928a51 Mon Sep 17 00:00:00 2001 From: Amir Reza Mehrbakhsh Date: Mon, 26 Feb 2024 09:10:21 +0100 Subject: [PATCH 25/25] Update return type (#50252) --- src/Illuminate/Auth/TokenGuard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Auth/TokenGuard.php b/src/Illuminate/Auth/TokenGuard.php index b1aa7a7e5162..7fe5a9f7802a 100644 --- a/src/Illuminate/Auth/TokenGuard.php +++ b/src/Illuminate/Auth/TokenGuard.php @@ -92,7 +92,7 @@ public function user() /** * Get the token for the current request. * - * @return string + * @return string|null */ public function getTokenForRequest() {