From fd238bcb46807b80fa37f16497c4fea646d95451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Pape=C5=BE?= Date: Sun, 29 Oct 2023 17:23:51 +0100 Subject: [PATCH 001/207] fix(Eloquent/Builder): calling the methods on passthru base object should be case-insensitive (#48852) * fix(Eloquent/Builder): calling the methods on passthru base object should be case-insensitive Fixes: https://github.com/laravel/framework/issues/48825 * Update Builder.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Eloquent/Builder.php | 30 ++++++------- .../Database/DatabaseEloquentBuilderTest.php | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 8ab8e055e77f..87c06c2f96fe 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -97,29 +97,29 @@ class Builder implements BuilderContract 'avg', 'count', 'dd', - 'ddRawSql', - 'doesntExist', - 'doesntExistOr', + 'ddrawsql', + 'doesntexist', + 'doesntexistor', 'dump', - 'dumpRawSql', + 'dumprawsql', 'exists', - 'existsOr', + 'existsor', 'explain', - 'getBindings', - 'getConnection', - 'getGrammar', + 'getbindings', + 'getconnection', + 'getgrammar', 'implode', 'insert', - 'insertGetId', - 'insertOrIgnore', - 'insertUsing', + 'insertgetid', + 'insertorignore', + 'insertusing', 'max', 'min', 'raw', - 'rawValue', + 'rawvalue', 'sum', - 'toSql', - 'toRawSql', + 'tosql', + 'torawsql', ]; /** @@ -1964,7 +1964,7 @@ public function __call($method, $parameters) return $this->callNamedScope($method, $parameters); } - if (in_array($method, $this->passthru)) { + if (in_array(strtolower($method), $this->passthru)) { return $this->toBase()->{$method}(...$parameters); } diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 470214f20a5c..31354f2c1ab0 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -2246,6 +2246,48 @@ public function testToRawSql() $this->assertSame('select * from "users" where "email" = \'foo\'', $builder->toRawSql()); } + public function testPassthruMethodsCallsAreNotCaseSensitive() + { + $query = m::mock(BaseBuilder::class); + + $mockResponse = 'select 1'; + $query + ->shouldReceive('toRawSql') + ->andReturn($mockResponse) + ->times(3); + + $builder = new Builder($query); + + $this->assertSame('select 1', $builder->TORAWSQL()); + $this->assertSame('select 1', $builder->toRawSql()); + $this->assertSame('select 1', $builder->toRawSQL()); + } + + public function testPassthruArrayElementsMustAllBeLowercase() + { + $builder = new class(m::mock(BaseBuilder::class)) extends Builder + { + // expose protected member for test + public function getPassthru(): array + { + return $this->passthru; + } + }; + + $passthru = $builder->getPassthru(); + + foreach ($passthru as $method) { + $lowercaseMethod = strtolower($method); + + $this->assertSame( + $lowercaseMethod, + $method, + 'Eloquent\\Builder relies on lowercase method names in $passthru array to correctly mimic PHP case-insensitivity on method dispatch.'. + 'If you are adding a new method to the $passthru array, make sure the name is lowercased.' + ); + } + } + protected function mockConnectionForModel($model, $database) { $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; From 84a15808901342df1356d883944e1ac38b0586f0 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Sun, 29 Oct 2023 12:25:11 -0400 Subject: [PATCH 002/207] typehint (#48847) --- .../Database/Eloquent/Concerns/QueriesRelationships.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 3c73ad230b09..dd0160adad13 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -686,7 +686,7 @@ public function withAggregate($relations, $column, $function = null) * Get the relation hashed column name for the given column and relation. * * @param string $column - * @param \Illuminate\Database\Eloquent\Relations\Relationship $relation + * @param \Illuminate\Database\Eloquent\Relations\Relation $relation * @return string */ protected function getRelationHashedColumn($column, $relation) From c948905636f60e79cf47365c82fda3773776be85 Mon Sep 17 00:00:00 2001 From: Moshe Brodsky <44633930+moshe-autoleadstar@users.noreply.github.com> Date: Sun, 29 Oct 2023 18:27:47 +0200 Subject: [PATCH 003/207] fix Exception: Cannot traverse an already closed generator when running Arr::first with an empty generator and no callback (#48851) * fix Exception: Cannot traverse an already closed generator when running Arr::first with an empty generator and no callback * styling * should use default return --- src/Illuminate/Collections/Arr.php | 2 ++ tests/Support/SupportArrTest.php | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index 3cb9a2ca180a..c14465c6b3fa 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -195,6 +195,8 @@ public static function first($array, callable $callback = null, $default = null) foreach ($array as $item) { return $item; } + + return value($default); } foreach ($array as $key => $value) { diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index f87a36364b79..2c5f6c224e67 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -214,6 +214,13 @@ public function testFirst() $this->assertSame('bar', $value3); $this->assertSame('baz', $value4); $this->assertEquals(100, $value5); + + $cursor = (function () { + while (false) { + yield 1; + } + })(); + $this->assertNull(Arr::first($cursor)); } public function testJoin() From 9d7868b11c152aa9416c8a4ab85f23b0473709ff Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 30 Oct 2023 11:09:19 +1100 Subject: [PATCH 004/207] [10.x] Remember the job on the exception (#48830) * Remember the job on the exception * Update tests/Queue/QueueExceptionTest.php Co-authored-by: Julius Kiekbusch * Update tests/Queue/QueueExceptionTest.php Co-authored-by: Julius Kiekbusch * Update src/Illuminate/Queue/MaxAttemptsExceededException.php Co-authored-by: Dries Vints * Update MaxAttemptsExceededException.php --------- Co-authored-by: Dries Vints Co-authored-by: Julius Kiekbusch Co-authored-by: Taylor Otwell --- .../Queue/MaxAttemptsExceededException.php | 20 ++++++++- .../Queue/TimeoutExceededException.php | 13 +++++- src/Illuminate/Queue/Worker.php | 8 +--- tests/Queue/QueueExceptionTest.php | 41 +++++++++++++++++++ 4 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 tests/Queue/QueueExceptionTest.php diff --git a/src/Illuminate/Queue/MaxAttemptsExceededException.php b/src/Illuminate/Queue/MaxAttemptsExceededException.php index 89e4bdd2168b..0e95c870e793 100644 --- a/src/Illuminate/Queue/MaxAttemptsExceededException.php +++ b/src/Illuminate/Queue/MaxAttemptsExceededException.php @@ -6,5 +6,23 @@ class MaxAttemptsExceededException extends RuntimeException { - // + /** + * The job instance. + * + * @var \Illuminate\Contracts\Queue\Job|null + */ + public $job; + + /** + * Create a new instance for the job. + * + * @param \Illuminate\Contracts\Queue\Job $job + * @return static + */ + public static function forJob($job) + { + return tap(new static($job->resolveName().' has been attempted too many times.'), function ($e) use ($job) { + $e->job = $job; + }); + } } diff --git a/src/Illuminate/Queue/TimeoutExceededException.php b/src/Illuminate/Queue/TimeoutExceededException.php index 37309ceaf04b..41efabec1af3 100644 --- a/src/Illuminate/Queue/TimeoutExceededException.php +++ b/src/Illuminate/Queue/TimeoutExceededException.php @@ -4,5 +4,16 @@ class TimeoutExceededException extends MaxAttemptsExceededException { - // + /** + * Create a new instance for the job. + * + * @param \Illuminate\Contracts\Queue\Job $job + * @return static + */ + public static function forJob($job) + { + return tap(new static($job->resolveName().' has timed out.'), function ($e) use ($job) { + $e->job = $job; + }); + } } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 0408cc99c84b..2e7474f71c1e 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -782,9 +782,7 @@ public function kill($status = 0, $options = null) */ protected function maxAttemptsExceededException($job) { - return new MaxAttemptsExceededException( - $job->resolveName().' has been attempted too many times.' - ); + return MaxAttemptsExceededException::forJob($job); } /** @@ -795,9 +793,7 @@ protected function maxAttemptsExceededException($job) */ protected function timeoutExceededException($job) { - return new TimeoutExceededException( - $job->resolveName().' has timed out.' - ); + return TimeoutExceededException::forJob($job); } /** diff --git a/tests/Queue/QueueExceptionTest.php b/tests/Queue/QueueExceptionTest.php new file mode 100644 index 000000000000..22c49933fbc3 --- /dev/null +++ b/tests/Queue/QueueExceptionTest.php @@ -0,0 +1,41 @@ +assertSame('App\\Jobs\\UnderlyingJob has timed out.', $e->getMessage()); + $this->assertSame($job, $e->job); + } + + public function test_it_can_create_max_attempts_exception_for_job() + { + $e = MaxAttemptsExceededException::forJob($job = new MyFakeRedisJob()); + + $this->assertSame('App\\Jobs\\UnderlyingJob has been attempted too many times.', $e->getMessage()); + $this->assertSame($job, $e->job); + } +} + +class MyFakeRedisJob extends RedisJob +{ + public function __construct() + { + // + } + + public function resolveName() + { + return 'App\\Jobs\\UnderlyingJob'; + } +} From 0646f334443a2f6a310c51254f1c6db1049ef87c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 30 Oct 2023 00:09:39 +0000 Subject: [PATCH 005/207] Apply fixes from StyleCI --- tests/Queue/QueueExceptionTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Queue/QueueExceptionTest.php b/tests/Queue/QueueExceptionTest.php index 22c49933fbc3..9b4b246b47cd 100644 --- a/tests/Queue/QueueExceptionTest.php +++ b/tests/Queue/QueueExceptionTest.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Queue; use Illuminate\Queue\Jobs\RedisJob; -use Illuminate\Queue\Jobs\SyncJob; use Illuminate\Queue\MaxAttemptsExceededException; use Illuminate\Queue\TimeoutExceededException; use PHPUnit\Framework\TestCase; From c55667a1cb8602b952edc53ebfe61ea83b7abf1f Mon Sep 17 00:00:00 2001 From: Mohammadhossein Fereydouni Date: Mon, 30 Oct 2023 03:51:53 +0330 Subject: [PATCH 006/207] fix bug for always throwing exception when we pass a callable to throwUnlessStatus method [test included] (#48844) * fix bug for always throwing exception when we pass a callable * added test for throwUnlessStatus method with callable input returning true --- src/Illuminate/Http/Client/Response.php | 5 ++--- tests/Http/HttpClientTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index f26886e2f80e..b73dd8706536 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -338,9 +338,8 @@ public function throwIfStatus($statusCode) */ public function throwUnlessStatus($statusCode) { - if (is_callable($statusCode) && - ! $statusCode($this->status(), $this)) { - return $this->throw(); + if (is_callable($statusCode)) { + return $statusCode($this->status(), $this) ? $this : $this->throw(); } return $this->status() === $statusCode ? $this : $this->throw(); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index c2056867c970..6aadac7005ac 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -2194,6 +2194,16 @@ public function testRequestExceptionIsThrownUnlessStatusCodeIsSatisfied() } $this->assertNull($exception); + + $exception = null; + + try { + $this->factory->get('http://foo.com/api/500')->throwUnlessStatus(fn ($status) => $status === 500); + } catch (RequestException $e) { + $exception = $e; + } + + $this->assertNull($exception); } public function testRequestExceptionIsThrownIfIsClientError() From 0525a8819db53bc66c878413829a4368d63c3d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Guimar=C3=A3es?= Date: Sun, 29 Oct 2023 21:59:22 -0300 Subject: [PATCH 007/207] [10.x] Dispatch events based on a DB transaction result (#48705) * wip * Refactor * Add EventFake support * remove strict types * Make styleCI happy * Fix test * Add missing test for EventFake * Add test to handle nested transactions * fix typo * Make styleci happy * formatting, inject manager resolver * formatting * formatting * formatting * formatting * more thorough solution * Add additional test for nested transactions * Add additional nested transaction test --------- Co-authored-by: Taylor Otwell --- .../Events/ShouldDispatchAfterCommit.php | 8 + .../Events/ShouldHandleEventsAfterCommit.php | 8 + .../Queue/ShouldQueueAfterCommit.php | 8 + .../Eloquent/BroadcastsEventsAfterCommit.php | 18 ++ src/Illuminate/Events/Dispatcher.php | 79 ++++++++- .../Events/EventServiceProvider.php | 4 + src/Illuminate/Mail/SendQueuedMailable.php | 8 +- .../Notifications/SendQueuedNotifications.php | 9 +- src/Illuminate/Queue/Queue.php | 5 + .../Support/Testing/Fakes/EventFake.php | 22 ++- tests/Integration/Events/EventFakeTest.php | 60 +++++++ .../ShouldDispatchAfterCommitEventTest.php | 163 ++++++++++++++++++ 12 files changed, 383 insertions(+), 9 deletions(-) create mode 100644 src/Illuminate/Contracts/Events/ShouldDispatchAfterCommit.php create mode 100644 src/Illuminate/Contracts/Events/ShouldHandleEventsAfterCommit.php create mode 100644 src/Illuminate/Contracts/Queue/ShouldQueueAfterCommit.php create mode 100644 src/Illuminate/Database/Eloquent/BroadcastsEventsAfterCommit.php create mode 100644 tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php diff --git a/src/Illuminate/Contracts/Events/ShouldDispatchAfterCommit.php b/src/Illuminate/Contracts/Events/ShouldDispatchAfterCommit.php new file mode 100644 index 000000000000..8f1fbdd4d697 --- /dev/null +++ b/src/Illuminate/Contracts/Events/ShouldDispatchAfterCommit.php @@ -0,0 +1,8 @@ +parseEventAndPayload( - $event, $payload - ); + [$isEventObject, $event, $payload] = [ + is_object($event), + ...$this->parseEventAndPayload($event, $payload) + ]; + + // If the event is not intended to be dispatched unless the current database + // transaction is successful, we'll register a callback which will handle + // dispatching this event on the next successful DB transaction commit. + if ($isEventObject && + $payload[0] instanceof ShouldDispatchAfterCommit && + ! is_null($transactions = $this->resolveTransactionManager())) { + $transactions->addCallback( + fn () => $this->invokeListeners($event, $payload, $halt) + ); + return null; + } + + return $this->invokeListeners($event, $payload, $halt); + } + + /** + * Broadcast an event and call its listeners. + * + * @param string|object $event + * @param mixed $payload + * @param bool $halt + * @return array|null + */ + protected function invokeListeners($event, $payload, $halt = false) + { if ($this->shouldBroadcast($payload)) { $this->broadcastEvent($payload[0]); } @@ -525,7 +562,9 @@ protected function createQueuedHandlerCallable($class, $method) */ protected function handlerShouldBeDispatchedAfterDatabaseTransactions($listener) { - return ($listener->afterCommit ?? null) && $this->container->bound('db.transactions'); + return (($listener->afterCommit ?? null) || + $listener instanceof ShouldHandleEventsAfterCommit) && + $this->resolveTransactionManager(); } /** @@ -540,7 +579,7 @@ protected function createCallbackForListenerRunningAfterCommits($listener, $meth return function () use ($method, $listener) { $payload = func_get_args(); - $this->container->make('db.transactions')->addCallback( + $this->resolveTransactionManager()->addCallback( function () use ($listener, $method, $payload) { $listener->$method(...$payload); } @@ -624,7 +663,12 @@ protected function propagateListenerOptions($listener, $job) return tap($job, function ($job) use ($listener) { $data = array_values($job->data); - $job->afterCommit = property_exists($listener, 'afterCommit') ? $listener->afterCommit : null; + if ($listener instanceof ShouldQueueAfterCommit) { + $job->afterCommit = true; + } else { + $job->afterCommit = property_exists($listener, 'afterCommit') ? $listener->afterCommit : null; + } + $job->backoff = method_exists($listener, 'backoff') ? $listener->backoff(...$data) : ($listener->backoff ?? null); $job->maxExceptions = $listener->maxExceptions ?? null; $job->retryUntil = method_exists($listener, 'retryUntil') ? $listener->retryUntil(...$data) : null; @@ -697,6 +741,29 @@ public function setQueueResolver(callable $resolver) return $this; } + /** + * Get the database transaction manager implementation from the resolver. + * + * @return \Illuminate\Database\DatabaseTransactionsManager|null + */ + protected function resolveTransactionManager() + { + return call_user_func($this->transactionManagerResolver); + } + + /** + * Set the database transaction manager resolver implementation. + * + * @param callable $resolver + * @return $this + */ + public function setTransactionManagerResolver(callable $resolver) + { + $this->transactionManagerResolver = $resolver; + + return $this; + } + /** * Gets the raw, unprepared listeners. * diff --git a/src/Illuminate/Events/EventServiceProvider.php b/src/Illuminate/Events/EventServiceProvider.php index 15fb60b10bba..cf9fbe25e3c3 100755 --- a/src/Illuminate/Events/EventServiceProvider.php +++ b/src/Illuminate/Events/EventServiceProvider.php @@ -17,6 +17,10 @@ public function register() $this->app->singleton('events', function ($app) { return (new Dispatcher($app))->setQueueResolver(function () use ($app) { return $app->make(QueueFactoryContract::class); + })->setTransactionManagerResolver(function () use ($app) { + return $app->bound('db.transactions') + ? $app->make('db.transactions') + : null; }); }); } diff --git a/src/Illuminate/Mail/SendQueuedMailable.php b/src/Illuminate/Mail/SendQueuedMailable.php index 28a72c47c7de..b9fec9d03849 100644 --- a/src/Illuminate/Mail/SendQueuedMailable.php +++ b/src/Illuminate/Mail/SendQueuedMailable.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Mail\Factory as MailFactory; use Illuminate\Contracts\Mail\Mailable as MailableContract; use Illuminate\Contracts\Queue\ShouldBeEncrypted; +use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; use Illuminate\Queue\InteractsWithQueue; class SendQueuedMailable @@ -57,7 +58,12 @@ public function __construct(MailableContract $mailable) { $this->mailable = $mailable; - $this->afterCommit = property_exists($mailable, 'afterCommit') ? $mailable->afterCommit : null; + if ($mailable instanceof ShouldQueueAfterCommit) { + $this->afterCommit = true; + } else { + $this->afterCommit = property_exists($mailable, 'afterCommit') ? $mailable->afterCommit : null; + } + $this->connection = property_exists($mailable, 'connection') ? $mailable->connection : null; $this->maxExceptions = property_exists($mailable, 'maxExceptions') ? $mailable->maxExceptions : null; $this->queue = property_exists($mailable, 'queue') ? $mailable->queue : null; diff --git a/src/Illuminate/Notifications/SendQueuedNotifications.php b/src/Illuminate/Notifications/SendQueuedNotifications.php index 19af18853667..f63d7bf9afe4 100644 --- a/src/Illuminate/Notifications/SendQueuedNotifications.php +++ b/src/Illuminate/Notifications/SendQueuedNotifications.php @@ -5,6 +5,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Queue\InteractsWithQueue; @@ -80,7 +81,13 @@ public function __construct($notifiables, $notification, array $channels = null) $this->tries = property_exists($notification, 'tries') ? $notification->tries : null; $this->timeout = property_exists($notification, 'timeout') ? $notification->timeout : null; $this->maxExceptions = property_exists($notification, 'maxExceptions') ? $notification->maxExceptions : null; - $this->afterCommit = property_exists($notification, 'afterCommit') ? $notification->afterCommit : null; + + if ($notification instanceof ShouldQueueAfterCommit) { + $this->afterCommit = true; + } else { + $this->afterCommit = property_exists($notification, 'afterCommit') ? $notification->afterCommit : null; + } + $this->shouldBeEncrypted = $notification instanceof ShouldBeEncrypted; } diff --git a/src/Illuminate/Queue/Queue.php b/src/Illuminate/Queue/Queue.php index 03085f60a05d..0ce7ad1ac1ce 100755 --- a/src/Illuminate/Queue/Queue.php +++ b/src/Illuminate/Queue/Queue.php @@ -7,6 +7,7 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Queue\ShouldBeEncrypted; +use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; use Illuminate\Queue\Events\JobQueued; use Illuminate\Support\Arr; use Illuminate\Support\InteractsWithTime; @@ -325,6 +326,10 @@ function () use ($payload, $queue, $delay, $callback, $job) { */ protected function shouldDispatchAfterCommit($job) { + if (is_object($job) && $job instanceof ShouldQueueAfterCommit) { + return true; + } + if (! $job instanceof Closure && is_object($job) && isset($job->afterCommit)) { return $job->afterCommit; } diff --git a/src/Illuminate/Support/Testing/Fakes/EventFake.php b/src/Illuminate/Support/Testing/Fakes/EventFake.php index 7a32315ce5d5..4a4fc7c5b22d 100644 --- a/src/Illuminate/Support/Testing/Fakes/EventFake.php +++ b/src/Illuminate/Support/Testing/Fakes/EventFake.php @@ -3,7 +3,9 @@ namespace Illuminate\Support\Testing\Fakes; use Closure; +use Illuminate\Container\Container; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; @@ -297,7 +299,7 @@ public function dispatch($event, $payload = [], $halt = false) $name = is_object($event) ? get_class($event) : (string) $event; if ($this->shouldFakeEvent($name, $payload)) { - $this->events[$name][] = func_get_args(); + $this->fakeEvent($event, $name, func_get_args()); } else { return $this->dispatcher->dispatch($event, $payload, $halt); } @@ -329,6 +331,24 @@ protected function shouldFakeEvent($eventName, $payload) ->isNotEmpty(); } + /** + * Push the event onto the fake events array immediately or after the next database transaction. + * + * @param string|object $event + * @param string $name + * @param array $arguments + * @return void + */ + protected function fakeEvent($event, $name, $arguments) + { + if ($event instanceof ShouldDispatchAfterCommit && Container::getInstance()->bound('db.transactions')) { + return Container::getInstance()->make('db.transactions') + ->addCallback(fn () => $this->events[$name][] = $arguments); + } + + $this->events[$name][] = $arguments; + } + /** * Determine whether an event should be dispatched or not. * diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php index 2d85bbd01274..a7e9b97cf096 100644 --- a/tests/Integration/Events/EventFakeTest.php +++ b/tests/Integration/Events/EventFakeTest.php @@ -3,12 +3,16 @@ namespace Illuminate\Tests\Integration\Events; use Closure; +use Exception; +use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\ExpectationFailedException; class EventFakeTest extends TestCase { @@ -182,6 +186,57 @@ public function testMissingMethodsAreForwarded() $this->assertEquals('bar', Event::fake()->foo()); } + + public function testShouldDispatchAfterCommitEventsAreNotDispatchedIfTransactionFails() + { + Event::fake(); + + try { + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitEvent()); + + throw new Exception('foo'); + }); + } catch (Exception $e) { + } + + Event::assertNotDispatched(ShouldDispatchAfterCommitEvent::class); + } + + public function testShouldDispatchAfterCommitEventsAreDispatchedIfTransactionSucceeds() + { + Event::fake(); + + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitEvent()); + }); + + Event::assertDispatched(ShouldDispatchAfterCommitEvent::class); + } + + public function testShouldDispatchAfterCommitEventsAreDispatchedIfThereIsNoTransaction() + { + Event::fake(); + + Event::dispatch(new ShouldDispatchAfterCommitEvent()); + Event::assertDispatched(ShouldDispatchAfterCommitEvent::class); + } + + public function testAssertNothingDispatchedShouldDispatchAfterCommit() + { + Event::fake(); + Event::assertNothingDispatched(); + + Event::dispatch(new ShouldDispatchAfterCommitEvent); + Event::dispatch(new ShouldDispatchAfterCommitEvent); + + try { + Event::assertNothingDispatched(); + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertStringContainsString('2 unexpected events were dispatched.', $e->getMessage()); + } + } } class Post extends Model @@ -248,3 +303,8 @@ public function __invoke($event) // } } + +class ShouldDispatchAfterCommitEvent implements ShouldDispatchAfterCommit +{ + // +} diff --git a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php new file mode 100644 index 000000000000..6984f26a6a35 --- /dev/null +++ b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php @@ -0,0 +1,163 @@ +assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testEventIsNotDispatchedIfTransactionFails() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + + try { + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + + throw new \Exception; + }); + } catch (\Exception) { + } + + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testEventIsDispatchedIfTransactionSucceeds() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + }); + + $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testItHandlesNestedTransactions() + { + // We are going to dispatch 2 different events in 2 different transactions. + // The parent transaction will succeed, but the nested transaction is going to fail and be rolled back. + // We want to ensure the event dispatched on the child transaction does not get published, since it failed, + // however, the event dispatched on the parent transaction should still be dispatched as usual. + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + Event::listen(AnotherShouldDispatchAfterCommitTestEvent::class, AnotherShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + try { + DB::transaction(function () { + // This event should not be dispatched since the transaction is going to fail. + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + throw new \Exception; + }); + } catch (\Exception) { + } + + // This event should be dispatched, as the parent transaction does not fail. + Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); + }); + + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + $this->assertTrue(AnotherShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testItOnlyDispatchesNestedTransactionsEventsAfterTheRootTransactionIsCommitted() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + Event::listen(AnotherShouldDispatchAfterCommitTestEvent::class, AnotherShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); + + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + }); + + // Although the child transaction has been concluded, the parent transaction has not. + // The event dispatched on the child transaction should not have been dispatched. + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + $this->assertFalse(AnotherShouldDispatchAfterCommitTestEvent::$ran); + }); + + // Now that the parent transaction has been committed, the event + // on the child transaction should also have been dispatched. + $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + $this->assertTrue(AnotherShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testItOnlyDispatchesNestedTransactionsEventsAfterTheRootTransactionIsCommitedDifferentOrder() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + Event::listen(AnotherShouldDispatchAfterCommitTestEvent::class, AnotherShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + }); + + // Although the child transaction has been concluded, the parent transaction has not. + // The event dispatched on the child transaction should not have been dispatched. + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + + // The main difference with this test is that we dispatch an event on the parent transaction + // at the end. This is important due to how the DatabaseTransactionsManager works. + Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); + }); + + // Now that the parent transaction has been committed, the event + // on the child transaction should also have been dispatched. + $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + $this->assertTrue(AnotherShouldDispatchAfterCommitTestEvent::$ran); + } +} + +class TransactionUnawareTestEvent +{ + public static $ran = false; +} + +class ShouldDispatchAfterCommitTestEvent implements ShouldDispatchAfterCommit +{ + public static $ran = false; +} + +class AnotherShouldDispatchAfterCommitTestEvent implements ShouldDispatchAfterCommit +{ + public static $ran = false; +} + +class ShouldDispatchAfterCommitListener +{ + public function handle(object $event) + { + $event::$ran = true; + } +} + +class AnotherShouldDispatchAfterCommitListener +{ + public function handle(object $event) + { + $event::$ran = true; + } +} From f0c9fceafd9e198f18264ee66a9910b47aa77829 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 30 Oct 2023 00:59:35 +0000 Subject: [PATCH 008/207] Apply fixes from StyleCI --- src/Illuminate/Events/Dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index 9a8a4d7b6786..2f3ac4cb9a76 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -247,7 +247,7 @@ public function dispatch($event, $payload = [], $halt = false) // payload to the handler, which makes object based events quite simple. [$isEventObject, $event, $payload] = [ is_object($event), - ...$this->parseEventAndPayload($event, $payload) + ...$this->parseEventAndPayload($event, $payload), ]; // If the event is not intended to be dispatched unless the current database From 8473522243f7a3e0a8f2ec1270f8658788bba28c Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Mon, 30 Oct 2023 01:00:17 +0000 Subject: [PATCH 009/207] Update facade docblocks --- src/Illuminate/Support/Facades/Event.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Event.php b/src/Illuminate/Support/Facades/Event.php index b2784d096f41..3694d0304455 100755 --- a/src/Illuminate/Support/Facades/Event.php +++ b/src/Illuminate/Support/Facades/Event.php @@ -20,6 +20,7 @@ * @method static void forget(string $event) * @method static void forgetPushed() * @method static \Illuminate\Events\Dispatcher setQueueResolver(callable $resolver) + * @method static \Illuminate\Events\Dispatcher setTransactionManagerResolver(callable $resolver) * @method static array getRawListeners() * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) From 6f06ad4514e655800f7a6d93d262ec830edcce78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Guimar=C3=A3es?= Date: Mon, 30 Oct 2023 11:13:13 -0300 Subject: [PATCH 010/207] Reset ShouldDispatchAfterCommitEventTest objects properties (#48858) --- tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php index 6984f26a6a35..e20d65891a23 100644 --- a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php +++ b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php @@ -14,6 +14,7 @@ protected function tearDown(): void { TransactionUnawareTestEvent::$ran = false; ShouldDispatchAfterCommitTestEvent::$ran = false; + AnotherShouldDispatchAfterCommitTestEvent::$ran = false; m::close(); } From 0aeececbde74888cfc24839961dd8c49452eb05f Mon Sep 17 00:00:00 2001 From: sidneyprins <84329386+sidneyprins@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:29:38 +0100 Subject: [PATCH 011/207] [10.x] Throw exception when trying to escape array for database connection (#48836) * Throw error when trying to escape array * Add tests to test escaping array throwing exception * Update Connection.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Connection.php | 2 ++ tests/Integration/Database/MySql/EscapeTest.php | 7 +++++++ tests/Integration/Database/Postgres/EscapeTest.php | 7 +++++++ tests/Integration/Database/SqlServer/EscapeTest.php | 7 +++++++ tests/Integration/Database/Sqlite/EscapeTest.php | 7 +++++++ 5 files changed, 30 insertions(+) diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index 24583f4a4079..a46448bb8974 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -1101,6 +1101,8 @@ public function escape($value, $binary = false) return (string) $value; } elseif (is_bool($value)) { return $this->escapeBool($value); + } elseif (is_array($value)) { + throw new RuntimeException('The database connection does not support escaping arrays.'); } else { if (str_contains($value, "\00")) { throw new RuntimeException('Strings with null bytes cannot be escaped. Use the binary escape option.'); diff --git a/tests/Integration/Database/MySql/EscapeTest.php b/tests/Integration/Database/MySql/EscapeTest.php index 9ad6d6e8a41f..53d70bd8292d 100644 --- a/tests/Integration/Database/MySql/EscapeTest.php +++ b/tests/Integration/Database/MySql/EscapeTest.php @@ -61,4 +61,11 @@ public function testEscapeStringNullByte() $this->app['db']->escape("I am hiding a \00 byte"); } + + public function testEscapeArray() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape(['a', 'b']); + } } diff --git a/tests/Integration/Database/Postgres/EscapeTest.php b/tests/Integration/Database/Postgres/EscapeTest.php index dc382b4a1143..3a8de1329264 100644 --- a/tests/Integration/Database/Postgres/EscapeTest.php +++ b/tests/Integration/Database/Postgres/EscapeTest.php @@ -61,4 +61,11 @@ public function testEscapeStringNullByte() $this->app['db']->escape("I am hiding a \00 byte"); } + + public function testEscapeArray() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape(['a', 'b']); + } } diff --git a/tests/Integration/Database/SqlServer/EscapeTest.php b/tests/Integration/Database/SqlServer/EscapeTest.php index 77037d1cc810..51c4cbcdd4ca 100644 --- a/tests/Integration/Database/SqlServer/EscapeTest.php +++ b/tests/Integration/Database/SqlServer/EscapeTest.php @@ -57,4 +57,11 @@ public function testEscapeStringNullByte() $this->app['db']->escape("I am hiding a \00 byte"); } + + public function testEscapeArray() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape(['a', 'b']); + } } diff --git a/tests/Integration/Database/Sqlite/EscapeTest.php b/tests/Integration/Database/Sqlite/EscapeTest.php index f642c6fe3363..bcf6c2f0d5c2 100644 --- a/tests/Integration/Database/Sqlite/EscapeTest.php +++ b/tests/Integration/Database/Sqlite/EscapeTest.php @@ -73,4 +73,11 @@ public function testEscapeStringNullByte() $this->app['db']->escape("I am hiding a \00 byte"); } + + public function testEscapeArray() + { + $this->expectException(RuntimeException::class); + + $this->app['db']->escape(['a', 'b']); + } } From 0294be7761c632b0e5498c2067e728e039fb3bb2 Mon Sep 17 00:00:00 2001 From: Lasse R Date: Tue, 31 Oct 2023 03:44:34 +0800 Subject: [PATCH 012/207] [10.x] Fix Stringable objects not converted to string in HTTP facade Query parameters and Body (#48849) * Update PendingRequest.php * Add Test * Fix tests and code * Minor tidy * Support for nested arrays of Stringable + tests * refactor --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Http/Client/PendingRequest.php | 24 +++++- tests/Http/HttpClientTest.php | 77 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index a3a022c352f3..cfab639cf511 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -19,6 +19,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; use JsonSerializable; @@ -1035,10 +1036,12 @@ protected function sendRequest(string $method, string $url, array $options = []) $this->transferStats = $transferStats; }; - return $this->buildClient()->$clientMethod($method, $url, $this->mergeOptions([ + $mergedOptions = $this->normalizeRequestOptions($this->mergeOptions([ 'laravel_data' => $laravelData, 'on_stats' => $onStats, ], $options)); + + return $this->buildClient()->$clientMethod($method, $url, $mergedOptions); } /** @@ -1076,6 +1079,25 @@ protected function parseRequestData($method, $url, array $options) return is_array($laravelData) ? $laravelData : []; } + /** + * Normalize the given request options. + * + * @param array $options + * @return array + */ + 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, + }; + } + + return $options; + } + /** * Populate the given response with additional data. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 6aadac7005ac..446980b88d22 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -481,6 +481,57 @@ public function toArray(): array }); } + public function testCanSendJsonDataWithStringable() + { + $this->factory->fake(); + + $this->factory->withHeaders([ + 'X-Test-Header' => 'foo', + 'X-Test-ArrayHeader' => ['bar', 'baz'], + ])->post('http://foo.com/json', [ + 'name' => Str::of('Taylor'), + ]); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/json' && + $request->hasHeader('Content-Type', 'application/json') && + $request->hasHeader('X-Test-Header', 'foo') && + $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && + $request['name'] === 'Taylor'; + }); + } + + public function testCanSendFormDataWithStringable() + { + $this->factory->fake(); + + $this->factory->asForm()->post('http://foo.com/form', [ + 'name' => Str::of('Taylor'), + 'title' => 'Laravel Developer', + ]); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/form' && + $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && + $request['name'] === 'Taylor'; + }); + } + + public function testCanSendFormDataWithStringableInArrays() + { + $this->factory->fake(); + + $this->factory->asForm()->post('http://foo.com/form', [ + 'posts' => [['title' => Str::of('Taylor')]], + ]); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'http://foo.com/form' && + $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && + $request['posts'][0]['title'] === 'Taylor'; + }); + } + public function testRecordedCallsAreEmptiedWhenFakeIsCalled() { $this->factory->fake([ @@ -800,6 +851,32 @@ public function testWithQueryParametersAllowsOverridingParameterOnRequest() }); } + public function testWithStringableQueryParameters() + { + $this->factory->fake(); + + $this->factory->withQueryParameters( + ['foo' => Str::of('bar'),] + )->get('https://laravel.com'); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'https://laravel.com?foo=bar'; + }); + } + + public function testWithArrayStringableQueryParameters() + { + $this->factory->fake(); + + $this->factory->withQueryParameters( + ['foo' => ['bar', Str::of('baz')]], + )->get('https://laravel.com'); + + $this->factory->assertSent(function (Request $request) { + return $request->url() === 'https://laravel.com?foo%5B0%5D=bar&foo%5B1%5D=baz'; + }); + } + public function testGetWithArrayQueryParam() { $this->factory->fake(); From 7d221aa744845861657d0196e197957cb5a447ad Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 30 Oct 2023 19:44:59 +0000 Subject: [PATCH 013/207] 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 446980b88d22..6e8b48710990 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -856,7 +856,7 @@ public function testWithStringableQueryParameters() $this->factory->fake(); $this->factory->withQueryParameters( - ['foo' => Str::of('bar'),] + ['foo' => Str::of('bar')] )->get('https://laravel.com'); $this->factory->assertSent(function (Request $request) { From 3dd85d9dbea82b937f8eaf344b50d613c5d1127a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 31 Oct 2023 08:19:42 -0500 Subject: [PATCH 014/207] version --- 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 e87faa82f0dd..0cb1696cca29 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.29.0'; + const VERSION = '10.30.0'; /** * The base path for the Laravel installation. From 87b9e7997e178dfc4acd5e22fa8d77ba333c3abd Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 31 Oct 2023 13:27:49 +0000 Subject: [PATCH 015/207] Update CHANGELOG --- CHANGELOG.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c6e8efbd1c..c6e894786d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.29.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.30.0...10.x) + +## [v10.30.0](https://github.com/laravel/framework/compare/v10.29.0...v10.30.0) - 2023-10-31 + +- [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/48815 +- [10.x] Verify hash config by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/48814 +- [10.x] Fix the issue of using the now function within the ArrayCache in Lumen by [@cxlblm](https://github.com/cxlblm) in https://github.com/laravel/framework/pull/48826 +- [10.x] Match service provider after resolved by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/48824 +- [10.x] Fix type error registering PSR Request by [@kpicaza](https://github.com/kpicaza) in https://github.com/laravel/framework/pull/48823 +- [10.x] Ability to configure default session block timeouts by [@bytestream](https://github.com/bytestream) in https://github.com/laravel/framework/pull/48795 +- [10.x] Improvements for `artisan migrate --pretend` command 🚀 by [@NickSdot](https://github.com/NickSdot) in https://github.com/laravel/framework/pull/48768 +- [10.x] Add support for getting native columns' attributes by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/48357 +- fix(Eloquent/Builder): calling the methods on passthru base object should be case-insensitive by [@luka-papez](https://github.com/luka-papez) in https://github.com/laravel/framework/pull/48852 +- [10.x] Fix `QueriesRelationships[@getRelationHashedColumn](https://github.com/getRelationHashedColumn)()` typehint by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/48847 +- [10.x] Remember the job on the exception by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/48830 +- fix bug for always throwing exception when we pass a callable to throwUnlessStatus method [test included] by [@mhfereydouni](https://github.com/mhfereydouni) in https://github.com/laravel/framework/pull/48844 +- [10.x] Dispatch events based on a DB transaction result by [@mateusjatenee](https://github.com/mateusjatenee) in https://github.com/laravel/framework/pull/48705 +- [10.x] Reset ShouldDispatchAfterCommitEventTest objects properties by [@mateusjatenee](https://github.com/mateusjatenee) in https://github.com/laravel/framework/pull/48858 +- [10.x] Throw exception when trying to escape array for database connection by [@sidneyprins](https://github.com/sidneyprins) in https://github.com/laravel/framework/pull/48836 +- [10.x] Fix Stringable objects not converted to string in HTTP facade Query parameters and Body by [@LasseRafn](https://github.com/LasseRafn) in https://github.com/laravel/framework/pull/48849 ## [v10.29.0](https://github.com/laravel/framework/compare/v10.28.0...v10.29.0) - 2023-10-24 From a6db48eb049614c0e964bc7b155d69c07fff3b56 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 1 Nov 2023 17:19:58 +0330 Subject: [PATCH 016/207] [10.x] Fix postgreSQL reserved word column names w/ guarded attributes broken in native column attributes implementation (#48877) * unquote quoted names on postgresql * add test * fix mysql 5.7 error * fix table prefixed twice * revert removing 'end' keyword --- .../Query/Processors/PostgresProcessor.php | 2 +- src/Illuminate/Database/Schema/Builder.php | 4 +-- .../Database/EloquentModelTest.php | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index 0ec35de6853b..efa6cf027fb6 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -59,7 +59,7 @@ public function processColumns($results) $autoincrement = $result->default !== null && str_starts_with($result->default, 'nextval('); return [ - 'name' => $result->name, + 'name' => str_starts_with($result->name, '"') ? str_replace('"', '', $result->name) : $result->name, 'type_name' => $result->type_name, 'type' => $result->type, 'collation' => $result->collation, diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 6597dcf57cb7..1c1e4ff3c8be 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -238,9 +238,9 @@ public function whenTableDoesntHaveColumn(string $table, string $column, Closure */ public function getColumnType($table, $column, $fullDefinition = false) { - $table = $this->connection->getTablePrefix().$table; - if (! $this->connection->usingNativeSchemaOperations()) { + $table = $this->connection->getTablePrefix().$table; + return $this->connection->getDoctrineColumn($table, $column)->getType()->getName(); } diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index fca6fe3e9578..937a8a7b55c8 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -94,6 +94,38 @@ public function testDiscardChanges() $user->save(); $this->assertFalse($user->wasChanged()); } + + public function testInsertRecordWithReservedWordFieldName() + { + Schema::create('actions', function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->timestamp('start'); + $table->timestamp('end')->nullable(); + $table->boolean('analyze'); + }); + + $model = new class extends Model + { + protected $table = 'actions'; + protected $guarded = ['id']; + public $timestamps = false; + }; + + $model->newInstance()->create([ + 'label' => 'test', + 'start' => '2023-01-01 00:00:00', + 'end' => '2024-01-01 00:00:00', + 'analyze' => true, + ]); + + $this->assertDatabaseHas('actions', [ + 'label' => 'test', + 'start' => '2023-01-01 00:00:00', + 'end' => '2024-01-01 00:00:00', + 'analyze' => true, + ]); + } } class TestModel1 extends Model From 7a2da50258c4d0f693b738d3f3c69b2693aea6c1 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 1 Nov 2023 08:52:17 -0500 Subject: [PATCH 017/207] version --- 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 0cb1696cca29..94769ee51d33 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.30.0'; + const VERSION = '10.30.1'; /** * The base path for the Laravel installation. From d0a6080db78a7e8a3316ff9c41eb86aa0866742d Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Wed, 1 Nov 2023 13:55:13 +0000 Subject: [PATCH 018/207] Update CHANGELOG --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e894786d56..53381a90f8dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.30.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.30.1...10.x) + +## [v10.30.1](https://github.com/laravel/framework/compare/v10.30.0...v10.30.1) - 2023-11-01 + +### What's Changed + +- [10.x] Fix postgreSQL reserved word column names w/ guarded attributes broken in native column attributes implementation by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/48877 + +**Full Changelog**: https://github.com/laravel/framework/compare/v10.30.0...v10.30.1 ## [v10.30.0](https://github.com/laravel/framework/compare/v10.29.0...v10.30.0) - 2023-10-31 From 444141b759b34b48f63d2a16c83978c1377faefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Guimar=C3=A3es?= Date: Wed, 1 Nov 2023 16:35:43 -0300 Subject: [PATCH 019/207] [11.x] Allow `SyncQueue` to dispatch jobs after a transaction is committed (#48860) * Fix after commit jobs using the sync queue * Additional test * Remove unnecessary check * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Queue/Queue.php | 2 +- src/Illuminate/Queue/SyncQueue.php | 22 ++++++++++++++ tests/Queue/QueueSyncQueueTest.php | 48 ++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Queue.php b/src/Illuminate/Queue/Queue.php index 0ce7ad1ac1ce..69fe00c4ddd1 100755 --- a/src/Illuminate/Queue/Queue.php +++ b/src/Illuminate/Queue/Queue.php @@ -326,7 +326,7 @@ function () use ($payload, $queue, $delay, $callback, $job) { */ protected function shouldDispatchAfterCommit($job) { - if (is_object($job) && $job instanceof ShouldQueueAfterCommit) { + if ($job instanceof ShouldQueueAfterCommit) { return true; } diff --git a/src/Illuminate/Queue/SyncQueue.php b/src/Illuminate/Queue/SyncQueue.php index f416b0232fd3..c687635ee3ea 100755 --- a/src/Illuminate/Queue/SyncQueue.php +++ b/src/Illuminate/Queue/SyncQueue.php @@ -34,6 +34,28 @@ public function size($queue = null) * @throws \Throwable */ public function push($job, $data = '', $queue = null) + { + if ($this->shouldDispatchAfterCommit($job) && + $this->container->bound('db.transactions')) { + return $this->container->make('db.transactions')->addCallback( + fn () => $this->executeJob($job, $data, $queue) + ); + } + + return $this->executeJob($job, $data, $queue); + } + + /** + * Execute a given job synchronously. + * + * @param string $job + * @param mixed $data + * @param string|null $queue + * @return int + * + * @throws \Throwable + */ + protected function executeJob($job, $data = '', $queue = null) { $queueJob = $this->resolveJob($this->createPayload($job, $queue, $data), $queue); diff --git a/tests/Queue/QueueSyncQueueTest.php b/tests/Queue/QueueSyncQueueTest.php index c833b87a30ca..a361bf5e773b 100755 --- a/tests/Queue/QueueSyncQueueTest.php +++ b/tests/Queue/QueueSyncQueueTest.php @@ -7,6 +7,8 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Queue\QueueableEntity; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; +use Illuminate\Database\DatabaseTransactionsManager; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Jobs\SyncJob; use Illuminate\Queue\SyncQueue; @@ -77,6 +79,32 @@ public function testCreatesPayloadObject() $this->assertSame('extraValue', $e->getMessage()); } } + + public function testItAddsATransactionCallbackForAfterCommitJobs() + { + $sync = new SyncQueue; + $container = new Container; + $container->bind(\Illuminate\Contracts\Container\Container::class, \Illuminate\Container\Container::class); + $transactionManager = m::mock(DatabaseTransactionsManager::class); + $transactionManager->shouldReceive('addCallback')->once()->andReturn(null); + $container->instance('db.transactions', $transactionManager); + + $sync->setContainer($container); + $sync->push(new SyncQueueAfterCommitJob()); + } + + public function testItAddsATransactionCallbackForInterfaceBasedAfterCommitJobs() + { + $sync = new SyncQueue; + $container = new Container; + $container->bind(\Illuminate\Contracts\Container\Container::class, \Illuminate\Container\Container::class); + $transactionManager = m::mock(DatabaseTransactionsManager::class); + $transactionManager->shouldReceive('addCallback')->once()->andReturn(null); + $container->instance('db.transactions', $transactionManager); + + $sync->setContainer($container); + $sync->push(new SyncQueueAfterCommitInterfaceJob()); + } } class SyncQueueTestEntity implements QueueableEntity @@ -134,3 +162,23 @@ public function getValueFromJob($key) return $payload['data'][$key] ?? null; } } + +class SyncQueueAfterCommitJob +{ + use InteractsWithQueue; + + public $afterCommit = true; + + public function handle() + { + } +} + +class SyncQueueAfterCommitInterfaceJob implements ShouldQueueAfterCommit +{ + use InteractsWithQueue; + + public function handle() + { + } +} From 65f00dde474a267cdb84a311a88018c50d1c83fe Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Thu, 2 Nov 2023 08:48:58 +0100 Subject: [PATCH 020/207] Update CHANGELOG.md --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53381a90f8dc..f70c7854e43a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,8 @@ ## [v10.30.1](https://github.com/laravel/framework/compare/v10.30.0...v10.30.1) - 2023-11-01 -### What's Changed - - [10.x] Fix postgreSQL reserved word column names w/ guarded attributes broken in native column attributes implementation by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/48877 -**Full Changelog**: https://github.com/laravel/framework/compare/v10.30.0...v10.30.1 - ## [v10.30.0](https://github.com/laravel/framework/compare/v10.29.0...v10.30.0) - 2023-10-31 - [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/48815 From 0fde761c0f895b30d9a60ac22b323aa9b50662eb Mon Sep 17 00:00:00 2001 From: James Hulse Date: Fri, 3 Nov 2023 02:47:02 +1300 Subject: [PATCH 021/207] [10.x] Allow `Sleep::until()` to be passed a timestamp as a string (#48883) * Allow Sleep::until to be passed a timestamp as a string Also loosens up the type hinting to match what is actually handled. * Add test for milliseconds --------- Co-authored-by: Tim MacDonald --- src/Illuminate/Support/Sleep.php | 4 ++-- tests/Support/SleepTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index 680dfb1490af..cd32d7f479c5 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -80,12 +80,12 @@ public static function for($duration) /** * Sleep until the given timestamp. * - * @param \DateTimeInterface|int $timestamp + * @param \DateTimeInterface|int|float|numeric-string $timestamp * @return static */ public static function until($timestamp) { - if (is_int($timestamp)) { + if (is_numeric($timestamp)) { $timestamp = Carbon::createFromTimestamp($timestamp); } diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index 1a75b8aeb429..d43b96a0d7f7 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -229,6 +229,32 @@ public function testItCanSleepTillGivenTimestamp() ]); } + public function testItCanSleepTillGivenTimestampAsString() + { + Sleep::fake(); + Carbon::setTestNow(now()->startOfDay()); + + Sleep::until(strval(now()->addMinute()->timestamp)); + + Sleep::assertSequence([ + Sleep::for(60)->seconds(), + ]); + } + + public function testItCanSleepTillGivenTimestampAsStringWithMilliseconds() + { + Sleep::fake(); + Carbon::setTestNow('2000-01-01 00:00:00.000'); // 946684800 + + Sleep::until('946684899.123'); + + Sleep::assertSequence([ + Sleep::for(1)->minute() + ->and(39)->seconds() + ->and(123)->milliseconds(), + ]); + } + public function testItSleepsForZeroTimeWithNegativeDateTime() { Sleep::fake(); From 8378ab20a1f2409f283f2c046a76c3e574e92221 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Fri, 3 Nov 2023 14:08:47 +0100 Subject: [PATCH 022/207] [10.x] Fix whereHasMorph() with nullable morphs (#48903) * Support nullable morphs in whereHasMorph() * Expand the whereHasMorph with nullable morph by using a sub condition * Support nullable morphs in whereDoesntHaveMorph and orWhereHasMorph * Fix testOrWhereHasMorphWithWildcardAndOnlyNullMorphTypes so there are no morph types in the database --- .../Concerns/QueriesRelationships.php | 4 ++ .../Database/EloquentWhereHasMorphTest.php | 40 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index dd0160adad13..154717fa4d81 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -230,6 +230,10 @@ public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boole $types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all(); } + if (empty($types)) { + return $this->where(new Expression('0'), $operator, $count, $boolean); + } + foreach ($types as &$type) { $type = Relation::getMorphedModel($type) ?? $type; } diff --git a/tests/Integration/Database/EloquentWhereHasMorphTest.php b/tests/Integration/Database/EloquentWhereHasMorphTest.php index a662ea73e04f..73dffc85cb5f 100644 --- a/tests/Integration/Database/EloquentWhereHasMorphTest.php +++ b/tests/Integration/Database/EloquentWhereHasMorphTest.php @@ -27,7 +27,7 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() Schema::create('comments', function (Blueprint $table) { $table->increments('id'); - $table->morphs('commentable'); + $table->nullableMorphs('commentable'); $table->softDeletes(); }); @@ -41,6 +41,8 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() $models[] = Video::create(['title' => 'foo']); $models[] = Video::create(['title' => 'bar']); $models[] = Video::create(['title' => 'baz']); + $models[] = null; // deleted + $models[] = null; // deleted foreach ($models as $model) { (new Comment)->commentable()->associate($model)->save(); @@ -103,6 +105,19 @@ public function testWhereHasMorphWithWildcardAndMorphMap() } } + public function testWhereHasMorphWithWildcardAndOnlyNullMorphTypes() + { + Comment::whereNotNull('commentable_type')->forceDelete(); + + $comments = Comment::query() + ->whereHasMorph('commentable', '*', function (Builder $query) { + $query->where('title', 'foo'); + }) + ->orderBy('id')->get(); + + $this->assertEmpty($comments->pluck('id')->all()); + } + public function testWhereHasMorphWithRelationConstraint() { $comments = Comment::whereHasMorph('commentableWithConstraint', Video::class, function (Builder $query) { @@ -190,6 +205,18 @@ public function testOrWhereHasMorph() $this->assertEquals([1, 4], $comments->pluck('id')->all()); } + public function testOrWhereHasMorphWithWildcardAndOnlyNullMorphTypes() + { + Comment::whereNotNull('commentable_type')->forceDelete(); + + $comments = Comment::where('id', 7) + ->orWhereHasMorph('commentable', '*', function (Builder $query) { + $query->where('title', 'foo'); + })->orderBy('id')->get(); + + $this->assertEquals([7], $comments->pluck('id')->all()); + } + public function testWhereDoesntHaveMorph() { $comments = Comment::whereDoesntHaveMorph('commentable', Post::class, function (Builder $query) { @@ -199,6 +226,17 @@ public function testWhereDoesntHaveMorph() $this->assertEquals([2, 3], $comments->pluck('id')->all()); } + public function testWhereDoesntHaveMorphWithWildcardAndOnlyNullMorphTypes() + { + Comment::whereNotNull('commentable_type')->forceDelete(); + + $comments = Comment::whereDoesntHaveMorph('commentable', [], function (Builder $query) { + $query->where('title', 'foo'); + })->orderBy('id')->get(); + + $this->assertEquals([7, 8], $comments->pluck('id')->all()); + } + public function testOrWhereDoesntHaveMorph() { $comments = Comment::where('id', 1) From 0ca0d328eefceebd443f59b0ad582e1082acfe1b Mon Sep 17 00:00:00 2001 From: Florian Vick Date: Fri, 3 Nov 2023 14:09:12 +0100 Subject: [PATCH 023/207] Handle `class_parents` returning false in `class_uses_recursive` (#48902) This will now take into account that `class_parents` can return false on error which would result in a boolean passed into `array_reverse`. --- 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 11348fa4f296..991f403f32d9 100755 --- a/src/Illuminate/Support/helpers.php +++ b/src/Illuminate/Support/helpers.php @@ -91,7 +91,7 @@ function class_uses_recursive($class) $results = []; - foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) { + foreach (array_reverse(class_parents($class) ?: []) + [$class => $class] as $class) { $results += trait_uses_recursive($class); } From 9c735447ffa04ce552bbc9287886e7746b64a330 Mon Sep 17 00:00:00 2001 From: Alexandr Chernyaev Date: Fri, 3 Nov 2023 16:41:08 +0300 Subject: [PATCH 024/207] [10.x] Enable default retrieval of all fragments in `fragments()` and `fragmentsIf()` methods (#48894) * Enable default retrieval of all fragments in `fragments()` and `fragmentsIf()` methods * Fixed docblock * Update src/Illuminate/View/View.php Co-authored-by: Dries Vints * Update src/Illuminate/View/View.php Co-authored-by: Dries Vints * Update src/Illuminate/View/View.php Co-authored-by: Dries Vints * Update src/Illuminate/View/View.php Co-authored-by: Dries Vints * formatting --------- Co-authored-by: Dries Vints Co-authored-by: Taylor Otwell --- src/Illuminate/View/View.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/View/View.php b/src/Illuminate/View/View.php index efe6fceffd5e..da2f79b141fd 100755 --- a/src/Illuminate/View/View.php +++ b/src/Illuminate/View/View.php @@ -91,14 +91,16 @@ public function fragment($fragment) } /** - * Get the evaluated contents for a given array of fragments. + * Get the evaluated contents for a given array of fragments or return all fragments. * - * @param array $fragments + * @param array|null $fragments * @return string */ - public function fragments(array $fragments) + public function fragments(?array $fragments = null) { - return collect($fragments)->map(fn ($f) => $this->fragment($f))->implode(''); + return is_null($fragments) + ? $this->allFragments() + : collect($fragments)->map(fn ($f) => $this->fragment($f))->implode(''); } /** @@ -121,10 +123,10 @@ public function fragmentIf($boolean, $fragment) * Get the evaluated contents for a given array of fragments if the given condition is true. * * @param bool $boolean - * @param array $fragments + * @param array|null $fragments * @return string */ - public function fragmentsIf($boolean, array $fragments) + public function fragmentsIf($boolean, ?array $fragments = null) { if (value($boolean)) { return $this->fragments($fragments); @@ -133,6 +135,16 @@ public function fragmentsIf($boolean, array $fragments) return $this->render(); } + /** + * Get all fragments as a single string. + * + * @return string + */ + protected function allFragments() + { + return collect($this->render(fn() => $this->factory->getFragments()))->implode(''); + } + /** * Get the string contents of the view. * From f171d70e28ceb4a7aae18e727c07e5d28925b3b8 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 3 Nov 2023 13:41:31 +0000 Subject: [PATCH 025/207] Apply fixes from StyleCI --- src/Illuminate/View/View.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/View.php b/src/Illuminate/View/View.php index da2f79b141fd..cdc6f8dfb5ba 100755 --- a/src/Illuminate/View/View.php +++ b/src/Illuminate/View/View.php @@ -93,7 +93,7 @@ public function fragment($fragment) /** * Get the evaluated contents for a given array of fragments or return all fragments. * - * @param array|null $fragments + * @param array|null $fragments * @return string */ public function fragments(?array $fragments = null) @@ -142,7 +142,7 @@ public function fragmentsIf($boolean, ?array $fragments = null) */ protected function allFragments() { - return collect($this->render(fn() => $this->factory->getFragments()))->implode(''); + return collect($this->render(fn () => $this->factory->getFragments()))->implode(''); } /** From e5e67c4b85ac2b46e67657b6f6a9ff2b9578f3a8 Mon Sep 17 00:00:00 2001 From: Sebastien Armand Date: Fri, 3 Nov 2023 06:47:52 -0700 Subject: [PATCH 026/207] [10.x] Allow placing a batch on a chain (#48633) * Allow placing a batch on a chain * missing file * Chains of batches of chains of batches * remove auto-format * remove auto-format * just collection * chained comes from queueable * better function name * reflop * messed up indentation * styleci * allow batch to fail but chain to succeed * formatting * formatting * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/ChainedBatch.php | 130 +++++++++++ src/Illuminate/Bus/Dispatcher.php | 1 + tests/Integration/Queue/JobChainingTest.php | 236 ++++++++++++++++++++ 3 files changed, 367 insertions(+) create mode 100644 src/Illuminate/Bus/ChainedBatch.php diff --git a/src/Illuminate/Bus/ChainedBatch.php b/src/Illuminate/Bus/ChainedBatch.php new file mode 100644 index 000000000000..6d4f92107400 --- /dev/null +++ b/src/Illuminate/Bus/ChainedBatch.php @@ -0,0 +1,130 @@ +jobs = static::prepareNestedBatches($batch->jobs); + + $this->name = $batch->name; + $this->options = $batch->options; + } + + /** + * Prepare any nested batches within the given collection of jobs. + * + * @param \Illuminate\Support\Collection $jobs + * @return \Illuminate\Support\Collection + */ + public static function prepareNestedBatches(Collection $jobs): Collection + { + return $jobs->map(fn ($job) => match (true) { + is_array($job) => static::prepareNestedBatches(collect($job))->all(), + $job instanceof Collection => static::prepareNestedBatches($job), + $job instanceof PendingBatch => new ChainedBatch($job), + default => $job, + }); + } + + /** + * Handle the job. + * + * @return void + */ + public function handle() + { + $batch = new PendingBatch(Container::getInstance(), $this->jobs); + + $batch->name = $this->name; + $batch->options = $this->options; + + if ($this->queue) { + $batch->onQueue($this->queue); + } + + if ($this->connection) { + $batch->onConnection($this->connection); + } + + $this->dispatchRemainderOfChainAfterBatch($batch); + + foreach ($this->chainCatchCallbacks ?? [] as $callback) { + $batch->catch(function (Batch $batch, ?Throwable $exception) use ($callback) { + if (! $batch->allowsFailures()) { + $callback($exception); + } + }); + } + + $batch->dispatch(); + } + + /** + * Move the remainder of the chain to a "finally" batch callback. + * + * @param \Illuminate\Bus\PendingBatch $batch + * @return + */ + protected function dispatchRemainderOfChainAfterBatch(PendingBatch $batch) + { + if (! empty($this->chained)) { + $next = unserialize(array_shift($this->chained)); + + $next->chained = $this->chained; + + $next->onConnection($next->connection ?: $this->chainConnection); + $next->onQueue($next->queue ?: $this->chainQueue); + + $next->chainConnection = $this->chainConnection; + $next->chainQueue = $this->chainQueue; + $next->chainCatchCallbacks = $this->chainCatchCallbacks; + + $batch->finally(function (Batch $batch) use ($next) { + if (! $batch->cancelled()) { + Container::getInstance()->make(Dispatcher::class)->dispatch($next); + } + }); + + $this->chained = []; + } + } +} diff --git a/src/Illuminate/Bus/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index 8ed3a21b7c4f..86b19dfea394 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -163,6 +163,7 @@ public function batch($jobs) public function chain($jobs) { $jobs = Collection::wrap($jobs); + $jobs = ChainedBatch::prepareNestedBatches($jobs); return new PendingChain($jobs->shift(), $jobs->toArray()); } diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index b1d7b955fcc6..9226d3bd6dae 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -2,13 +2,16 @@ namespace Illuminate\Tests\Integration\Queue; +use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Queue; +use Illuminate\Support\Facades\Schema; use Orchestra\Testbench\TestCase; class JobChainingTest extends TestCase @@ -26,6 +29,26 @@ protected function getEnvironmentSetUp($app) ]); } + protected function setUp(): void + { + parent::setUp(); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + JobRunRecorder::reset(); + } + protected function tearDown(): void { JobChainingTestFirstJob::$ran = false; @@ -244,6 +267,136 @@ public function testChainJobsCanBeAppendedWithoutExistingChain() $this->assertNotNull(JobChainAddingAddedJob::$ranAt); } + + public function testBatchCanBeAddedToChain() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + new JobChainingTestBatchedJob('b1'), + new JobChainingTestBatchedJob('b2'), + new JobChainingTestBatchedJob('b3'), + new JobChainingTestBatchedJob('b4'), + ]), + new JobChainingNamedTestJob('c3'), + ])->dispatch(); + + $this->assertEquals(['c1', 'c2', 'b1', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + } + + public function testDynamicBatchCanBeAddedToChain() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + new JobChainingTestBatchedJob('b1'), + new JobChainingTestBatchedJob('b2', times: 4), + new JobChainingTestBatchedJob('b3'), + new JobChainingTestBatchedJob('b4'), + ]), + new JobChainingNamedTestJob('c3'), + ])->dispatch(); + + $this->assertEquals(['c1', 'c2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + } + + public function testChainBatchChain() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + [ + new JobChainingNamedTestJob('bc1'), + new JobChainingNamedTestJob('bc2'), + ], + new JobChainingTestBatchedJob('b1'), + new JobChainingTestBatchedJob('b2', times: 4), + new JobChainingTestBatchedJob('b3'), + new JobChainingTestBatchedJob('b4'), + ]), + new JobChainingNamedTestJob('c3'), + ])->dispatch(); + + $this->assertEquals(['c1', 'c2', 'bc1', 'bc2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + } + + public function testChainBatchChainBatch() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + [ + new JobChainingNamedTestJob('bc1'), + new JobChainingNamedTestJob('bc2'), + Bus::batch([ + new JobChainingTestBatchedJob('bb1'), + new JobChainingTestBatchedJob('bb2'), + ]), + ], + new JobChainingTestBatchedJob('b1'), + new JobChainingTestBatchedJob('b2', times: 4), + new JobChainingTestBatchedJob('b3'), + new JobChainingTestBatchedJob('b4'), + ]), + new JobChainingNamedTestJob('c3'), + ])->dispatch(); + + $this->assertEquals(['c1', 'c2', 'bc1', 'bc2', 'bb1', 'bb2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + } + + public function testBatchCatchCallbacks() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + new JobChainingTestFailingBatchedJob('fb1'), + ])->catch(fn () => JobRunRecorder::recordFailure('batch failed')), + new JobChainingNamedTestJob('c3'), + ])->catch(fn () => JobRunRecorder::recordFailure('chain failed'))->dispatch(); + + $this->assertEquals(['c1', 'c2'], JobRunRecorder::$results); + $this->assertEquals(['batch failed', 'chain failed'], JobRunRecorder::$failures); + } + + public function testChainBatchFailureAllowed() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + new JobChainingTestBatchedJob('b1'), + new JobChainingTestFailingBatchedJob('b2'), + new JobChainingTestBatchedJob('b3'), + ])->allowFailures()->catch(fn () => JobRunRecorder::recordFailure('batch failed')), + new JobChainingNamedTestJob('c3'), + ])->catch(fn () => JobRunRecorder::recordFailure('chain failed'))->dispatch(); + + $this->assertEquals(['c1', 'c2', 'b1', 'b3', 'c3'], JobRunRecorder::$results); + // Only the batch failed, but the chain should keep going since the batch allows failures + $this->assertEquals(['batch failed'], JobRunRecorder::$failures); + } + + public function testChainBatchFailureNotAllowed() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + new JobChainingNamedTestJob('c2'), + Bus::batch([ + new JobChainingTestBatchedJob('b1'), + new JobChainingTestFailingBatchedJob('b2'), + new JobChainingTestBatchedJob('b3'), + ])->allowFailures(false)->catch(fn () => JobRunRecorder::recordFailure('batch failed')), + new JobChainingNamedTestJob('c3'), + ])->catch(fn () => JobRunRecorder::recordFailure('chain failed'))->dispatch(); + + $this->assertEquals(['c1', 'c2', 'b1', 'b3'], JobRunRecorder::$results); + $this->assertEquals(['batch failed', 'chain failed'], JobRunRecorder::$failures); + } } class JobChainingTestFirstJob implements ShouldQueue @@ -251,7 +404,9 @@ class JobChainingTestFirstJob implements ShouldQueue use Dispatchable, Queueable; public static $ran = false; + public static $usedQueue = null; + public static $usedConnection = null; public function handle() @@ -267,7 +422,9 @@ class JobChainingTestSecondJob implements ShouldQueue use Dispatchable, Queueable; public static $ran = false; + public static $usedQueue = null; + public static $usedConnection = null; public function handle() @@ -283,7 +440,9 @@ class JobChainingTestThirdJob implements ShouldQueue use Dispatchable, Queueable; public static $ran = false; + public static $usedQueue = null; + public static $usedConnection = null; public function handle() @@ -382,3 +541,80 @@ public function handle() throw new \Exception(); } } + +class JobChainingNamedTestJob implements ShouldQueue +{ + use Batchable, Dispatchable, InteractsWithQueue, Queueable; + + public static $results = []; + + public string $id; + + public function __construct(string $id) + { + $this->id = $id; + } + + public function handle() + { + JobRunRecorder::record($this->id); + } +} + +class JobChainingTestBatchedJob implements ShouldQueue +{ + use Batchable, Dispatchable, InteractsWithQueue, Queueable; + + public string $id; + + public int $times; + + public function __construct(string $id, int $times = 0) + { + $this->id = $id; + $this->times = $times; + } + + public function handle() + { + for ($i = 0; $i < $this->times; $i++) { + $this->batch()->add(new JobChainingTestBatchedJob($this->id.'-'.$i)); + } + JobRunRecorder::record($this->id); + } +} + +class JobChainingTestFailingBatchedJob implements ShouldQueue +{ + use Batchable, Dispatchable, InteractsWithQueue, Queueable; + + public function handle() + { + $this->fail(); + } +} + +class JobRunRecorder +{ + public static $results = []; + + public static $failures = []; + + public static function record(string $id) + { + self::$results[] = $id; + } + + public static function recordFailure(string $message) + { + self::$failures[] = $message; + + return $message; + } + + public static function reset() + { + self::$results = []; + self::$failures = []; + } +} From 3d37d781d4bd299e17e4c58c6a85963747ac4762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Debrauwer?= Date: Fri, 3 Nov 2023 15:23:36 +0100 Subject: [PATCH 027/207] Update PendingRequest.php (#48900) --- src/Illuminate/Http/Client/PendingRequest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index cfab639cf511..ba910f833111 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -1008,6 +1008,10 @@ protected function makePromise(string $method, string $url, array $options = []) }); }) ->otherwise(function (TransferException $e) { + if ($e instanceof ConnectException) { + $this->dispatchConnectionFailedEvent(); + } + return $e instanceof RequestException && $e->hasResponse() ? $this->populateResponse($this->newResponse($e->getResponse())) : $e; }); } From ea4198a1dcb8dcaf5037f7f1f9cb5d9dc2e1a58e Mon Sep 17 00:00:00 2001 From: miladev95 <35612406+miladev95@users.noreply.github.com> Date: Mon, 6 Nov 2023 00:11:15 +0330 Subject: [PATCH 028/207] authenticate method refactored to use null coalescing operator (#48917) --- src/Illuminate/Auth/GuardHelpers.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Illuminate/Auth/GuardHelpers.php b/src/Illuminate/Auth/GuardHelpers.php index 21094bf8a82b..9cdf69776c2e 100644 --- a/src/Illuminate/Auth/GuardHelpers.php +++ b/src/Illuminate/Auth/GuardHelpers.php @@ -33,11 +33,7 @@ trait GuardHelpers */ public function authenticate() { - if (! is_null($user = $this->user())) { - return $user; - } - - throw new AuthenticationException; + return $this->user() ?? throw new AuthenticationException; } /** From b9266bcbddc33acc9f6c3922653af93b48c2ae62 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 6 Nov 2023 21:49:50 +0000 Subject: [PATCH 029/207] Add support for Sec-Purpose header (#48925) --- src/Illuminate/Http/Request.php | 3 ++- tests/Http/HttpRequestTest.php | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index 251bb2c7c940..1a36ec2d0f1e 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -297,7 +297,8 @@ public function pjax() public function prefetch() { return strcasecmp($this->server->get('HTTP_X_MOZ') ?? '', 'prefetch') === 0 || - strcasecmp($this->headers->get('Purpose') ?? '', 'prefetch') === 0; + strcasecmp($this->headers->get('Purpose') ?? '', 'prefetch') === 0 || + strcasecmp($this->headers->get('Sec-Purpose') ?? '', 'prefetch') === 0; } /** diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 124f0bdbd33a..75a0cf5571af 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -254,6 +254,15 @@ public function testPrefetchMethod() $this->assertTrue($request->prefetch()); $request->headers->set('Purpose', 'Prefetch'); $this->assertTrue($request->prefetch()); + + $request->headers->remove('Purpose'); + + $request->headers->set('Sec-Purpose', ''); + $this->assertFalse($request->prefetch()); + $request->headers->set('Sec-Purpose', 'prefetch'); + $this->assertTrue($request->prefetch()); + $request->headers->set('Sec-Purpose', 'Prefetch'); + $this->assertTrue($request->prefetch()); } public function testPjaxMethod() From fe2ac4aa199ee6d32dfb22a7ddb197599864379e Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:10:29 +0100 Subject: [PATCH 030/207] Allow setting retain_visibility config option on Flysystem filesystems (#48935) --- src/Illuminate/Filesystem/FilesystemManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index abc146e14631..d81b31ff6e91 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -319,6 +319,7 @@ protected function createFlysystem(FlysystemAdapter $adapter, array $config) return new Flysystem($adapter, Arr::only($config, [ 'directory_visibility', 'disable_asserts', + 'retain_visibility', 'temporary_url', 'url', 'visibility', From 9cd047cc2076b969e2e7980dd0038997bf2aded4 Mon Sep 17 00:00:00 2001 From: matt-farrugia <35701810+matt-farrugia@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:17:03 +0000 Subject: [PATCH 031/207] [10.x] Escape forward slashes when exploding wildcard rules (#48936) * Escape forward slashes when exploding wildcard rules Array keys with a forward slash '/' passed into the validator will cause a preg_match(): Unknown modifier exception if they are validated using a rule keyed with a wildcard '*' This is due to the pattern being terminated early by the existence of the '/' in the pattern variable, this same fix is also used in Validator.php * Add test * Fix for continuous integration style --- .../Validation/ValidationRuleParser.php | 2 +- tests/Validation/ValidationRuleParserTest.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/ValidationRuleParser.php b/src/Illuminate/Validation/ValidationRuleParser.php index ec70a5260bb2..0469e0daa758 100644 --- a/src/Illuminate/Validation/ValidationRuleParser.php +++ b/src/Illuminate/Validation/ValidationRuleParser.php @@ -144,7 +144,7 @@ protected function prepareRule($rule, $attribute) */ protected function explodeWildcardRules($results, $attribute, $rules) { - $pattern = str_replace('\*', '[^\.]*', preg_quote($attribute)); + $pattern = str_replace('\*', '[^\.]*', preg_quote($attribute, '/')); $data = ValidationData::initializeAndGatherData($attribute, $this->data); diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php index db4ef31c04cb..acf9a4cd926e 100644 --- a/tests/Validation/ValidationRuleParserTest.php +++ b/tests/Validation/ValidationRuleParserTest.php @@ -199,6 +199,28 @@ public function testExplodeGeneratesNestedRulesForNonNestedData() $this->assertEquals([], $results->implicitAttributes); } + public function testExplodeHandlesForwardSlashesInWildcardRule() + { + $parser = (new ValidationRuleParser([ + 'redirects' => [ + 'directory/subdirectory/file' => [ + 'directory/subdirectory/redirectedfile', + ], + ], + ])); + + $results = $parser->explode([ + 'redirects.directory/subdirectory/file.*' => 'string', + ]); + + $this->assertEquals([ + 'redirects.directory/subdirectory/file.0' => ['string'], + ], $results->rules); + $this->assertEquals([ + 'redirects.directory/subdirectory/file.*' => ['redirects.directory/subdirectory/file.0'], + ], $results->implicitAttributes); + } + public function testExplodeHandlesArraysOfNestedRules() { $parser = (new ValidationRuleParser([ From 507ce9b28bce4b5e4140c28943092ca38e9a52e4 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 7 Nov 2023 07:48:30 -0600 Subject: [PATCH 032/207] version --- 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 94769ee51d33..7cffa8813078 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.30.1'; + const VERSION = '10.31.0'; /** * The base path for the Laravel installation. From 02b77ff27477074b284319840abe1303f5818f10 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 7 Nov 2023 14:24:33 +0000 Subject: [PATCH 033/207] Update CHANGELOG --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f70c7854e43a..3f05920b60d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.30.1...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.31.0...10.x) + +## [v10.31.0](https://github.com/laravel/framework/compare/v10.30.1...v10.31.0) - 2023-11-07 + +- [10.x] Allow `Sleep::until()` to be passed a timestamp as a string by [@jameshulse](https://github.com/jameshulse) in https://github.com/laravel/framework/pull/48883 +- [10.x] Fix whereHasMorph() with nullable morphs by [@MarkKremer](https://github.com/MarkKremer) in https://github.com/laravel/framework/pull/48903 +- [10.x] Handle `class_parents` returning false in `class_uses_recursive` by [@RoflCopter24](https://github.com/RoflCopter24) in https://github.com/laravel/framework/pull/48902 +- [10.x] Enable default retrieval of all fragments in `fragments()` and `fragmentsIf()` methods by [@tabuna](https://github.com/tabuna) in https://github.com/laravel/framework/pull/48894 +- [10.x] Allow placing a batch on a chain by [@khepin](https://github.com/khepin) in https://github.com/laravel/framework/pull/48633 +- [10.x] Dispatch 'connection failed' event in async http client request by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/48900 +- authenticate method refactored to use null coalescing operator by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/48917 +- [10.x] Add support for Sec-Purpose header by [@nanos](https://github.com/nanos) in https://github.com/laravel/framework/pull/48925 +- [10.x] Allow setting retain_visibility config option on Flysystem filesystems by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/48935 +- [10.x] Escape forward slashes when exploding wildcard rules by [@matt-farrugia](https://github.com/matt-farrugia) in https://github.com/laravel/framework/pull/48936 ## [v10.30.1](https://github.com/laravel/framework/compare/v10.30.0...v10.30.1) - 2023-11-01 From aacd0669c122f462656a8f9fb48a24e5f8c3f6fb Mon Sep 17 00:00:00 2001 From: Matt Kingshott <51963402+mattkingshott@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:24:32 +0000 Subject: [PATCH 034/207] Update PendingRequest.php (#48939) * Update PendingRequest.php This fixes #48938 by making the parameter a union type and adding `OutOfBoundsException`. * Update PendingRequest.php Fix styling. * Update PendingRequest.php * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Http/Client/PendingRequest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index ba910f833111..78e361feffad 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -23,6 +23,7 @@ use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; use JsonSerializable; +use OutOfBoundsException; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; use RuntimeException; @@ -1007,7 +1008,7 @@ protected function makePromise(string $method, string $url, array $options = []) $this->dispatchResponseReceivedEvent($response); }); }) - ->otherwise(function (TransferException $e) { + ->otherwise(function (OutOfBoundsException|TransferException $e) { if ($e instanceof ConnectException) { $this->dispatchConnectionFailedEvent(); } From e79eb86cada68b514e8328ff3276867ba15a3a3a Mon Sep 17 00:00:00 2001 From: miladev95 <35612406+miladev95@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:49:58 +0330 Subject: [PATCH 035/207] Change array_key_exists with null coalescing assignment operator to simplify the assignment of Content-Type and Content-Length headers (#48943) --- src/Illuminate/Filesystem/FilesystemAdapter.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 4a4fa1486b64..a8e260d246d7 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -289,13 +289,8 @@ public function response($path, $name = null, array $headers = [], $disposition { $response = new StreamedResponse; - if (! array_key_exists('Content-Type', $headers)) { - $headers['Content-Type'] = $this->mimeType($path); - } - - if (! array_key_exists('Content-Length', $headers)) { - $headers['Content-Length'] = $this->size($path); - } + $headers['Content-Type'] ??= $this->mimeType($path); + $headers['Content-Length'] ??= $this->size($path); if (! array_key_exists('Content-Disposition', $headers)) { $filename = $name ?? basename($path); From fc8f819bef5b4843baadda393c51320ceeb41a70 Mon Sep 17 00:00:00 2001 From: Orkhan Ahmadov Date: Wed, 8 Nov 2023 15:20:48 +0100 Subject: [PATCH 036/207] [10.x] Use container to resolve email validator class (#48942) * Update ValidatesAttributes.php * Use container to resolve EmailValidator --- src/Illuminate/Validation/Concerns/ValidatesAttributes.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 9b131c01dd5e..091681c34cb6 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -15,6 +15,7 @@ use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; use Egulias\EmailValidator\Validation\RFCValidation; use Exception; +use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Exceptions\MathException; @@ -840,7 +841,9 @@ public function validateEmail($attribute, $value, $parameters) ->values() ->all() ?: [new RFCValidation]; - return (new EmailValidator)->isValid($value, new MultipleValidationWithAnd($validations)); + $emailValidator = Container::getInstance()->make(EmailValidator::class); + + return $emailValidator->isValid($value, new MultipleValidationWithAnd($validations)); } /** From 61988b9337aa65c08f35fd82f49c6a46175e75d5 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 8 Nov 2023 19:13:32 +0100 Subject: [PATCH 037/207] [10.x] Added `getGlobalMiddleware` method to HTTP Client Factory (#48950) * Added `getGlobalMiddleware` method to HTTP Client Factory * Fix formatting * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Http/Client/Factory.php | 10 ++++++++++ src/Illuminate/Support/Facades/Http.php | 1 + tests/Http/HttpClientTest.php | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 6c24ed4ea08e..d3411ce18777 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -413,6 +413,16 @@ public function getDispatcher() return $this->dispatcher; } + /** + * Get the array of global middleware. + * + * @return array + */ + public function getGlobalMiddleware() + { + return $this->globalMiddleware; + } + /** * Execute a method against a new pending request instance. * diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index a6d9363d7d15..f551e1da9bc2 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -8,6 +8,7 @@ * @method static \Illuminate\Http\Client\Factory globalMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) + * @method static array getGlobalMiddleware() * @method static \GuzzleHttp\Promise\PromiseInterface response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \Illuminate\Http\Client\ResponseSequence sequence(array $responses = []) * @method static \Illuminate\Http\Client\Factory allowStrayRequests() diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 6e8b48710990..df4f7165373c 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -2593,6 +2593,13 @@ public function testItCanAddGlobalResponseMiddleware() $this->assertSame('Bar', $responses[1]->header('X-Foo')); } + public function testItCanGetTheGlobalMiddleware() + { + $this->factory->globalMiddleware($middleware = fn () => null); + + $this->assertEquals([$middleware], $this->factory->getGlobalMiddleware()); + } + public function testItCanAddRequestMiddleware() { $requests = []; From 9e95e46a43d66e19ba11522fb69b89789a41877d Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Wed, 8 Nov 2023 18:14:03 +0000 Subject: [PATCH 038/207] Update facade docblocks --- src/Illuminate/Support/Facades/Http.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index f551e1da9bc2..6a445e078055 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -8,7 +8,6 @@ * @method static \Illuminate\Http\Client\Factory globalMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) - * @method static array getGlobalMiddleware() * @method static \GuzzleHttp\Promise\PromiseInterface response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \Illuminate\Http\Client\ResponseSequence sequence(array $responses = []) * @method static \Illuminate\Http\Client\Factory allowStrayRequests() @@ -21,6 +20,7 @@ * @method static void assertSequencesAreEmpty() * @method static \Illuminate\Support\Collection recorded(callable $callback = null) * @method static \Illuminate\Contracts\Events\Dispatcher|null getDispatcher() + * @method static array getGlobalMiddleware() * @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 79b3fa38a27e7390f337666fa2f9c3bc2c13ed02 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:15:31 -0500 Subject: [PATCH 039/207] [10.x] Detect MySQL read-only mode error as a lost connection (#48937) * detect read-only mode error as a lost connection * re-add error string --- src/Illuminate/Database/DetectsLostConnections.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index 36600c7649d1..32492eb0729c 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -70,6 +70,7 @@ protected function causedByLostConnection(Throwable $e) 'SQLSTATE[HY000] [2002] Network is unreachable', 'SQLSTATE[HY000] [2002] The requested address is not valid in its context', 'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network', + 'SQLSTATE[HY000]: General error: 3989', ]); } } From f58ec8c83cf655020d4c9ac3a21f8a1b9dfeeeff Mon Sep 17 00:00:00 2001 From: Diamond <7268931+diamondobama@users.noreply.github.com> Date: Wed, 8 Nov 2023 20:19:02 +0200 Subject: [PATCH 040/207] Adds more implicit validation rules for `present` based on other fields (#48908) --- .../Translation/lang/en/validation.php | 4 + .../Concerns/ReplacesAttributes.php | 62 +++++++++ .../Concerns/ValidatesAttributes.php | 80 +++++++++++ src/Illuminate/Validation/Validator.php | 4 + tests/Validation/ValidationValidatorTest.php | 128 ++++++++++++++++++ 5 files changed, 278 insertions(+) diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index 4f8f726210fa..bd3eecd7d014 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -121,6 +121,10 @@ 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', ], 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', 'prohibited' => 'The :attribute field is prohibited.', 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index 660818ed0d08..23efc2303115 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -357,6 +357,68 @@ protected function replaceMimes($message, $attribute, $rule, $parameters) return str_replace(':values', implode(', ', $parameters), $message); } + /** + * Replace all place-holders for the present_if rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replacePresentIf($message, $attribute, $rule, $parameters) + { + $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); + $parameters[0] = $this->getDisplayableAttribute($parameters[0]); + + return str_replace([':other', ':value'], $parameters, $message); + } + + /** + * Replace all place-holders for the present_unless rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replacePresentUnless($message, $attribute, $rule, $parameters) + { + return str_replace([':other', ':value'], [ + $this->getDisplayableAttribute($parameters[0]), + $this->getDisplayableValue($parameters[0], $parameters[1]), + ], $message); + } + + /** + * Replace all place-holders for the present_with rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replacePresentWith($message, $attribute, $rule, $parameters) + { + return str_replace(':values', implode(' / ', $this->getAttributeList($parameters)), $message); + } + + /** + * Replace all place-holders for the present_with_all rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replacePresentWithAll($message, $attribute, $rule, $parameters) + { + return $this->replacePresentWith($message, $attribute, $rule, $parameters); + } + /** * Replace all place-holders for the required_with rule. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 091681c34cb6..4b11c28f5e30 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1728,6 +1728,86 @@ public function validatePresent($attribute, $value) return Arr::has($this->data, $attribute); } + /** + * Validate that an attribute is present when another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param array $parameters + * @return bool + */ + public function validatePresentIf($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'present_if'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (in_array($other, $values, is_bool($other) || is_null($other))) { + return $this->validatePresent($attribute, $value, $parameters); + } + + return true; + } + + /** + * Validate that an attribute is present unless another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param array $parameters + * @return bool + */ + public function validatePresentUnless($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'present_unless'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (! in_array($other, $values, is_bool($other) || is_null($other))) { + return $this->validatePresent($attribute, $value, $parameters); + } + + return true; + } + + /** + * Validate that an attribute is present when any given attribute is present. + * + * @param string $attribute + * @param mixed $value + * @param array $parameters + * @return bool + */ + public function validatePresentWith($attribute, $value, $parameters) + { + $this->requireParameterCount(1, $parameters, 'present_with'); + + if (Arr::hasAny($this->data, $parameters)) { + return $this->validatePresent($attribute, $value, $parameters); + } + + return true; + } + + /** + * Validate that an attribute is present when all given attributes are present. + * + * @param string $attribute + * @param mixed $value + * @param array $parameters + * @return bool + */ + public function validatePresentWithAll($attribute, $value, $parameters) + { + $this->requireParameterCount(1, $parameters, 'present_with_all'); + + if (Arr::has($this->data, $parameters)) { + return $this->validatePresent($attribute, $value, $parameters); + } + + return true; + } + /** * Validate that an attribute passes a regular expression check. * diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 579e82135282..0629287d6e8a 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -213,6 +213,10 @@ class Validator implements ValidatorContract 'MissingWith', 'MissingWithAll', 'Present', + 'PresentIf', + 'PresentUnless', + 'PresentWith', + 'PresentWithAll', 'Required', 'RequiredIf', 'RequiredIfAccepted', diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 51133741082d..925f463b7da6 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1120,6 +1120,134 @@ public function testValidatePresent() $this->assertTrue($v->passes()); } + public function testValidatePresentIf() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.present_if' => 'The :attribute field must be present when :other is :value.'], 'en'); + + $v = new Validator($trans, ['bar' => 1], ['foo' => 'present_if:bar,1']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo field must be present when bar is 1.', $v->errors()->first('foo')); + + $v = new Validator($trans, ['bar' => 1, 'foo' => null], ['foo' => 'present_if:bar,2']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 1, 'foo' => ''], ['foo' => 'present_if:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 1, 'foo' => [['name' => 'a']]], ['foo.*.id' => 'present_if:bar,1']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo.0.id field must be present when bar is 1.', $v->errors()->first('foo.0.id')); + + $v = new Validator($trans, ['bar' => 1, 'foo' => [['id' => '', 'name' => 'a']]], ['foo.*.id' => 'present_if:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 1, 'foo' => [['id' => null, 'name' => 'a']]], ['foo.*.id' => 'present_if:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 1, 'foo' => '2'], ['foo' => 'present_if:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2], ['foo' => 'present_if:bar,1']); + $this->assertTrue($v->passes()); + } + + public function testValidatePresentUnless() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.present_unless' => 'The :attribute field must be present unless :other is :value.'], 'en'); + + $v = new Validator($trans, ['bar' => 2], ['foo' => 'present_unless:bar,1']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo field must be present unless bar is 1.', $v->errors()->first('foo')); + + $v = new Validator($trans, ['bar' => 2, 'foo' => null], ['foo' => 'present_unless:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2, 'foo' => ''], ['foo' => 'present_unless:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2, 'foo' => [['name' => 'a']]], ['foo.*.id' => 'present_unless:bar,1']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo.0.id field must be present unless bar is 1.', $v->errors()->first('foo.0.id')); + + $v = new Validator($trans, ['bar' => 2, 'foo' => [['id' => '', 'name' => 'a']]], ['foo.*.id' => 'present_unless:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2, 'foo' => [['id' => null, 'name' => 'a']]], ['foo.*.id' => 'present_unless:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2, 'foo' => '2'], ['foo' => 'present_unless:bar,1']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 1], ['foo' => 'present_unless:bar,1']); + $this->assertTrue($v->passes()); + } + + public function testValidatePresentWith() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.present_with' => 'The :attribute field must be present when :values is present.'], 'en'); + + $v = new Validator($trans, ['foo' => 1, 'bar' => 2], ['foo' => 'present_with:bar']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => null, 'bar' => 2], ['foo' => 'present_with:bar']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => '', 'bar' => 2], ['foo' => 'present_with:bar']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => [['name' => 'a']], 'bar' => 2], ['foo.*.id' => 'present_with:bar']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo.0.id field must be present when bar is present.', $v->errors()->first('foo.0.id')); + + $v = new Validator($trans, ['foo' => [['id' => '']], 'bar' => 2], ['foo.*.id' => 'present_with:bar']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => [['id' => null]], 'bar' => 2], ['foo.*.id' => 'present_with:bar']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => 1], ['foo' => 'present_with:bar']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2], ['foo' => 'present_with:bar']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo field must be present when bar is present.', $v->errors()->first('foo')); + } + + public function testValidatePresentWithAll() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.present_with_all' => 'The :attribute field must be present when :values are present.'], 'en'); + + $v = new Validator($trans, ['foo' => 1, 'bar' => 2, 'baz' => 1], ['foo' => 'present_with_all:bar,baz']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => null, 'bar' => 2, 'baz' => 1], ['foo' => 'present_with_all:bar,baz']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => '', 'bar' => 2, 'baz' => 1], ['foo' => 'present_with_all:bar,baz']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => [['name' => 'a']], 'bar' => 2, 'baz' => 1], ['foo.*.id' => 'present_with_all:bar,baz']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo.0.id field must be present when bar / baz are present.', $v->errors()->first('foo.0.id')); + + $v = new Validator($trans, ['foo' => [['id' => '']], 'bar' => 2, 'baz' => 1], ['foo.*.id' => 'present_with_all:bar,baz']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => [['id' => null]], 'bar' => 2, 'baz' => 1], ['foo.*.id' => 'present_with_all:bar,baz']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['foo' => 1, 'bar' => 2], ['foo' => 'present_with_all:bar,baz']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['bar' => 2, 'baz' => 1], ['foo' => 'present_with_all:bar,baz']); + $this->assertFalse($v->passes()); + $this->assertSame('The foo field must be present when bar / baz are present.', $v->errors()->first('foo')); + } + public function testValidateRequired() { $trans = $this->getIlluminateArrayTranslator(); From be2175758bd88eb1a53c21a020988bbb2642c52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Debrauwer?= Date: Thu, 9 Nov 2023 00:14:53 +0100 Subject: [PATCH 041/207] [11.x] Support retry and throw on async http client request (in a http client request pool) (#48906) * Support throw and retry in async request * Small fixes * Fix linting issues * Fix another linting issue * Throw after last retry * Provide attempt and exception to retryDelay closure * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Http/Client/PendingRequest.php | 61 +++- tests/Http/HttpClientTest.php | 270 ++++++++++++++++++ 2 files changed, 330 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 0ddc69f6bc35..984f8d25a658 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -997,9 +997,10 @@ protected function parseMultipartBodyFormat(array $data) * @param string $method * @param string $url * @param array $options + * @param int $attempt * @return \GuzzleHttp\Promise\PromiseInterface */ - protected function makePromise(string $method, string $url, array $options = []) + protected function makePromise(string $method, string $url, array $options = [], int $attempt = 1) { return $this->promise = $this->sendRequest($method, $url, $options) ->then(function (MessageInterface $message) { @@ -1011,12 +1012,70 @@ protected function makePromise(string $method, string $url, array $options = []) ->otherwise(function (TransferException $e) { if ($e instanceof ConnectException) { $this->dispatchConnectionFailedEvent(); + + return new ConnectionException($e->getMessage(), 0, $e); } return $e instanceof RequestException && $e->hasResponse() ? $this->populateResponse($this->newResponse($e->getResponse())) : $e; + }) + ->then(function (Response|ConnectionException|TransferException $response) use ($method, $url, $options, $attempt) { + return $this->handlePromiseResponse($response, $method, $url, $options, $attempt); }); } + /** + * Handle the response of an asynchronous request. + * + * @param \Illuminate\Http\Client\Response $response + * @param string $method + * @param string $url + * @param array $options + * @param int $attempt + * @return mixed + */ + protected function handlePromiseResponse(Response|ConnectionException|TransferException $response, $method, $url, $options, $attempt) + { + if ($response instanceof Response && $response->successful()) { + return $response; + } + + if ($response instanceof RequestException) { + $response = $this->populateResponse($this->newResponse($response->getResponse())); + } + + try { + $shouldRetry = $this->retryWhenCallback ? call_user_func( + $this->retryWhenCallback, + $response instanceof Response ? $response->toException() : $response, + $this + ) : true; + } catch (Exception $exception) { + return $exception; + } + + if ($attempt < $this->tries && $shouldRetry) { + $options['delay'] = value($this->retryDelay, $attempt, $response->toException()); + + return $this->makePromise($method, $url, $options, $attempt + 1); + } + + if ($response instanceof Response && + $this->throwCallback && + ($this->throwIfCallback === null || call_user_func($this->throwIfCallback, $response))) { + try { + $response->throw($this->throwCallback); + } catch (Exception $exception) { + return $exception; + } + } + + if ($this->tries > 1 && $this->retryThrow) { + return $response instanceof Response ? $response->toException() : $response; + } + + return $response; + } + /** * Send a request either synchronously or asynchronously. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 7d59b2c3822d..e32f01e6a68b 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -1837,6 +1837,132 @@ public function testRequestsWillBeWaitingSleepMillisecondsReceivedBeforeRetry() ]); } + public function testRequestExceptionReturnedWhenRetriesExhaustedInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->retry(2, 1000, null, true)->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + + $this->factory->assertSentCount(2); + } + + public function testRequestExceptionIsReturnedWithoutRetriesIfRetryNotNecessaryInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 500), + ]); + + $whenAttempts = collect(); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->retry(2, 1000, function ($exception) use ($whenAttempts) { + $whenAttempts->push($exception); + + return $exception->response->status() === 403; + }, true)->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + + $this->assertCount(1, $whenAttempts); + + $this->factory->assertSentCount(1); + } + + public function testRequestExceptionIsNotReturnedWhenDisabledAndRetriesExhaustedInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + [$response] = $this->factory->pool(fn ($pool) => [ + $pool->retry(2, 1000, null, false)->get('http://foo.com/get'), + ]); + + $this->assertNotNull($response); + $this->assertInstanceOf(Response::class, $response); + $this->assertTrue($response->failed()); + + $this->factory->assertSentCount(2); + } + + public function testRequestExceptionIsNotReturnedWithoutRetriesIfRetryNotNecessaryInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 500), + ]); + + $whenAttempts = collect(); + + [$response] = $this->factory->pool(fn ($pool) => [ + $pool->retry(2, 1000, function ($exception) use ($whenAttempts) { + $whenAttempts->push($exception); + + return $exception->response->status() === 403; + }, false)->get('http://foo.com/get'), + ]); + + $this->assertNotNull($response); + $this->assertInstanceOf(Response::class, $response); + $this->assertTrue($response->failed()); + + $this->assertCount(1, $whenAttempts); + + $this->factory->assertSentCount(1); + } + + public function testRequestCanBeModifiedInRetryCallbackInPool() + { + $this->factory->fake([ + '*' => $this->factory->sequence() + ->push(['error'], 500) + ->push(['ok'], 200), + ]); + + [$response] = $this->factory->pool(fn ($pool) => [ + $pool->retry(2, 1000, function ($exception, $request) { + $this->assertInstanceOf(PendingRequest::class, $request); + + $request->withHeaders(['Foo' => 'Bar']); + + return true; + }, false)->get('http://foo.com/get'), + ]); + + $this->assertTrue($response->successful()); + + $this->factory->assertSent(function (Request $request) { + return $request->hasHeader('Foo') && $request->header('Foo') === ['Bar']; + }); + } + + public function testExceptionThrownInRetryCallbackIsReturnedWithoutRetryingInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 500), + ]); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->retry(2, 1000, function ($exception) { + throw new Exception('Foo bar'); + }, false)->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(Exception::class, $exception); + $this->assertEquals('Foo bar', $exception->getMessage()); + + $this->factory->assertSentCount(1); + } + public function testMiddlewareRunsWhenFaked() { $this->factory->fake(function (Request $request) { @@ -2081,6 +2207,150 @@ public function testRequestExceptionIsNotThrownIfTheRequestDoesNotFail() $this->assertSame('{"result":{"foo":"bar"}}', $response->body()); } + public function testRequestExceptionIsNotReturnedIfThePendingRequestIsSetToThrowOnFailureButTheResponseIsSuccessfulInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['success'], 200), + ]); + + [$response] = $this->factory->pool(fn ($pool) => [ + $pool->throw()->get('http://foo.com/get'), + ]); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->status()); + } + + public function testRequestExceptionIsReturnedIfThePendingRequestIsSetToThrowOnFailureInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->throw()->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + } + + public function testRequestExceptionIsReturnedIfTheThrowIfOnThePendingRequestIsSetToTrueOnFailureInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->throwIf(true)->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + } + + public function testRequestExceptionIsNotReturnedIfTheThrowIfOnThePendingRequestIsSetToFalseOnFailureInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + [$response] = $this->factory->pool(fn ($pool) => [ + $pool->throwIf(false)->get('http://foo.com/get'), + ]); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(403, $response->status()); + } + + public function testRequestExceptionIsReturnedIfTheThrowIfClosureOnThePendingRequestReturnsTrueInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + $hitThrowCallback = collect(); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->throwIf(function ($response) { + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(403, $response->status()); + + return true; + }, function ($response, $e) use (&$hitThrowCallback) { + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(403, $response->status()); + + $this->assertInstanceOf(RequestException::class, $e); + + $hitThrowCallback->push(true); + })->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + $this->assertCount(1, $hitThrowCallback); + } + + public function testRequestExceptionIsNotReturnedIfTheThrowIfClosureOnThePendingRequestReturnsFalseInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + $hitThrowCallback = collect(); + + [$response] = $this->factory->pool(fn ($pool) => [ + $pool->throwIf(function ($response) { + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(403, $response->status()); + + return false; + }, function ($response, $e) use (&$hitThrowCallback) { + $hitThrowCallback->push(true); + })->get('http://foo.com/get'), + ]); + + $this->assertCount(0, $hitThrowCallback); + $this->assertSame(403, $response->status()); + } + + public function testRequestExceptionIsReturnedWithCallbackIfThePendingRequestIsSetToThrowOnFailureInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + $flag = collect(); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->throw(function ($exception) use (&$flag) { + $flag->push(true); + })->get('http://foo.com/get'), + ]); + + $this->assertCount(1, $flag); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + } + + public function testRequestExceptionIsReturnedAfterLastRetryInPool() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + [$exception] = $this->factory->pool(fn ($pool) => [ + $pool->retry(3)->throw()->get('http://foo.com/get'), + ]); + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + + $this->factory->assertSentCount(3); + } + public function testRequestExceptionIsThrowIfConditionIsSatisfied() { $this->factory->fake([ From 2703f3e85511d8a6eb70752fc5d5e2d9287d0712 Mon Sep 17 00:00:00 2001 From: Milad <35612406+miladev95@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:56:41 +0330 Subject: [PATCH 042/207] Use arrow function for set_error_handler callback (#48954) --- .../Testing/Concerns/InteractsWithDeprecationHandling.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDeprecationHandling.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDeprecationHandling.php index 7a914f7e01d2..6b9300ac3cb9 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDeprecationHandling.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDeprecationHandling.php @@ -21,9 +21,7 @@ trait InteractsWithDeprecationHandling protected function withDeprecationHandling() { if ($this->originalDeprecationHandler) { - set_error_handler(tap($this->originalDeprecationHandler, function () { - $this->originalDeprecationHandler = null; - })); + set_error_handler(tap($this->originalDeprecationHandler, fn () => $this->originalDeprecationHandler = null)); } return $this; From 0ff1229c4bbcd8e63fe1c0f3d295415dfa7d51fa Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 10 Nov 2023 21:47:17 +0800 Subject: [PATCH 043/207] [10.x] Test Improvements (#48962) * Test Improvements Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * Test Improvements Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- composer.json | 2 +- .../Testing/DatabaseMigrationsTest.php | 121 +++++++----------- .../Testing/RefreshDatabaseTest.php | 109 +++++----------- .../ConfigureCustomDoctrineTypeTest.php | 2 + .../Database/DBAL/TimestampTypeTest.php | 2 + .../Integration/Database/DatabaseLockTest.php | 13 +- .../Integration/Database/DatabaseTestCase.php | 2 +- tests/Integration/Queue/JobChainingTest.php | 20 +-- tests/Integration/Queue/JobEncryptionTest.php | 22 +--- tests/Integration/Queue/UniqueJobTest.php | 25 +--- tests/Integration/Queue/WorkCommandTest.php | 20 +-- 11 files changed, 100 insertions(+), 238 deletions(-) diff --git a/composer.json b/composer.json index a79770c3f587..d9d4a64a3783 100644 --- a/composer.json +++ b/composer.json @@ -105,7 +105,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.12", + "orchestra/testbench-core": "^8.15.1", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.0.7", diff --git a/tests/Foundation/Testing/DatabaseMigrationsTest.php b/tests/Foundation/Testing/DatabaseMigrationsTest.php index 0d14f7909c22..be759665dd78 100644 --- a/tests/Foundation/Testing/DatabaseMigrationsTest.php +++ b/tests/Foundation/Testing/DatabaseMigrationsTest.php @@ -2,52 +2,65 @@ namespace Illuminate\Tests\Foundation\Testing; +use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; +use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\RefreshDatabaseState; use Mockery as m; -use Orchestra\Testbench\Concerns\Testing; +use Orchestra\Testbench\Concerns\ApplicationTestingHooks; use Orchestra\Testbench\Foundation\Application as Testbench; use PHPUnit\Framework\TestCase; -use ReflectionMethod; use function Orchestra\Testbench\package_path; class DatabaseMigrationsTest extends TestCase { - protected $traitObject; + use ApplicationTestingHooks; + use DatabaseMigrations; + use InteractsWithConsole; + + public $dropViews = false; + + public $dropTypes = false; protected function setUp(): void { - $this->traitObject = m::mock(DatabaseMigrationsTestMockClass::class)->makePartial(); - $this->traitObject->setUp(); + RefreshDatabaseState::$migrated = false; + + $this->afterApplicationCreated(function () { + $this->app['config']->set([ + 'database.default' => 'testing', + 'database.connections.testing' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ], + ]); + }); + + $this->setUpTheApplicationTestingHooks(); + $this->withoutMockingConsoleOutput(); } protected function tearDown(): void { - $this->traitObject->tearDown(); - - if ($container = m::getContainer()) { - $this->addToAssertionCount($container->mockery_getExpectationCount()); - } + $this->tearDownTheApplicationTestingHooks(); - m::close(); + RefreshDatabaseState::$migrated = false; } - private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) + protected function refreshApplication() { - $migrateFreshUsingReflection = new ReflectionMethod( - get_class($this->traitObject), - $methodName + $this->app = Testbench::create( + basePath: package_path('vendor/orchestra/testbench-core/laravel'), ); - - return $migrateFreshUsingReflection; } public function testRefreshTestDatabaseDefault() { - $this->traitObject - ->shouldReceive('artisan') + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') ->once() ->with('migrate:fresh', [ '--drop-views' => false, @@ -55,17 +68,16 @@ public function testRefreshTestDatabaseDefault() '--seed' => false, ]); - $refreshTestDatabaseReflection = $this->__reflectAndSetupAccessibleForProtectedTraitMethod('runDatabaseMigrations'); - - $refreshTestDatabaseReflection->invoke($this->traitObject); + $this->runDatabaseMigrations(); } public function testRefreshTestDatabaseWithDropViewsOption() { - $this->traitObject->dropViews = true; + $this->dropViews = true; - $this->traitObject - ->shouldReceive('artisan') + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') ->once() ->with('migrate:fresh', [ '--drop-views' => true, @@ -73,17 +85,16 @@ public function testRefreshTestDatabaseWithDropViewsOption() '--seed' => false, ]); - $refreshTestDatabaseReflection = $this->__reflectAndSetupAccessibleForProtectedTraitMethod('runDatabaseMigrations'); - - $refreshTestDatabaseReflection->invoke($this->traitObject); + $this->runDatabaseMigrations(); } public function testRefreshTestDatabaseWithDropTypesOption() { - $this->traitObject->dropTypes = true; + $this->dropTypes = true; + + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); - $this->traitObject - ->shouldReceive('artisan') + $kernel->shouldReceive('call') ->once() ->with('migrate:fresh', [ '--drop-views' => false, @@ -91,52 +102,6 @@ public function testRefreshTestDatabaseWithDropTypesOption() '--seed' => false, ]); - $refreshTestDatabaseReflection = $this->__reflectAndSetupAccessibleForProtectedTraitMethod('runDatabaseMigrations'); - - $refreshTestDatabaseReflection->invoke($this->traitObject); - } -} - -class DatabaseMigrationsTestMockClass -{ - use DatabaseMigrations; - use InteractsWithConsole; - use Testing; - - public $dropViews = false; - - public $dropTypes = false; - - public function setUp() - { - RefreshDatabaseState::$migrated = false; - - $this->app = $this->refreshApplication(); - $this->withoutMockingConsoleOutput(); - } - - public function tearDown() - { - RefreshDatabaseState::$migrated = false; - - $this->callBeforeApplicationDestroyedCallbacks(); - $this->app?->flush(); - } - - protected function setUpTraits() - { - return []; - } - - protected function setUpTheTestEnvironmentTraitToBeIgnored(string $use): bool - { - return true; - } - - protected function refreshApplication() - { - return Testbench::create( - basePath: package_path('vendor/orchestra/testbench-core/laravel') - ); + $this->runDatabaseMigrations(); } } diff --git a/tests/Foundation/Testing/RefreshDatabaseTest.php b/tests/Foundation/Testing/RefreshDatabaseTest.php index 5a15a5d4958e..7a225c8d1e99 100644 --- a/tests/Foundation/Testing/RefreshDatabaseTest.php +++ b/tests/Foundation/Testing/RefreshDatabaseTest.php @@ -2,54 +2,55 @@ namespace Illuminate\Tests\Foundation\Testing; +use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; +use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabaseState; use Mockery as m; -use Orchestra\Testbench\Concerns\Testing; +use Orchestra\Testbench\Concerns\ApplicationTestingHooks; use Orchestra\Testbench\Foundation\Application as Testbench; use PHPUnit\Framework\TestCase; -use ReflectionMethod; use function Orchestra\Testbench\package_path; class RefreshDatabaseTest extends TestCase { - protected $traitObject; + use ApplicationTestingHooks; + use InteractsWithConsole; + use RefreshDatabase; + + public $dropViews = false; + + public $dropTypes = false; protected function setUp(): void { RefreshDatabaseState::$migrated = false; - $this->traitObject = m::mock(RefreshDatabaseTestMockClass::class)->makePartial(); - $this->traitObject->setUp(); + $this->setUpTheApplicationTestingHooks(); + $this->withoutMockingConsoleOutput(); } protected function tearDown(): void { - $this->traitObject->tearDown(); - - if ($container = m::getContainer()) { - $this->addToAssertionCount($container->mockery_getExpectationCount()); - } + $this->tearDownTheApplicationTestingHooks(); - m::close(); + RefreshDatabaseState::$migrated = false; } - private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) + protected function refreshApplication() { - $migrateFreshUsingReflection = new ReflectionMethod( - get_class($this->traitObject), - $methodName + $this->app = Testbench::create( + basePath: package_path('vendor/orchestra/testbench-core/laravel'), ); - - return $migrateFreshUsingReflection; } public function testRefreshTestDatabaseDefault() { - $this->traitObject - ->shouldReceive('artisan') + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') ->once() ->with('migrate:fresh', [ '--drop-views' => false, @@ -57,17 +58,16 @@ public function testRefreshTestDatabaseDefault() '--seed' => false, ]); - $refreshTestDatabaseReflection = $this->__reflectAndSetupAccessibleForProtectedTraitMethod('refreshTestDatabase'); - - $refreshTestDatabaseReflection->invoke($this->traitObject); + $this->refreshTestDatabase(); } public function testRefreshTestDatabaseWithDropViewsOption() { - $this->traitObject->dropViews = true; + $this->dropViews = true; - $this->traitObject - ->shouldReceive('artisan') + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); + + $kernel->shouldReceive('call') ->once() ->with('migrate:fresh', [ '--drop-views' => true, @@ -75,17 +75,16 @@ public function testRefreshTestDatabaseWithDropViewsOption() '--seed' => false, ]); - $refreshTestDatabaseReflection = $this->__reflectAndSetupAccessibleForProtectedTraitMethod('refreshTestDatabase'); - - $refreshTestDatabaseReflection->invoke($this->traitObject); + $this->refreshTestDatabase(); } public function testRefreshTestDatabaseWithDropTypesOption() { - $this->traitObject->dropTypes = true; + $this->dropTypes = true; + + $this->app->instance(ConsoleKernelContract::class, $kernel = m::spy(ConsoleKernel::class)); - $this->traitObject - ->shouldReceive('artisan') + $kernel->shouldReceive('call') ->once() ->with('migrate:fresh', [ '--drop-views' => false, @@ -93,52 +92,6 @@ public function testRefreshTestDatabaseWithDropTypesOption() '--seed' => false, ]); - $refreshTestDatabaseReflection = $this->__reflectAndSetupAccessibleForProtectedTraitMethod('refreshTestDatabase'); - - $refreshTestDatabaseReflection->invoke($this->traitObject); - } -} - -class RefreshDatabaseTestMockClass -{ - use InteractsWithConsole; - use RefreshDatabase; - use Testing; - - public $dropViews = false; - - public $dropTypes = false; - - public function setUp() - { - RefreshDatabaseState::$migrated = false; - - $this->app = $this->refreshApplication(); - $this->withoutMockingConsoleOutput(); - } - - public function tearDown() - { - RefreshDatabaseState::$migrated = false; - - $this->callBeforeApplicationDestroyedCallbacks(); - $this->app?->flush(); - } - - protected function setUpTraits() - { - return []; - } - - protected function setUpTheTestEnvironmentTraitToBeIgnored(string $use): bool - { - return true; - } - - public function refreshApplication() - { - return Testbench::create( - basePath: package_path('vendor/orchestra/testbench-core/laravel') - ); + $this->refreshTestDatabase(); } } diff --git a/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php b/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php index f0f36deb4e36..a11959c828f8 100644 --- a/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php +++ b/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php @@ -14,6 +14,8 @@ class ConfigureCustomDoctrineTypeTest extends DatabaseTestCase { protected function defineEnvironment($app) { + parent::defineEnvironment($app); + $app['config']['database.connections.sqlite.database'] = ':memory:'; $app['config']['database.dbal.types'] = [ 'bit' => MySQLBitType::class, diff --git a/tests/Integration/Database/DBAL/TimestampTypeTest.php b/tests/Integration/Database/DBAL/TimestampTypeTest.php index d7cec38c53b0..b8c2289d3961 100644 --- a/tests/Integration/Database/DBAL/TimestampTypeTest.php +++ b/tests/Integration/Database/DBAL/TimestampTypeTest.php @@ -11,6 +11,8 @@ class TimestampTypeTest extends DatabaseTestCase { protected function defineEnvironment($app) { + parent::defineEnvironment($app); + $app['config']['database.dbal.types'] = [ 'timestamp' => TimestampType::class, ]; diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php index 0c0beae75d8f..08f848e22543 100644 --- a/tests/Integration/Database/DatabaseLockTest.php +++ b/tests/Integration/Database/DatabaseLockTest.php @@ -2,22 +2,13 @@ namespace Illuminate\Tests\Integration\Database; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Schema; +use Orchestra\Testbench\Attributes\WithMigration; +#[WithMigration('cache')] class DatabaseLockTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() - { - Schema::create('cache_locks', function (Blueprint $table) { - $table->string('key')->primary(); - $table->string('owner'); - $table->integer('expiration'); - }); - } - public function testLockCanHaveASeparateConnection() { $this->app['config']->set('cache.stores.database.lock_connection', 'test'); diff --git a/tests/Integration/Database/DatabaseTestCase.php b/tests/Integration/Database/DatabaseTestCase.php index 4b08becfff1a..14f78bd71e0a 100644 --- a/tests/Integration/Database/DatabaseTestCase.php +++ b/tests/Integration/Database/DatabaseTestCase.php @@ -27,7 +27,7 @@ protected function setUp(): void parent::setUp(); } - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $connection = $app['config']->get('database.default'); diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 9226d3bd6dae..9dec15912fbd 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -5,17 +5,20 @@ use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Queue; -use Illuminate\Support\Facades\Schema; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; +#[WithMigration('queue')] class JobChainingTest extends TestCase { + use DatabaseMigrations; + public static $catchCallbackRan = false; protected function getEnvironmentSetUp($app) @@ -33,19 +36,6 @@ protected function setUp(): void { parent::setUp(); - Schema::create('job_batches', function (Blueprint $table) { - $table->string('id')->primary(); - $table->string('name'); - $table->integer('total_jobs'); - $table->integer('pending_jobs'); - $table->integer('failed_jobs'); - $table->longText('failed_job_ids'); - $table->mediumText('options')->nullable(); - $table->integer('cancelled_at')->nullable(); - $table->integer('created_at'); - $table->integer('finished_at')->nullable(); - }); - JobRunRecorder::reset(); } diff --git a/tests/Integration/Queue/JobEncryptionTest.php b/tests/Integration/Queue/JobEncryptionTest.php index 0bf9d15fa562..0042e24bf20c 100644 --- a/tests/Integration/Queue/JobEncryptionTest.php +++ b/tests/Integration/Queue/JobEncryptionTest.php @@ -6,17 +6,20 @@ use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Queue; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Illuminate\Tests\Integration\Database\DatabaseTestCase; +use Orchestra\Testbench\Attributes\WithMigration; +#[WithMigration('queue')] class JobEncryptionTest extends DatabaseTestCase { + use DatabaseMigrations; + protected function getEnvironmentSetUp($app) { parent::getEnvironmentSetUp($app); @@ -25,21 +28,6 @@ protected function getEnvironmentSetUp($app) $app['config']->set('queue.default', 'database'); } - protected function setUp(): void - { - parent::setUp(); - - Schema::create('jobs', function (Blueprint $table) { - $table->bigIncrements('id'); - $table->string('queue')->index(); - $table->longText('payload'); - $table->unsignedTinyInteger('attempts'); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); - $table->unsignedInteger('created_at'); - }); - } - protected function tearDown(): void { JobEncryptionTestEncryptedJob::$ran = false; diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index be5dfe3c1c4f..eb259a73fb9b 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -8,34 +8,17 @@ use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Bus; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; +#[WithMigration('queue')] class UniqueJobTest extends TestCase { - protected function getEnvironmentSetUp($app) - { - $app['db']->connection()->getSchemaBuilder()->create('jobs', function (Blueprint $table) { - $table->bigIncrements('id'); - $table->string('queue'); - $table->longText('payload'); - $table->tinyInteger('attempts')->unsigned(); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); - $table->unsignedInteger('created_at'); - $table->index(['queue', 'reserved_at']); - }); - } - - protected function tearDown(): void - { - $this->app['db']->connection()->getSchemaBuilder()->drop('jobs'); - - parent::tearDown(); - } + use DatabaseMigrations; public function testUniqueJobsAreNotDispatched() { diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 8ba2e90fb6a7..968b1c5d3548 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -4,32 +4,20 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Support\Carbon; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; use Queue; +#[WithMigration('queue')] class WorkCommandTest extends TestCase { - protected function getEnvironmentSetUp($app) - { - $app['db']->connection()->getSchemaBuilder()->create('jobs', function (Blueprint $table) { - $table->bigIncrements('id'); - $table->string('queue'); - $table->longText('payload'); - $table->tinyInteger('attempts')->unsigned(); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); - $table->unsignedInteger('created_at'); - $table->index(['queue', 'reserved_at']); - }); - } + use DatabaseMigrations; protected function tearDown(): void { - $this->app['db']->connection()->getSchemaBuilder()->drop('jobs'); - parent::tearDown(); FirstJob::$ran = false; From 7faedf1134772a4c4cdc25fbcf81916735da3e91 Mon Sep 17 00:00:00 2001 From: Juan Lago Date: Fri, 10 Nov 2023 20:27:13 +0100 Subject: [PATCH 044/207] Prevent to raise an exception when compiled blade template was already removed. (#48957) Co-authored-by: Juan Lago --- src/Illuminate/View/Compilers/BladeCompiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 18d1978af176..51479e81edc5 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -332,7 +332,7 @@ public function render() return tap($view->render(), function () use ($view, $deleteCachedView) { if ($deleteCachedView) { - unlink($view->getPath()); + @unlink($view->getPath()); } }); } From 95e924d8f624bb15ec36ac4dbd20f4726ce734eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Guimar=C3=A3es?= Date: Fri, 10 Nov 2023 16:49:32 -0300 Subject: [PATCH 045/207] [10.x] Fix how nested transaction callbacks are handled (#48859) * failing test * naive solution * Add comment * Revert "naive solution" This reverts commit ef4604928c933d27280f24f082cdda13b4e8cc4c. * wip * Fix DatabaseTransactionsManager tests * fix null object * Fix Testing DatabaseTransactionsManager * make styleci happy * additional test * Pass connection name to stageTransactions * Add unit test * add tests for Testing DatabaseTransactionsManager * 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 * formatting --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: Mior Muhammad Zaki Co-authored-by: Taylor Otwell --- .../Database/Concerns/ManagesTransactions.php | 4 + .../Database/DatabaseTransactionsManager.php | 59 +++++++-- .../Testing/DatabaseTransactionsManager.php | 4 +- .../DatabaseTransactionsManagerTest.php | 76 ++++++++--- tests/Database/DatabaseTransactionsTest.php | 5 + .../DatabaseTransactionsManagerTest.php | 71 ++++++++++ .../Database/DatabaseTransactionsTest.php | 121 ++++++++++++++++++ 7 files changed, 306 insertions(+), 34 deletions(-) create mode 100644 tests/Foundation/Testing/DatabaseTransactionsManagerTest.php create mode 100644 tests/Integration/Database/DatabaseTransactionsTest.php diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index a690f7b5cb46..0f6aa0239a71 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -47,6 +47,8 @@ public function transaction(Closure $callback, $attempts = 1) $this->getPdo()->commit(); } + $this->transactionsManager?->stageTransactions($this->getName()); + $this->transactions = max(0, $this->transactions - 1); if ($this->afterCommitCallbacksShouldBeExecuted()) { @@ -194,6 +196,8 @@ public function commit() $this->getPdo()->commit(); } + $this->transactionsManager?->stageTransactions($this->getName()); + $this->transactions = max(0, $this->transactions - 1); if ($this->afterCommitCallbacksShouldBeExecuted()) { diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index e198f4f3f6d6..2914464858e6 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -2,14 +2,23 @@ namespace Illuminate\Database; +use Illuminate\Support\Collection; + class DatabaseTransactionsManager { /** - * All of the recorded transactions. + * All of the committed transactions. + * + * @var \Illuminate\Support\Collection + */ + protected $committedTransactions; + + /** + * All of the pending transactions. * * @var \Illuminate\Support\Collection */ - protected $transactions; + protected $pendingTransactions; /** * Create a new database transactions manager instance. @@ -18,7 +27,8 @@ class DatabaseTransactionsManager */ public function __construct() { - $this->transactions = collect(); + $this->committedTransactions = new Collection; + $this->pendingTransactions = new Collection; } /** @@ -30,7 +40,7 @@ public function __construct() */ public function begin($connection, $level) { - $this->transactions->push( + $this->pendingTransactions->push( new DatabaseTransactionRecord($connection, $level) ); } @@ -44,7 +54,7 @@ public function begin($connection, $level) */ public function rollback($connection, $level) { - $this->transactions = $this->transactions->reject( + $this->pendingTransactions = $this->pendingTransactions->reject( fn ($transaction) => $transaction->connection == $connection && $transaction->level > $level )->values(); } @@ -57,11 +67,11 @@ public function rollback($connection, $level) */ public function commit($connection) { - [$forThisConnection, $forOtherConnections] = $this->transactions->partition( + [$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition( fn ($transaction) => $transaction->connection == $connection ); - $this->transactions = $forOtherConnections->values(); + $this->committedTransactions = $forOtherConnections->values(); $forThisConnection->map->executeCallbacks(); } @@ -81,6 +91,23 @@ public function addCallback($callback) $callback(); } + /** + * Move all the pending transactions to a committed state. + * + * @param string $connection + * @return void + */ + public function stageTransactions($connection) + { + $this->committedTransactions = $this->committedTransactions->merge( + $this->pendingTransactions->filter(fn ($transaction) => $transaction->connection === $connection) + ); + + $this->pendingTransactions = $this->pendingTransactions->reject( + fn ($transaction) => $transaction->connection === $connection + ); + } + /** * Get the transactions that are applicable to callbacks. * @@ -88,7 +115,7 @@ public function addCallback($callback) */ public function callbackApplicableTransactions() { - return $this->transactions; + return $this->pendingTransactions; } /** @@ -103,12 +130,22 @@ public function afterCommitCallbacksShouldBeExecuted($level) } /** - * Get all the transactions. + * Get all of the pending transactions. + * + * @return \Illuminate\Support\Collection + */ + public function getPendingTransactions() + { + return $this->pendingTransactions; + } + + /** + * Get all of the committed transactions. * * @return \Illuminate\Support\Collection */ - public function getTransactions() + public function getCommittedTransactions() { - return $this->transactions; + return $this->committedTransactions; } } diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php b/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php index 08c1635443a6..bc5450486d48 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php @@ -21,7 +21,7 @@ public function addCallback($callback) return $callback(); } - $this->transactions->last()->addCallback($callback); + $this->pendingTransactions->last()->addCallback($callback); } /** @@ -31,7 +31,7 @@ public function addCallback($callback) */ public function callbackApplicableTransactions() { - return $this->transactions->skip(1)->values(); + return $this->pendingTransactions->skip(1)->values(); } /** diff --git a/tests/Database/DatabaseTransactionsManagerTest.php b/tests/Database/DatabaseTransactionsManagerTest.php index e8d82e048720..e303b82d89cd 100755 --- a/tests/Database/DatabaseTransactionsManagerTest.php +++ b/tests/Database/DatabaseTransactionsManagerTest.php @@ -15,13 +15,13 @@ public function testBeginningTransactions() $manager->begin('default', 2); $manager->begin('admin', 1); - $this->assertCount(3, $manager->getTransactions()); - $this->assertSame('default', $manager->getTransactions()[0]->connection); - $this->assertEquals(1, $manager->getTransactions()[0]->level); - $this->assertSame('default', $manager->getTransactions()[1]->connection); - $this->assertEquals(2, $manager->getTransactions()[1]->level); - $this->assertSame('admin', $manager->getTransactions()[2]->connection); - $this->assertEquals(1, $manager->getTransactions()[2]->level); + $this->assertCount(3, $manager->getPendingTransactions()); + $this->assertSame('default', $manager->getPendingTransactions()[0]->connection); + $this->assertEquals(1, $manager->getPendingTransactions()[0]->level); + $this->assertSame('default', $manager->getPendingTransactions()[1]->connection); + $this->assertEquals(2, $manager->getPendingTransactions()[1]->level); + $this->assertSame('admin', $manager->getPendingTransactions()[2]->connection); + $this->assertEquals(1, $manager->getPendingTransactions()[2]->level); } public function testRollingBackTransactions() @@ -34,13 +34,13 @@ public function testRollingBackTransactions() $manager->rollback('default', 1); - $this->assertCount(2, $manager->getTransactions()); + $this->assertCount(2, $manager->getPendingTransactions()); - $this->assertSame('default', $manager->getTransactions()[0]->connection); - $this->assertEquals(1, $manager->getTransactions()[0]->level); + $this->assertSame('default', $manager->getPendingTransactions()[0]->connection); + $this->assertEquals(1, $manager->getPendingTransactions()[0]->level); - $this->assertSame('admin', $manager->getTransactions()[1]->connection); - $this->assertEquals(1, $manager->getTransactions()[1]->level); + $this->assertSame('admin', $manager->getPendingTransactions()[1]->connection); + $this->assertEquals(1, $manager->getPendingTransactions()[1]->level); } public function testRollingBackTransactionsAllTheWay() @@ -53,10 +53,10 @@ public function testRollingBackTransactionsAllTheWay() $manager->rollback('default', 0); - $this->assertCount(1, $manager->getTransactions()); + $this->assertCount(1, $manager->getPendingTransactions()); - $this->assertSame('admin', $manager->getTransactions()[0]->connection); - $this->assertEquals(1, $manager->getTransactions()[0]->level); + $this->assertSame('admin', $manager->getPendingTransactions()[0]->connection); + $this->assertEquals(1, $manager->getPendingTransactions()[0]->level); } public function testCommittingTransactions() @@ -67,12 +67,15 @@ public function testCommittingTransactions() $manager->begin('default', 2); $manager->begin('admin', 1); + $manager->stageTransactions('default'); + $manager->stageTransactions('admin'); $manager->commit('default'); - $this->assertCount(1, $manager->getTransactions()); + $this->assertCount(0, $manager->getPendingTransactions()); + $this->assertCount(1, $manager->getCommittedTransactions()); - $this->assertSame('admin', $manager->getTransactions()[0]->connection); - $this->assertEquals(1, $manager->getTransactions()[0]->level); + $this->assertSame('admin', $manager->getCommittedTransactions()[0]->connection); + $this->assertEquals(1, $manager->getCommittedTransactions()[0]->level); } public function testCallbacksAreAddedToTheCurrentTransaction() @@ -93,9 +96,9 @@ public function testCallbacksAreAddedToTheCurrentTransaction() $manager->addCallback(function () use (&$callbacks) { }); - $this->assertCount(1, $manager->getTransactions()[0]->getCallbacks()); - $this->assertCount(0, $manager->getTransactions()[1]->getCallbacks()); - $this->assertCount(1, $manager->getTransactions()[2]->getCallbacks()); + $this->assertCount(1, $manager->getPendingTransactions()[0]->getCallbacks()); + $this->assertCount(0, $manager->getPendingTransactions()[1]->getCallbacks()); + $this->assertCount(1, $manager->getPendingTransactions()[2]->getCallbacks()); } public function testCommittingTransactionsExecutesCallbacks() @@ -118,6 +121,7 @@ public function testCommittingTransactionsExecutesCallbacks() $manager->begin('admin', 1); + $manager->stageTransactions('default'); $manager->commit('default'); $this->assertCount(2, $callbacks); @@ -144,6 +148,7 @@ public function testCommittingExecutesOnlyCallbacksOfTheConnection() $callbacks[] = ['admin', 1]; }); + $manager->stageTransactions('default'); $manager->commit('default'); $this->assertCount(1, $callbacks); @@ -163,4 +168,33 @@ public function testCallbackIsExecutedIfNoTransactions() $this->assertCount(1, $callbacks); $this->assertEquals(['default', 1], $callbacks[0]); } + + public function testStageTransactions() + { + $manager = (new DatabaseTransactionsManager); + + $manager->begin('default', 1); + $manager->begin('admin', 1); + + $this->assertCount(2, $manager->getPendingTransactions()); + + $pendingTransactions = $manager->getPendingTransactions(); + + $this->assertEquals(1, $pendingTransactions[0]->level); + $this->assertEquals('default', $pendingTransactions[0]->connection); + $this->assertEquals(1, $pendingTransactions[1]->level); + $this->assertEquals('admin', $pendingTransactions[1]->connection); + + $manager->stageTransactions('default'); + + $this->assertCount(1, $manager->getPendingTransactions()); + $this->assertCount(1, $manager->getCommittedTransactions()); + $this->assertEquals('default', $manager->getCommittedTransactions()[0]->connection); + + $manager->stageTransactions('admin'); + + $this->assertCount(0, $manager->getPendingTransactions()); + $this->assertCount(2, $manager->getCommittedTransactions()); + $this->assertEquals('admin', $manager->getCommittedTransactions()[1]->connection); + } } diff --git a/tests/Database/DatabaseTransactionsTest.php b/tests/Database/DatabaseTransactionsTest.php index ac9587419eeb..94998f010c01 100644 --- a/tests/Database/DatabaseTransactionsTest.php +++ b/tests/Database/DatabaseTransactionsTest.php @@ -64,6 +64,7 @@ public function testTransactionIsRecordedAndCommitted() { $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default'); $transactionManager->shouldReceive('commit')->once()->with('default'); $this->connection()->setTransactionManager($transactionManager); @@ -83,6 +84,7 @@ public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods() { $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default'); $transactionManager->shouldReceive('commit')->once()->with('default'); $this->connection()->setTransactionManager($transactionManager); @@ -103,6 +105,7 @@ public function testNestedTransactionIsRecordedAndCommitted() $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('default', 2); + $transactionManager->shouldReceive('stageTransactions')->twice()->with('default'); $transactionManager->shouldReceive('commit')->once()->with('default'); $this->connection()->setTransactionManager($transactionManager); @@ -130,6 +133,8 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 2); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default'); + $transactionManager->shouldReceive('stageTransactions')->twice()->with('second_connection'); $transactionManager->shouldReceive('commit')->once()->with('default'); $transactionManager->shouldReceive('commit')->once()->with('second_connection'); diff --git a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php new file mode 100644 index 000000000000..b0add2e650eb --- /dev/null +++ b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php @@ -0,0 +1,71 @@ +begin('foo', 1); + + $manager->addCallback(fn () => $testObject->handle()); + + $this->assertTrue($testObject->ran); + $this->assertEquals(1, $testObject->runs); + } + + public function testItIgnoresTheBaseTransactionForCallbackApplicableTransactions() + { + $manager = new DatabaseTransactionsManager; + + $manager->begin('foo', 1); + $manager->begin('foo', 2); + + $this->assertCount(1, $manager->callbackApplicableTransactions()); + $this->assertEquals(2, $manager->callbackApplicableTransactions()[0]->level); + } + + public function testItExecutesCallbacksForTheSecondTransaction() + { + $testObject = new TestingDatabaseTransactionsManagerTestObject(); + $manager = new DatabaseTransactionsManager; + $manager->begin('foo', 1); + $manager->begin('foo', 2); + + $manager->addCallback(fn () => $testObject->handle()); + + $this->assertFalse($testObject->ran); + + $manager->stageTransactions('foo'); + $manager->commit('foo'); + $this->assertTrue($testObject->ran); + $this->assertEquals(1, $testObject->runs); + } + + public function testItExecutesTransactionCallbacksAtLevelOne() + { + $manager = new DatabaseTransactionsManager; + + $this->assertFalse($manager->afterCommitCallbacksShouldBeExecuted(0)); + $this->assertTrue($manager->afterCommitCallbacksShouldBeExecuted(1)); + $this->assertFalse($manager->afterCommitCallbacksShouldBeExecuted(2)); + } +} + +class TestingDatabaseTransactionsManagerTestObject +{ + public $ran = false; + public $runs = 0; + + public function handle() + { + $this->ran = true; + $this->runs++; + } +} diff --git a/tests/Integration/Database/DatabaseTransactionsTest.php b/tests/Integration/Database/DatabaseTransactionsTest.php new file mode 100644 index 000000000000..58894d01ae5e --- /dev/null +++ b/tests/Integration/Database/DatabaseTransactionsTest.php @@ -0,0 +1,121 @@ +set([ + 'database.connections.second_connection' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ], + ]); + } + + public function testTransactionCallbacks() + { + [$firstObject, $secondObject, $thirdObject] = [ + new TestObjectForTransactions(), + new TestObjectForTransactions(), + new TestObjectForTransactions(), + ]; + + DB::transaction(function () use ($secondObject, $firstObject) { + DB::afterCommit(fn () => $firstObject->handle()); + + DB::transaction(function () use ($secondObject) { + DB::afterCommit(fn () => $secondObject->handle()); + }); + }); + + $this->assertTrue($firstObject->ran); + $this->assertTrue($secondObject->ran); + $this->assertEquals(1, $firstObject->runs); + $this->assertEquals(1, $secondObject->runs); + $this->assertFalse($thirdObject->ran); + } + + public function testTransactionCallbacksDoNotInterfereWithOneAnother() + { + [$firstObject, $secondObject, $thirdObject] = [ + new TestObjectForTransactions(), + new TestObjectForTransactions(), + new TestObjectForTransactions(), + ]; + + // The problem here is that we're initiating a base transaction, and then two nested transactions. + // Although these two nested transactions are not the same, they share the same level (2). + // Since they are not the same, the latter one failing should not affect the first one. + DB::transaction(function () use ($thirdObject, $secondObject, $firstObject) { // Adds a transaction @ level 1 + DB::transaction(function () use ($firstObject) { // Adds a transaction @ level 2 + DB::afterCommit(fn () => $firstObject->handle()); // Adds a callback to be executed after transaction level 2 is committed + }); + + DB::afterCommit(fn () => $secondObject->handle()); // Adds a callback to be executed after transaction 1 @ lvl 1 + + try { + DB::transaction(function () use ($thirdObject) { // Adds a transaction 3 @ level 2 + DB::afterCommit(fn () => $thirdObject->handle()); + throw new \Exception(); // This should only affect callback 3, not 1, even though both share the same transaction level. + }); + } catch (\Exception) { + } + }); + + $this->assertTrue($firstObject->ran); + $this->assertTrue($secondObject->ran); + $this->assertEquals(1, $firstObject->runs); + $this->assertEquals(1, $secondObject->runs); + $this->assertFalse($thirdObject->ran); + } + + public function testTransactionsDoNotAffectDifferentConnections() + { + [$firstObject, $secondObject, $thirdObject] = [ + new TestObjectForTransactions(), + new TestObjectForTransactions(), + new TestObjectForTransactions(), + ]; + + DB::transaction(function () use ($secondObject, $firstObject, $thirdObject) { + DB::transaction(function () use ($secondObject) { + DB::afterCommit(fn () => $secondObject->handle()); + }); + + DB::afterCommit(fn () => $firstObject->handle()); + + try { + DB::connection('second_connection')->transaction(function () use ($thirdObject) { + DB::afterCommit(fn () => $thirdObject->handle()); + + throw new \Exception; + }); + } catch (\Exception) { + // + } + }); + + $this->assertTrue($firstObject->ran); + $this->assertTrue($secondObject->ran); + $this->assertFalse($thirdObject->ran); + } +} + +class TestObjectForTransactions +{ + public $ran = false; + + public $runs = 0; + + public function handle() + { + $this->ran = true; + $this->runs++; + } +} From 20085feef61119b58fef540cba79595b4a85a89a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sat, 11 Nov 2023 06:47:14 +0800 Subject: [PATCH 046/207] [10.x] Fixes Batch Callbacks not triggering if job timeout while in transaction (#48961) * 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 * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Update BatchableTransactionTest.php * Apply fixes from StyleCI * wip * wip Signed-off-by: Mior Muhammad Zaki * formatting * Apply fixes from StyleCI * formatting * formatting * Apply fixes from StyleCI * fix test --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/BatchRepository.php | 3 + .../Bus/DatabaseBatchRepository.php | 10 +++ src/Illuminate/Queue/Jobs/Job.php | 23 +++++++ .../Queue/BatchableTransactionTest.php | 65 +++++++++++++++++++ .../Fixtures/TimeOutJobWithTransaction.php | 22 +++++++ tests/Queue/QueueBeanstalkdJobTest.php | 2 +- 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Database/Queue/BatchableTransactionTest.php create mode 100644 tests/Integration/Database/Queue/Fixtures/TimeOutJobWithTransaction.php diff --git a/src/Illuminate/Bus/BatchRepository.php b/src/Illuminate/Bus/BatchRepository.php index 098ccef20ed6..0e580ca4dcd9 100644 --- a/src/Illuminate/Bus/BatchRepository.php +++ b/src/Illuminate/Bus/BatchRepository.php @@ -4,6 +4,9 @@ use Closure; +/** + * @method void rollBack() + */ interface BatchRepository { /** diff --git a/src/Illuminate/Bus/DatabaseBatchRepository.php b/src/Illuminate/Bus/DatabaseBatchRepository.php index c68e5c1a5fdd..4333c515ac79 100644 --- a/src/Illuminate/Bus/DatabaseBatchRepository.php +++ b/src/Illuminate/Bus/DatabaseBatchRepository.php @@ -312,6 +312,16 @@ public function transaction(Closure $callback) return $this->connection->transaction(fn () => $callback()); } + /** + * Rollback the last database transaction for the connection. + * + * @return void + */ + public function rollBack() + { + $this->connection->rollBack(); + } + /** * Serialize the given value. * diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index d5464946b7ee..67a80e50c03e 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -2,10 +2,14 @@ namespace Illuminate\Queue\Jobs; +use Illuminate\Bus\Batchable; +use Illuminate\Bus\BatchRepository; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\ManuallyFailedException; +use Illuminate\Queue\TimeoutExceededException; use Illuminate\Support\InteractsWithTime; +use Throwable; abstract class Job { @@ -183,6 +187,25 @@ public function fail($e = null) return; } + $commandName = $this->payload()['data']['commandName'] ?? false; + + // If the exception is due to a job timing out, we need to rollback the current + // database transaction so that the failed job count can be incremented with + // the proper value. Otherwise, the current transaction will never commit. + if ($e instanceof TimeoutExceededException && + $commandName && + in_array(Batchable::class, class_uses_recursive($commandName))) { + $batchRepository = $this->resolve(BatchRepository::class); + + if (method_exists($batchRepository, 'rollBack')) { + try { + $batchRepository->rollBack(); + } catch (Throwable $e) { + // ... + } + } + } + try { // If the job has failed, we will delete it, call the "failed" method and then call // an event indicating the job has failed so it can be logged if needed. This is diff --git a/tests/Integration/Database/Queue/BatchableTransactionTest.php b/tests/Integration/Database/Queue/BatchableTransactionTest.php new file mode 100644 index 000000000000..46c1f66366fa --- /dev/null +++ b/tests/Integration/Database/Queue/BatchableTransactionTest.php @@ -0,0 +1,65 @@ +get('database.default') === 'testing') { + $this->markTestSkipped('Test does not support using :memory: database connection'); + } + + $config->set(['queue.default' => 'database']); + } + + public function testItCanHandleTimeoutJob() + { + Bus::batch([new Fixtures\TimeOutJobWithTransaction()]) + ->allowFailures() + ->dispatch(); + + $this->assertSame(1, DB::table('jobs')->count()); + $this->assertSame(0, DB::table('failed_jobs')->count()); + $this->assertSame(1, DB::table('job_batches')->count()); + + try { + remote('queue:work --stop-when-empty', [ + 'DB_CONNECTION' => config('database.default'), + 'QUEUE_CONNECTION' => config('queue.default'), + ])->run(); + } catch (Throwable $e) { + $this->assertInstanceOf(ProcessSignaledException::class, $e); + $this->assertSame('The process has been signaled with signal "9".', $e->getMessage()); + } + + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(1, DB::table('failed_jobs')->count()); + + $this->assertDatabaseHas('job_batches', [ + 'total_jobs' => 1, + 'pending_jobs' => 1, + 'failed_jobs' => 1, + 'failed_job_ids' => json_encode(DB::table('failed_jobs')->pluck('uuid')->all()), + ]); + } +} diff --git a/tests/Integration/Database/Queue/Fixtures/TimeOutJobWithTransaction.php b/tests/Integration/Database/Queue/Fixtures/TimeOutJobWithTransaction.php new file mode 100644 index 000000000000..cff613dcc63d --- /dev/null +++ b/tests/Integration/Database/Queue/Fixtures/TimeOutJobWithTransaction.php @@ -0,0 +1,22 @@ + sleep(20)); + } +} diff --git a/tests/Queue/QueueBeanstalkdJobTest.php b/tests/Queue/QueueBeanstalkdJobTest.php index ebea82a7de38..ec9bd34277c4 100755 --- a/tests/Queue/QueueBeanstalkdJobTest.php +++ b/tests/Queue/QueueBeanstalkdJobTest.php @@ -33,7 +33,7 @@ public function testFireProperlyCallsTheJobHandler() public function testFailProperlyCallsTheJobHandler() { $job = $this->getJob(); - $job->getPheanstalkJob()->shouldReceive('getData')->once()->andReturn(json_encode(['job' => 'foo', 'uuid' => 'test-uuid', 'data' => ['data']])); + $job->getPheanstalkJob()->shouldReceive('getData')->andReturn(json_encode(['job' => 'foo', 'uuid' => 'test-uuid', 'data' => ['data']])); $job->getContainer()->shouldReceive('make')->once()->with('foo')->andReturn($handler = m::mock(BeanstalkdJobTestFailedTest::class)); $job->getPheanstalk()->shouldReceive('delete')->once()->with($job->getPheanstalkJob())->andReturnSelf(); $handler->shouldReceive('failed')->once()->with(['data'], m::type(Exception::class), 'test-uuid'); From 3cb7ed217c568190a39db15bb0d85af4d6889abc Mon Sep 17 00:00:00 2001 From: Tobias Petry Date: Mon, 13 Nov 2023 17:39:12 +0100 Subject: [PATCH 047/207] fix: support expression in computed/virtual/stored columns (#48976) --- .../Database/Schema/Grammars/MySqlGrammar.php | 4 ++-- .../Database/Schema/Grammars/PostgresGrammar.php | 4 ++-- .../Database/Schema/Grammars/SQLiteGrammar.php | 4 ++-- .../Database/Schema/Grammars/SqlServerGrammar.php | 2 +- tests/Database/DatabaseMySqlSchemaGrammarTest.php | 12 ++++++++++++ .../DatabasePostgresSchemaGrammarTest.php | 15 +++++++++++++++ .../Database/DatabaseSQLiteSchemaGrammarTest.php | 14 ++++++++++++++ .../DatabaseSqlServerSchemaGrammarTest.php | 9 +++++++++ 8 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 7f940b95f975..797a5897ff81 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -1071,7 +1071,7 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) } if (! is_null($virtualAs = $column->virtualAs)) { - return " as ({$virtualAs})"; + return " as ({$this->getValue($virtualAs)})"; } } @@ -1093,7 +1093,7 @@ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) } if (! is_null($storedAs = $column->storedAs)) { - return " as ({$storedAs}) stored"; + return " as ({$this->getValue($storedAs)}) stored"; } } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 845e818de542..c8f19cac2b3b 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -1183,7 +1183,7 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) } if (! is_null($column->virtualAs)) { - return " generated always as ({$column->virtualAs})"; + return " generated always as ({$this->getValue($column->virtualAs)})"; } } @@ -1207,7 +1207,7 @@ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) } if (! is_null($column->storedAs)) { - return " generated always as ({$column->storedAs}) stored"; + return " generated always as ({$this->getValue($column->storedAs)}) stored"; } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 60171610d020..05294f2151e2 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -937,7 +937,7 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) } if (! is_null($virtualAs = $column->virtualAs)) { - return " as ({$virtualAs})"; + return " as ({$this->getValue($virtualAs)})"; } } @@ -959,7 +959,7 @@ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) } if (! is_null($storedAs = $column->storedAs)) { - return " as ({$column->storedAs}) stored"; + return " as ({$this->getValue($column->storedAs)}) stored"; } } diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index d70aedadc0e9..ab1944a83341 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -964,7 +964,7 @@ public function typeMultiPolygon(Fluent $column) */ protected function typeComputed(Fluent $column) { - return "as ({$column->expression})"; + return "as ({$this->getValue($column->expression)})"; } /** diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index 7bca44c41a3d..fa8deab2fff9 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -585,6 +585,18 @@ public function testAddingGeneratedColumnWithCharset() $this->assertSame('alter table `links` add `url` varchar(2083) character set ascii not null, add `url_hash_virtual` varchar(64) character set ascii as (sha2(url, 256)), add `url_hash_stored` varchar(64) character set ascii as (sha2(url, 256)) stored', $statements[0]); } + public function testAddingGeneratedColumnByExpression() + { + $blueprint = new Blueprint('products'); + $blueprint->integer('price'); + $blueprint->integer('discounted_virtual')->virtualAs(new Expression('price - 5')); + $blueprint->integer('discounted_stored')->storedAs(new Expression('price - 5')); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `products` add `price` int not null, add `discounted_virtual` int as (price - 5), add `discounted_stored` int as (price - 5) stored', $statements[0]); + } + public function testAddingInvisibleColumn() { $blueprint = new Blueprint('users'); diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index db139e523280..9229c6effc8f 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Builder; use Illuminate\Database\Schema\ForeignIdColumnDefinition; @@ -957,6 +958,13 @@ public function testAddingVirtualAs() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); $this->assertSame('alter table "users" add column "foo" integer null, add column "bar" boolean not null generated always as (foo is not null)', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->integer('foo')->nullable(); + $blueprint->boolean('bar')->virtualAs(new Expression('foo is not null')); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" integer null, add column "bar" boolean not null generated always as (foo is not null)', $statements[0]); } public function testAddingStoredAs() @@ -967,6 +975,13 @@ public function testAddingStoredAs() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); $this->assertSame('alter table "users" add column "foo" integer null, add column "bar" boolean not null generated always as (foo is not null) stored', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->integer('foo')->nullable(); + $blueprint->boolean('bar')->storedAs(new Expression('foo is not null')); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" integer null, add column "bar" boolean not null generated always as (foo is not null) stored', $statements[0]); } public function testAddingIpAddress() diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 1a80835d8abe..9e48556d9f12 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Capsule\Manager; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\ForeignIdColumnDefinition; use Illuminate\Database\Schema\Grammars\SQLiteGrammar; @@ -888,6 +889,19 @@ public function testAddingGeneratedColumn() $this->assertSame($expected, $statements); } + public function testAddingGeneratedColumnByExpression() + { + $blueprint = new Blueprint('products'); + $blueprint->create(); + $blueprint->integer('price'); + $blueprint->integer('discounted_virtual')->virtualAs(new Expression('"price" - 5')); + $blueprint->integer('discounted_stored')->storedAs(new Expression('"price" - 5')); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create table "products" ("price" integer not null, "discounted_virtual" integer as ("price" - 5), "discounted_stored" integer as ("price" - 5) stored)', $statements[0]); + } + public function testGrammarsAreMacroable() { // compileReplace macro. diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index cc110d1edaad..f75d8d932966 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\ForeignIdColumnDefinition; use Illuminate\Database\Schema\Grammars\SqlServerGrammar; @@ -911,6 +912,14 @@ public function testAddingGeneratedColumn() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); $this->assertSame('alter table "products" add "price" int not null, "discounted_virtual" as (price - 5), "discounted_stored" as (price - 5) persisted', $statements[0]); + + $blueprint = new Blueprint('products'); + $blueprint->integer('price'); + $blueprint->computed('discounted_virtual', new Expression('price - 5')); + $blueprint->computed('discounted_stored', new Expression('price - 5'))->persisted(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "products" add "price" int not null, "discounted_virtual" as (price - 5), "discounted_stored" as (price - 5) persisted', $statements[0]); } public function testGrammarsAreMacroable() From 0a3070224fda70819e26fd8697c7767137d52737 Mon Sep 17 00:00:00 2001 From: Moshe Brodsky <44633930+moshe-autoleadstar@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:41:37 +0200 Subject: [PATCH 048/207] [10.x] Fixes Exception: Cannot traverse an already closed generator when running Arr::first with an empty generator and no callback (#48979) * fix Exception: Cannot traverse an already closed generator when running Arr::first with an empty generator and no callback * force rerun actions * force rerun actions --- src/Illuminate/Collections/Arr.php | 2 ++ tests/Support/SupportArrTest.php | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index 3cb9a2ca180a..c14465c6b3fa 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -195,6 +195,8 @@ public static function first($array, callable $callback = null, $default = null) foreach ($array as $item) { return $item; } + + return value($default); } foreach ($array as $key => $value) { diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index f87a36364b79..2c5f6c224e67 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -214,6 +214,13 @@ public function testFirst() $this->assertSame('bar', $value3); $this->assertSame('baz', $value4); $this->assertEquals(100, $value5); + + $cursor = (function () { + while (false) { + yield 1; + } + })(); + $this->assertNull(Arr::first($cursor)); } public function testJoin() From e5a751584dc53a7ccf087b831f1ff357ad2dd449 Mon Sep 17 00:00:00 2001 From: Niko Peikrishvili Date: Mon, 13 Nov 2023 20:58:20 +0400 Subject: [PATCH 049/207] fixes issue with stderr when there was "]" character. (#48975) * fixes issue with stderr when there was "]" character. * fixes code style issues * Update ServeCommand.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Console/ServeCommand.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index 8d109b5c6f7c..194c80fa5127 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -287,8 +287,13 @@ protected function handleProcessOutput() } elseif (str($line)->contains(['Closed without sending a request'])) { // ... } elseif (! empty($line)) { - $warning = explode('] ', $line); - $this->components->warn(count($warning) > 1 ? $warning[1] : $warning[0]); + $position = strpos($line, '] '); + + if ($position !== false) { + $line = substr($line, $position + 1); + } + + $this->components->warn($line); } }); } From c6aeffded35c10ac60b14d4cff4b6c45dc57e9ea Mon Sep 17 00:00:00 2001 From: xdevor Date: Tue, 14 Nov 2023 03:41:18 +0800 Subject: [PATCH 050/207] [10.x] Fix Postgres cache store failed to put exist cache in transaction (#48968) * [10.x] Fix Postgres cache store failed to put exist cache in transaction * Test PostgresCacheStore integration test * [10.x] Improve put operation of database cache store - Prevent non-rollback error query occur in transaction - For existing keys they will have to execute two queries instead of just one with upserts * Add database cache store integration test * Chore: ad db cache store integration test to databases workflows * Fix SqlServerCacheStoreTest * Refactor db cache store integration test * Refactor db cache store integration test * Refactor style * wip --------- Co-authored-by: Mior Muhammad Zaki --- src/Illuminate/Cache/DatabaseStore.php | 9 +--- tests/Cache/CacheDatabaseStoreTest.php | 25 ++-------- .../Database/DatabaseCacheStoreTest.php | 48 +++++++++++++++++++ 3 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 tests/Integration/Database/DatabaseCacheStoreTest.php diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 19c4ccefe431..457529fad8d2 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -3,7 +3,6 @@ namespace Illuminate\Cache; use Closure; -use Exception; use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Store; use Illuminate\Database\ConnectionInterface; @@ -137,13 +136,7 @@ public function put($key, $value, $seconds) $value = $this->serialize($value); $expiration = $this->getTime() + $seconds; - try { - return $this->table()->insert(compact('key', 'value', 'expiration')); - } catch (Exception) { - $result = $this->table()->where('key', $key)->update(compact('value', 'expiration')); - - return $result > 0; - } + return $this->table()->upsert(compact('key', 'value', 'expiration'), 'key') > 0; } /** diff --git a/tests/Cache/CacheDatabaseStoreTest.php b/tests/Cache/CacheDatabaseStoreTest.php index ac98021eb87c..48f838d4c6e6 100755 --- a/tests/Cache/CacheDatabaseStoreTest.php +++ b/tests/Cache/CacheDatabaseStoreTest.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Cache; use Closure; -use Exception; use Illuminate\Cache\DatabaseStore; use Illuminate\Database\Connection; use Illuminate\Database\PostgresConnection; @@ -63,41 +62,25 @@ public function testValueIsReturnedOnPostgres() $this->assertSame('bar', $store->get('foo')); } - public function testValueIsInsertedWhenNoExceptionsAreThrown() + public function testValueIsUpserted() { $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['getTime'])->setConstructorArgs($this->getMocks())->getMock(); $table = m::mock(stdClass::class); $store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table); $store->expects($this->once())->method('getTime')->willReturn(1); - $table->shouldReceive('insert')->once()->with(['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61])->andReturnTrue(); + $table->shouldReceive('upsert')->once()->with(['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61], 'key')->andReturnTrue(); $result = $store->put('foo', 'bar', 60); $this->assertTrue($result); } - public function testValueIsUpdatedWhenInsertThrowsException() - { - $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['getTime'])->setConstructorArgs($this->getMocks())->getMock(); - $table = m::mock(stdClass::class); - $store->getConnection()->shouldReceive('table')->with('table')->andReturn($table); - $store->expects($this->once())->method('getTime')->willReturn(1); - $table->shouldReceive('insert')->once()->with(['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61])->andReturnUsing(function () { - throw new Exception; - }); - $table->shouldReceive('where')->once()->with('key', 'prefixfoo')->andReturn($table); - $table->shouldReceive('update')->once()->with(['value' => serialize('bar'), 'expiration' => 61])->andReturnTrue(); - - $result = $store->put('foo', 'bar', 60); - $this->assertTrue($result); - } - - public function testValueIsInsertedOnPostgres() + public function testValueIsUpsertedOnPostgres() { $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['getTime'])->setConstructorArgs($this->getPostgresMocks())->getMock(); $table = m::mock(stdClass::class); $store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table); $store->expects($this->once())->method('getTime')->willReturn(1); - $table->shouldReceive('insert')->once()->with(['key' => 'prefixfoo', 'value' => base64_encode(serialize("\0")), 'expiration' => 61])->andReturnTrue(); + $table->shouldReceive('upsert')->once()->with(['key' => 'prefixfoo', 'value' => base64_encode(serialize("\0")), 'expiration' => 61], 'key')->andReturn(1); $result = $store->put('foo', "\0", 60); $this->assertTrue($result); diff --git a/tests/Integration/Database/DatabaseCacheStoreTest.php b/tests/Integration/Database/DatabaseCacheStoreTest.php new file mode 100644 index 000000000000..1f5bac9835d0 --- /dev/null +++ b/tests/Integration/Database/DatabaseCacheStoreTest.php @@ -0,0 +1,48 @@ +getStore(); + + $store->put('foo', 'bar', 60); + + $this->assertSame('bar', $store->get('foo')); + } + + public function testValueCanUpdateExistCache() + { + $store = $this->getStore(); + + $store->put('foo', 'bar', 60); + $store->put('foo', 'new-bar', 60); + + $this->assertSame('new-bar', $store->get('foo')); + } + + public function testValueCanUpdateExistCacheInTransaction() + { + $store = $this->getStore(); + + $store->put('foo', 'bar', 60); + + DB::beginTransaction(); + $store->put('foo', 'new-bar', 60); + DB::commit(); + + $this->assertSame('new-bar', $store->get('foo')); + } + + protected function getStore() + { + return Cache::store('database'); + } +} From 7e9d1304ed68ca73cc38dfe35e837e4bf4096deb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 14 Nov 2023 09:47:27 -0600 Subject: [PATCH 051/207] version --- 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 7cffa8813078..3c02e3311354 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.31.0'; + const VERSION = '10.32.0'; /** * The base path for the Laravel installation. From 35aa3dd44762ec4244fb5cd1d9d7156c9455f026 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 14 Nov 2023 15:51:37 +0000 Subject: [PATCH 052/207] Update CHANGELOG --- CHANGELOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f05920b60d9..7617d62d2e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.31.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.32.0...10.x) + +## [v10.32.0](https://github.com/laravel/framework/compare/v10.31.0...v10.32.0) - 2023-11-14 + +- Update PendingRequest.php by [@mattkingshott](https://github.com/mattkingshott) in https://github.com/laravel/framework/pull/48939 +- [10.x] Change array_key_exists with null coalescing assignment operator in FilesystemAdapter by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/48943 +- [10.x] Use container to resolve email validator class by [@orkhanahmadov](https://github.com/orkhanahmadov) in https://github.com/laravel/framework/pull/48942 +- [10.x] Added `getGlobalMiddleware` method to HTTP Client Factory by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/laravel/framework/pull/48950 +- [10.x] Detect MySQL read-only mode error as a lost connection by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/48937 +- [10.x] Adds more implicit validation rules for `present` based on other fields by [@diamondobama](https://github.com/diamondobama) in https://github.com/laravel/framework/pull/48908 +- [10.x] Refactor set_error_handler callback to use arrow function in `InteractsWithDeprecationHandling` by [@miladev95](https://github.com/miladev95) in https://github.com/laravel/framework/pull/48954 +- [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/48962 +- Fix issue that prevents BladeCompiler to raise an exception when temporal compiled blade template is not found. by [@juanparati](https://github.com/juanparati) in https://github.com/laravel/framework/pull/48957 +- [10.x] Fix how nested transaction callbacks are handled by [@mateusjatenee](https://github.com/mateusjatenee) in https://github.com/laravel/framework/pull/48859 +- [10.x] Fixes Batch Callbacks not triggering if job timeout while in transaction by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/48961 +- [10.x] expressions in migration computations fail by [@tpetry](https://github.com/tpetry) in https://github.com/laravel/framework/pull/48976 +- [10.x] Fixes Exception: Cannot traverse an already closed generator when running Arr::first with an empty generator and no callback by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/48979 +- fixes issue with stderr when there was "]" character. by [@nikopeikrishvili](https://github.com/nikopeikrishvili) in https://github.com/laravel/framework/pull/48975 +- [10.x] Fix Postgres cache store failed to put exist cache in transaction by [@xdevor](https://github.com/xdevor) in https://github.com/laravel/framework/pull/48968 ## [v10.31.0](https://github.com/laravel/framework/compare/v10.30.1...v10.31.0) - 2023-11-07 From 33edc51f3dfa9560dd561c553ecc79fbc705b121 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Tue, 14 Nov 2023 17:15:30 -0500 Subject: [PATCH 053/207] [10.x] Add `@pushElseIf` and `@pushElse` (#48990) * Add `@pushElseIf` and `@pushElse` * Stop push within each conditional branch * Symmetry --- .../Concerns/CompilesConditionals.php | 24 +++++++++++++++++++ tests/View/Blade/BladePushTest.php | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php index 2b1c0e8e41d4..b26494e822b5 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php @@ -373,6 +373,30 @@ protected function compilePushIf($expression) return "startPush({$parts[1]}); ?>"; } + /** + * Compile the else-if push statements into valid PHP. + * + * @param string $expression + * @return string + */ + protected function compileElsePushIf($expression) + { + $parts = explode(',', $this->stripParentheses($expression), 2); + + return "stopPush(); elseif({$parts[0]}): \$__env->startPush({$parts[1]}); ?>"; + } + + /** + * Compile the else push statements into valid PHP. + * + * @param string $expression + * @return string + */ + protected function compileElsePush($expression) + { + return "stopPush(); else: \$__env->startPush{$expression}; ?>"; + } + /** * Compile the end-push statements into valid PHP. * diff --git a/tests/View/Blade/BladePushTest.php b/tests/View/Blade/BladePushTest.php index 103c5c13b692..84f3d3f33bfd 100644 --- a/tests/View/Blade/BladePushTest.php +++ b/tests/View/Blade/BladePushTest.php @@ -65,6 +65,26 @@ public function testPushIfIsCompiled() @endPushIf'; $expected = 'startPush( \'foo\'); ?> test +stopPush(); endif; ?>'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testPushIfElseIsCompiled() + { + $string = '@pushIf(true, \'stack\') +if +@elsePushIf(false, \'stack\') +elseif +@elsePush(\'stack\') +else +@endPushIf'; + $expected = 'startPush( \'stack\'); ?> +if +stopPush(); elseif(false): $__env->startPush( \'stack\'); ?> +elseif +stopPush(); else: $__env->startPush(\'stack\'); ?> +else stopPush(); endif; ?>'; $this->assertEquals($expected, $this->compiler->compileString($string)); From 37416f0ce7a2a059ad08b53eeb07de286553c96f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 14 Nov 2023 16:33:48 -0600 Subject: [PATCH 054/207] fix basic testing of chained batches --- src/Illuminate/Support/Testing/Fakes/BusFake.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index 6bd897078537..fb6209f01fdc 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Bus\BatchRepository; +use Illuminate\Bus\ChainedBatch; use Illuminate\Bus\PendingBatch; use Illuminate\Contracts\Bus\QueueingDispatcher; use Illuminate\Support\Arr; @@ -671,6 +672,7 @@ public function dispatchAfterResponse($command) public function chain($jobs) { $jobs = Collection::wrap($jobs); + $jobs = ChainedBatch::prepareNestedBatches($jobs); return new PendingChainFake($this, $jobs->shift(), $jobs->toArray()); } From b30e44f20d244f7ba125283e14a8bbac167f4e5b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 14 Nov 2023 16:57:08 -0600 Subject: [PATCH 055/207] patch version --- 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 3c02e3311354..bf1b10d4c0a3 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.32.0'; + const VERSION = '10.32.1'; /** * The base path for the Laravel installation. From b79025607e4f1c8d9cec59a998d4e59a0b008b27 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Tue, 14 Nov 2023 22:59:35 +0000 Subject: [PATCH 056/207] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7617d62d2e65..c2cc6f052a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.32.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.32.1...10.x) + +## [v10.32.1](https://github.com/laravel/framework/compare/v10.32.0...v10.32.1) - 2023-11-14 + +- [10.x] Add `[@pushElseIf](https://github.com/pushElseIf)` and `[@pushElse](https://github.com/pushElse)` by [@jasonmccreary](https://github.com/jasonmccreary) in https://github.com/laravel/framework/pull/48990 ## [v10.32.0](https://github.com/laravel/framework/compare/v10.31.0...v10.32.0) - 2023-11-14 From 15b18bd277af8ef602a1192cf6f7474a3d8aeb43 Mon Sep 17 00:00:00 2001 From: Kay W Date: Wed, 15 Nov 2023 22:41:04 +0800 Subject: [PATCH 057/207] Fix wrong parameter passing and add these rules to dependent rules (#49008) --- .../Validation/Concerns/ValidatesAttributes.php | 8 ++++---- src/Illuminate/Validation/Validator.php | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 4b11c28f5e30..df5bb1fc2005 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1743,7 +1743,7 @@ public function validatePresentIf($attribute, $value, $parameters) [$values, $other] = $this->parseDependentRuleParameters($parameters); if (in_array($other, $values, is_bool($other) || is_null($other))) { - return $this->validatePresent($attribute, $value, $parameters); + return $this->validatePresent($attribute, $value); } return true; @@ -1764,7 +1764,7 @@ public function validatePresentUnless($attribute, $value, $parameters) [$values, $other] = $this->parseDependentRuleParameters($parameters); if (! in_array($other, $values, is_bool($other) || is_null($other))) { - return $this->validatePresent($attribute, $value, $parameters); + return $this->validatePresent($attribute, $value); } return true; @@ -1783,7 +1783,7 @@ public function validatePresentWith($attribute, $value, $parameters) $this->requireParameterCount(1, $parameters, 'present_with'); if (Arr::hasAny($this->data, $parameters)) { - return $this->validatePresent($attribute, $value, $parameters); + return $this->validatePresent($attribute, $value); } return true; @@ -1802,7 +1802,7 @@ public function validatePresentWithAll($attribute, $value, $parameters) $this->requireParameterCount(1, $parameters, 'present_with_all'); if (Arr::has($this->data, $parameters)) { - return $this->validatePresent($attribute, $value, $parameters); + return $this->validatePresent($attribute, $value); } return true; diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 0629287d6e8a..ef74c14af8f4 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -256,6 +256,10 @@ class Validator implements ValidatorContract 'RequiredWithAll', 'RequiredWithout', 'RequiredWithoutAll', + 'PresentIf', + 'PresentUnless', + 'PresentWith', + 'PresentWithAll', 'Prohibited', 'ProhibitedIf', 'ProhibitedUnless', From dbda680cbf583e63531a47fdc0ecf9587ae8cb4b Mon Sep 17 00:00:00 2001 From: Shin <2082119+shinsenter@users.noreply.github.com> Date: Wed, 15 Nov 2023 23:43:06 +0900 Subject: [PATCH 058/207] Make Validator::getValue() public (#49007) Make the getValue() method of the Validator class have public access. --- src/Illuminate/Validation/Validator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index ef74c14af8f4..5a6844c3c758 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -1108,7 +1108,7 @@ public function setData(array $data) * @param string $attribute * @return mixed */ - protected function getValue($attribute) + public function getValue($attribute) { return Arr::get($this->data, $attribute); } From 6fb9acb4447cc54cc61f5e7bfee4cf0657aac017 Mon Sep 17 00:00:00 2001 From: Sergii Kauk Date: Wed, 15 Nov 2023 16:03:19 +0100 Subject: [PATCH 059/207] [10.x] Custom messages for `Password` validation rule (#48928) * Update Password.php Changes draft * Refine changes * Update tests * Style fixes * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Validation/Rules/Password.php | 55 +++------------- .../Validation/ValidationPasswordRuleTest.php | 64 +++++++++++++------ 2 files changed, 54 insertions(+), 65 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index fe0e19062922..93e06dc5d57a 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -269,7 +269,7 @@ public function symbols() /** * Specify additional validation rules that should be merged with the default rules during validation. * - * @param string|array $rules + * @param \Closure|string|array $rules * @return $this */ public function rules($rules) @@ -301,31 +301,19 @@ public function passes($attribute, $value) } if ($this->mixedCase && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { - $validator->errors()->add( - $attribute, - $this->getErrorMessage('validation.password.mixed') - ); + $validator->addFailure($attribute, 'password.mixed'); } if ($this->letters && ! preg_match('/\pL/u', $value)) { - $validator->errors()->add( - $attribute, - $this->getErrorMessage('validation.password.letters') - ); + $validator->addFailure($attribute, 'password.letters'); } if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/u', $value)) { - $validator->errors()->add( - $attribute, - $this->getErrorMessage('validation.password.symbols') - ); + $validator->addFailure($attribute, 'password.symbols'); } if ($this->numbers && ! preg_match('/\pN/u', $value)) { - $validator->errors()->add( - $attribute, - $this->getErrorMessage('validation.password.numbers') - ); + $validator->addFailure($attribute, 'password.numbers'); } }); @@ -337,7 +325,9 @@ public function passes($attribute, $value) 'value' => $value, 'threshold' => $this->compromisedThreshold, ])) { - return $this->fail($this->getErrorMessage('validation.password.uncompromised')); + $validator->addFailure($attribute, 'password.uncompromised'); + + return $this->fail($validator->messages()->all()); } return true; @@ -353,29 +343,6 @@ public function message() return $this->messages; } - /** - * Get the translated password error message. - * - * @param string $key - * @return string - */ - protected function getErrorMessage($key) - { - if (($message = $this->validator->getTranslator()->get($key)) !== $key) { - return $message; - } - - $messages = [ - 'validation.password.mixed' => 'The :attribute must contain at least one uppercase and one lowercase letter.', - 'validation.password.letters' => 'The :attribute must contain at least one letter.', - 'validation.password.symbols' => 'The :attribute must contain at least one symbol.', - 'validation.password.numbers' => 'The :attribute must contain at least one number.', - 'validation.password.uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', - ]; - - return $messages[$key]; - } - /** * Adds the given failures, and return false. * @@ -384,11 +351,7 @@ protected function getErrorMessage($key) */ protected function fail($messages) { - $messages = collect(Arr::wrap($messages))->map(function ($message) { - return $this->validator->getTranslator()->get($message); - })->all(); - - $this->messages = array_merge($this->messages, $messages); + $this->messages = array_merge($this->messages, Arr::wrap($messages)); return false; } diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index e2b390f7dabb..3cfc7c43911a 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -50,7 +50,7 @@ public function testConditional() }); $this->fails($rule, ['aaaaaaaa', '11111111'], [ - 'The my password must contain at least one symbol.', + 'validation.password.symbols', ]); $is_privileged_user = false; @@ -64,7 +64,7 @@ public function testConditional() public function testMixedCase() { $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM'], [ - 'The my password must contain at least one uppercase and one lowercase letter.', + 'validation.password.mixed', ]); $this->passes(Password::min(2)->mixedCase(), ['Nn', 'Mn', 'âA']); @@ -73,7 +73,7 @@ public function testMixedCase() public function testLetters() { $this->fails(Password::min(2)->letters(), ['11', '22', '^^', '``', '**'], [ - 'The my password must contain at least one letter.', + 'validation.password.letters', ]); $this->passes(Password::min(2)->letters(), ['1a', 'b2', 'â1', '1 京都府']); @@ -82,7 +82,7 @@ public function testLetters() public function testNumbers() { $this->fails(Password::min(2)->numbers(), ['aa', 'bb', ' a', '京都府'], [ - 'The my password must contain at least one number.', + 'validation.password.numbers', ]); $this->passes(Password::min(2)->numbers(), ['1a', 'b2', '00', '京都府 1']); @@ -99,7 +99,7 @@ public function testDefaultRules() public function testSymbols() { $this->fails(Password::min(2)->symbols(), ['ab', '1v'], [ - 'The my password must contain at least one symbol.', + 'validation.password.symbols', ]); $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè$', '金廿土弓竹中;']); @@ -116,7 +116,7 @@ public function testUncompromised() '12345678', 'nuno', ], [ - 'The given my password has appeared in a data leak. Please choose a different my password.', + 'validation.password.uncompromised', ]); $this->passes(Password::min(2)->uncompromised(9999999), [ @@ -146,22 +146,22 @@ public function testMessagesOrder() $this->fails($makeRules(), ['foo', 'azdazd'], [ 'validation.min.string', - 'The my password must contain at least one uppercase and one lowercase letter.', - 'The my password must contain at least one number.', + 'validation.password.mixed', + 'validation.password.numbers', ]); $this->fails($makeRules(), ['1231231'], [ 'validation.min.string', - 'The my password must contain at least one uppercase and one lowercase letter.', + 'validation.password.mixed', ]); $this->fails($makeRules(), ['4564654564564'], [ - 'The my password must contain at least one uppercase and one lowercase letter.', + 'validation.password.mixed', ]); $this->fails($makeRules(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [ - 'The my password must contain at least one uppercase and one lowercase letter.', - 'The my password must contain at least one number.', + 'validation.password.mixed', + 'validation.password.numbers', ]); $this->passes($makeRules(), ['4564654564564Abc']); @@ -174,26 +174,26 @@ public function testMessagesOrder() $this->fails($makeRules(), ['foo', 'azdazd'], [ 'validation.min.string', - 'The my password must contain at least one symbol.', + 'validation.password.symbols', ]); $this->fails($makeRules(), ['1231231'], [ 'validation.min.string', - 'The my password must contain at least one letter.', - 'The my password must contain at least one symbol.', + 'validation.password.letters', + 'validation.password.symbols', ]); $this->fails($makeRules(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [ - 'The my password must contain at least one symbol.', + 'validation.password.symbols', ]); $this->fails($makeRules(), ['4564654564564'], [ - 'The my password must contain at least one letter.', - 'The my password must contain at least one symbol.', + 'validation.password.letters', + 'validation.password.symbols', ]); $this->fails($makeRules(), ['abcabcabc!'], [ - 'The given my password has appeared in a data leak. Please choose a different my password.', + 'validation.password.uncompromised', ]); $v = new Validator( @@ -269,6 +269,32 @@ public function testItPassesWithValidDataIfTheSameValidationRulesAreReused() $this->assertTrue($v1->passes()); } + public function testCustomMessages() + { + $rules = [ + 'my_password' => Password::min(6)->letters(), + ]; + + $messages = [ + 'min' => 'Message for validating length', + 'password.letters' => 'Message for validating letters', + ]; + + $v = new Validator( + resolve('translator'), + ['my_password' => '1234'], + $rules, + $messages, + ); + + $this->assertFalse($v->passes()); + + $this->assertSame( + ['my_password' => array_values($messages)], + $v->messages()->toArray() + ); + } + public function testPassesWithCustomRules() { $closureRule = function ($attribute, $value, $fail) { From 3a3a9cc7cdad3ae3d0af66f3f08c367a81f8343b Mon Sep 17 00:00:00 2001 From: Sjors Ottjes Date: Wed, 15 Nov 2023 20:02:12 +0100 Subject: [PATCH 060/207] dont show milliseconds with decimals (#49014) --- src/Illuminate/Database/Seeder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index ba4cd4ae3826..bfb48aedf3b0 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -61,7 +61,7 @@ public function call($class, $silent = false, array $parameters = []) $seeder->__invoke($parameters); if ($silent === false && isset($this->command)) { - $runTime = number_format((microtime(true) - $startTime) * 1000, 2); + $runTime = number_format((microtime(true) - $startTime) * 1000); with(new TwoColumnDetail($this->command->getOutput()))->render( $name, From 6e4ecc7e2e67ba36eca325ff49adc55e93a72eb7 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Wed, 15 Nov 2023 22:06:41 +0100 Subject: [PATCH 061/207] [10.x] Add a `Number` utility class (#48845) * Create a new Number utility class * Add a `Number::bytesToHuman()` helper Ports https://github.com/laravel/framework/pull/48827 into the new `Number` utility class * Add a `Number::toHuman()` helper This is unfortunately dependent on the `ext-intl`, as it wraps the NumberFormatter class. * Use lowercase `k` for kilobytes See https://github.com/laravel/framework/issues/48845#issuecomment-1784984816 * Update Support package to suggest `ext-intl` * Throw if extension is not installed when using NumberFormatter wrapper As discussed in #internals, this seems to be a good compromise when the extension is not used. Instead of testing against the exception in the tests, I just skipped the test if the extension is missing, as that is what we do in the InteractsWithRedis testing trait. * Add a `Number::toCurrency()` helper * Make Number helper locale parameters null and default to App locale This makes so that if no locale parameter is specified, the configured App locale is used. * Add a `Number::format()` helper Adds a locale-aware number formatting helper * Fix number tests Could not get Mockery to work, so this is my fix. * Add a `Number::toPercent()` helper I'm dividing the supplied value by 100 so that 50 = 50% as that's how Rails does it, and that's what Taylor linked to in his suggestion for this class. The default number formatter would consider 0.5 to be 50% and 50 to be 5000%. I'm not sure which option is best, so I went with the Rails format. https://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_to_percentage * Rename Number toHuman helper to spellout We may want to remove it, as per https://github.com/laravel/framework/pull/48845#issuecomment-1802768912. But renaming it for now. * Create new `Number::toHuman()` helper Based on the Rails implementation, as requested by Taylor in https://github.com/laravel/framework/pull/48845#issuecomment-1802768912 See https://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_to_human Uses the short scale system, see https://en.wikipedia.org/wiki/Long_and_short_scales * Change toHuman implementation to better match Rails version Based more on the logic of Rails, but with added support for massive numbers. * Update toHuman helper to better handle extreme numbers Inverts negative numbers, and removes unreachable cases, and handles very large numbers * Clean up toHuman helper * formatting * formatting * formatting * formatting * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Support/Number.php | 161 +++++++++++++++++++++++++ tests/Support/SupportNumberTest.php | 178 ++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 src/Illuminate/Support/Number.php create mode 100644 tests/Support/SupportNumberTest.php diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php new file mode 100644 index 000000000000..f86e681ecb0a --- /dev/null +++ b/src/Illuminate/Support/Number.php @@ -0,0 +1,161 @@ +format($number); + } + + /** + * Convert the given number to its percentage equivalent. + * + * @param int|float $number + * @param int $precision + * @param ?string $locale + * @return string|false + */ + public static function toPercentage(int|float $number, int $precision = 0, ?string $locale = null) + { + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::PERCENT); + + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); + + return $formatter->format($number / 100); + } + + /** + * Convert the given number to its currency equivalent. + * + * @param int|float $number + * @param string $currency + * @param ?string $locale + * @return string|false + */ + public static function toCurrency(int|float $number, string $currency = 'USD', ?string $locale = null) + { + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::CURRENCY); + + return $formatter->formatCurrency($number, $currency); + } + + /** + * Convert the given number to its file size equivalent. + * + * @param int|float $bytes + * @param int $precision + * @return string + */ + public static function toFileSize(int|float $bytes, int $precision = 0) + { + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + for ($i = 0; ($bytes / 1024) > 0.9 && ($i < count($units) - 1); $i++) { + $bytes /= 1024; + } + + return sprintf('%s %s', number_format($bytes, $precision), $units[$i]); + } + + /** + * Convert the number to its human readable equivalent. + * + * @param int $number + * @param int $precision + * @return string + */ + public static function forHumans(int|float $number, int $precision = 0) + { + $units = [ + 3 => 'thousand', + 6 => 'million', + 9 => 'billion', + 12 => 'trillion', + 15 => 'quadrillion', + ]; + + switch (true) { + case $number === 0: + return '0'; + case $number < 0: + return sprintf('-%s', static::forHumans(abs($number), $precision)); + case $number >= 1e15: + return sprintf('%s quadrillion', static::forHumans($number / 1e15, $precision)); + } + + $numberExponent = floor(log10($number)); + $displayExponent = $numberExponent - ($numberExponent % 3); + $number /= pow(10, $displayExponent); + + return trim(sprintf('%s %s', number_format($number, $precision), $units[$displayExponent])); + } + + /** + * Execute the given callback using the given locale. + * + * @param string $locale + * @param callable $callback + * @return mixed + */ + public static function withLocale(string $locale, callable $callback) + { + $previousLocale = static::$locale; + + static::useLocale($locale); + + return tap($callback(), fn () => static::useLocale($previousLocale)); + } + + /** + * Set the default locale. + * + * @param string $locale + * @return void + */ + public static function useLocale(string $locale) + { + static::$locale = $locale; + } + + /** + * Ensure the "intl" PHP exntension is installed. + * + * @return void + */ + protected static function ensureIntlExtensionIsInstalled() + { + if (! extension_loaded('intl')) { + throw new RuntimeException('The "intl" PHP extension is required to use this method.'); + } + } +} diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php new file mode 100644 index 000000000000..b04d0edd2f6d --- /dev/null +++ b/tests/Support/SupportNumberTest.php @@ -0,0 +1,178 @@ +needsIntlExtension(); + + $this->assertSame('0', Number::format(0)); + $this->assertSame('1', Number::format(1)); + $this->assertSame('10', Number::format(10)); + $this->assertSame('25', Number::format(25)); + $this->assertSame('100', Number::format(100)); + $this->assertSame('100,000', Number::format(100000)); + $this->assertSame('123,456,789', Number::format(123456789)); + + $this->assertSame('-1', Number::format(-1)); + $this->assertSame('-10', Number::format(-10)); + $this->assertSame('-25', Number::format(-25)); + + $this->assertSame('0.2', Number::format(0.2)); + $this->assertSame('1.23', Number::format(1.23)); + $this->assertSame('-1.23', Number::format(-1.23)); + $this->assertSame('123.456', Number::format(123.456)); + + $this->assertSame('∞', Number::format(INF)); + $this->assertSame('NaN', Number::format(NAN)); + } + + public function testFormatWithDifferentLocale() + { + $this->needsIntlExtension(); + + $this->assertSame('123,456,789', Number::format(123456789, 'en')); + $this->assertSame('123.456.789', Number::format(123456789, 'de')); + $this->assertSame('123 456 789', Number::format(123456789, 'fr')); + $this->assertSame('123 456 789', Number::format(123456789, 'ru')); + $this->assertSame('123 456 789', Number::format(123456789, 'sv')); + } + + public function testFormatWithAppLocale() + { + $this->needsIntlExtension(); + + $this->assertSame('123,456,789', Number::format(123456789)); + + Number::useLocale('de'); + + $this->assertSame('123.456.789', Number::format(123456789)); + + Number::useLocale('en'); + } + + public function testToPercent() + { + $this->needsIntlExtension(); + + $this->assertSame('0%', Number::toPercentage(0, precision: 0)); + $this->assertSame('0%', Number::toPercentage(0)); + $this->assertSame('1%', Number::toPercentage(1)); + $this->assertSame('10.00%', Number::toPercentage(10, precision: 2)); + $this->assertSame('100%', Number::toPercentage(100)); + $this->assertSame('100.00%', Number::toPercentage(100, precision: 2)); + + $this->assertSame('300%', Number::toPercentage(300)); + $this->assertSame('1,000%', Number::toPercentage(1000)); + + $this->assertSame('2%', Number::toPercentage(1.75)); + $this->assertSame('1.75%', Number::toPercentage(1.75, precision: 2)); + $this->assertSame('1.750%', Number::toPercentage(1.75, precision: 3)); + $this->assertSame('0%', Number::toPercentage(0.12345)); + $this->assertSame('0.12%', Number::toPercentage(0.12345, precision: 2)); + $this->assertSame('0.1235%', Number::toPercentage(0.12345, precision: 4)); + } + + public function testToCurrency() + { + $this->needsIntlExtension(); + + $this->assertSame('$0.00', Number::toCurrency(0)); + $this->assertSame('$1.00', Number::toCurrency(1)); + $this->assertSame('$10.00', Number::toCurrency(10)); + + $this->assertSame('€0.00', Number::toCurrency(0, 'EUR')); + $this->assertSame('€1.00', Number::toCurrency(1, 'EUR')); + $this->assertSame('€10.00', Number::toCurrency(10, 'EUR')); + + $this->assertSame('-$5.00', Number::toCurrency(-5)); + $this->assertSame('$5.00', Number::toCurrency(5.00)); + $this->assertSame('$5.32', Number::toCurrency(5.325)); + } + + public function testToCurrencyWithDifferentLocale() + { + $this->needsIntlExtension(); + + $this->assertSame('1,00 €', Number::toCurrency(1, 'EUR', 'de')); + $this->assertSame('1,00 $', Number::toCurrency(1, 'USD', 'de')); + $this->assertSame('1,00 £', Number::toCurrency(1, 'GBP', 'de')); + + $this->assertSame('123.456.789,12 $', Number::toCurrency(123456789.12345, 'USD', 'de')); + $this->assertSame('123.456.789,12 €', Number::toCurrency(123456789.12345, 'EUR', 'de')); + $this->assertSame('1 234,56 $US', Number::toCurrency(1234.56, 'USD', 'fr')); + } + + public function testBytesToHuman() + { + $this->assertSame('0 B', Number::toFileSize(0)); + $this->assertSame('1 B', Number::toFileSize(1)); + $this->assertSame('1 KB', Number::toFileSize(1024)); + $this->assertSame('2 KB', Number::toFileSize(2048)); + $this->assertSame('2.00 KB', Number::toFileSize(2048, precision: 2)); + $this->assertSame('1.23 KB', Number::toFileSize(1264, precision: 2)); + $this->assertSame('1.234 KB', Number::toFileSize(1264, 3)); + $this->assertSame('5 GB', Number::toFileSize(1024 * 1024 * 1024 * 5)); + $this->assertSame('10 TB', Number::toFileSize((1024 ** 4) * 10)); + $this->assertSame('10 PB', Number::toFileSize((1024 ** 5) * 10)); + $this->assertSame('1 ZB', Number::toFileSize(1024 ** 7)); + $this->assertSame('1 YB', Number::toFileSize(1024 ** 8)); + $this->assertSame('1,024 YB', Number::toFileSize(1024 ** 9)); + } + + public function testToHuman() + { + $this->assertSame('1', Number::forHumans(1)); + $this->assertSame('10', Number::forHumans(10)); + $this->assertSame('100', Number::forHumans(100)); + $this->assertSame('1 thousand', Number::forHumans(1000)); + $this->assertSame('1 million', Number::forHumans(1000000)); + $this->assertSame('1 billion', Number::forHumans(1000000000)); + $this->assertSame('1 trillion', Number::forHumans(1000000000000)); + $this->assertSame('1 quadrillion', Number::forHumans(1000000000000000)); + $this->assertSame('1 thousand quadrillion', Number::forHumans(1000000000000000000)); + + $this->assertSame('123', Number::forHumans(123)); + $this->assertSame('1 thousand', Number::forHumans(1234)); + $this->assertSame('1.23 thousand', Number::forHumans(1234, precision: 2)); + $this->assertSame('12 thousand', Number::forHumans(12345)); + $this->assertSame('1 million', Number::forHumans(1234567)); + $this->assertSame('1 billion', Number::forHumans(1234567890)); + $this->assertSame('1 trillion', Number::forHumans(1234567890123)); + $this->assertSame('1.23 trillion', Number::forHumans(1234567890123, precision: 2)); + $this->assertSame('1 quadrillion', Number::forHumans(1234567890123456)); + $this->assertSame('1.23 thousand quadrillion', Number::forHumans(1234567890123456789, precision: 2)); + $this->assertSame('490 thousand', Number::forHumans(489939)); + $this->assertSame('489.9390 thousand', Number::forHumans(489939, precision: 4)); + $this->assertSame('500.00000 million', Number::forHumans(500000000, precision: 5)); + + $this->assertSame('1 million quadrillion', Number::forHumans(1000000000000000000000)); + $this->assertSame('1 billion quadrillion', Number::forHumans(1000000000000000000000000)); + $this->assertSame('1 trillion quadrillion', Number::forHumans(1000000000000000000000000000)); + $this->assertSame('1 quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000)); + $this->assertSame('1 thousand quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000000)); + + $this->assertSame('0', Number::forHumans(0)); + $this->assertSame('-1', Number::forHumans(-1)); + $this->assertSame('-10', Number::forHumans(-10)); + $this->assertSame('-100', Number::forHumans(-100)); + $this->assertSame('-1 thousand', Number::forHumans(-1000)); + $this->assertSame('-1 million', Number::forHumans(-1000000)); + $this->assertSame('-1 billion', Number::forHumans(-1000000000)); + $this->assertSame('-1 trillion', Number::forHumans(-1000000000000)); + $this->assertSame('-1 quadrillion', Number::forHumans(-1000000000000000)); + $this->assertSame('-1 thousand quadrillion', Number::forHumans(-1000000000000000000)); + } + + protected function needsIntlExtension() + { + if (! extension_loaded('intl')) { + $this->markTestSkipped('The intl extension is not installed. Please install the extension to enable '.__CLASS__); + } + } +} From f17491a51a5b21f5ec687b3438941ce3063700da Mon Sep 17 00:00:00 2001 From: Jona Goldman Date: Thu, 16 Nov 2023 11:33:15 -0300 Subject: [PATCH 062/207] Fix the replace() method in DefaultService class (#49022) --- src/Illuminate/Support/DefaultProviders.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/DefaultProviders.php b/src/Illuminate/Support/DefaultProviders.php index 441a083fed5c..ef7422fd6239 100644 --- a/src/Illuminate/Support/DefaultProviders.php +++ b/src/Illuminate/Support/DefaultProviders.php @@ -70,7 +70,7 @@ public function replace(array $replacements) foreach ($replacements as $from => $to) { $key = $current->search($from); - $current = $key ? $current->replace([$key => $to]) : $current; + $current = is_int($key) ? $current->replace([$key => $to]) : $current; } return new static($current->values()->toArray()); From 38ae673608b334c983ba7e729e605eef9729eab8 Mon Sep 17 00:00:00 2001 From: Shin <2082119+shinsenter@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:38:36 +0900 Subject: [PATCH 063/207] Pass the property $validator as a parameter to the $callback Closure in the class ClosureValidationRule (#49015) Pass the property $validator as a parameter to the $callback Closure, so that we could take advantage of the existing $validator property in the class ClosureValidationRule. --- src/Illuminate/Validation/ClosureValidationRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/ClosureValidationRule.php b/src/Illuminate/Validation/ClosureValidationRule.php index fc57dadd5673..43dffe55cc57 100644 --- a/src/Illuminate/Validation/ClosureValidationRule.php +++ b/src/Illuminate/Validation/ClosureValidationRule.php @@ -64,7 +64,7 @@ public function passes($attribute, $value) $this->failed = true; return $this->pendingPotentiallyTranslatedString($attribute, $message); - }); + }, $this->validator); return ! $this->failed; } From e72327a0ad074a2d9d56742e6c7ff9eab1042528 Mon Sep 17 00:00:00 2001 From: xdevor <0.yu.zhuang@gmail.com> Date: Fri, 17 Nov 2023 03:04:36 +0800 Subject: [PATCH 064/207] [10.x] Fix Cache DatabaseStore::add() error occur on Postgres within transaction (#49025) * [10.x] Fix Cache DatabaseStore `add()` error on Postgres within transaction * Add Integration test for DatabaseStore::add() * Style fix * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Cache/DatabaseStore.php | 21 ++++--- .../Database/DatabaseCacheStoreTest.php | 60 +++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 457529fad8d2..17340240a88c 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -8,6 +8,7 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Database\PostgresConnection; use Illuminate\Database\QueryException; +use Illuminate\Database\SqlServerConnection; use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; @@ -153,17 +154,23 @@ public function add($key, $value, $seconds) $value = $this->serialize($value); $expiration = $this->getTime() + $seconds; + if (! is_null($this->get($key))) { + return false; + } + + $doesntSupportInsertOrIgnore = [SqlServerConnection::class]; + + if (! in_array(get_class($this->getConnection()), $doesntSupportInsertOrIgnore)) { + return $this->table()->insertOrIgnore(compact('key', 'value', 'expiration')) > 0; + } + try { return $this->table()->insert(compact('key', 'value', 'expiration')); } catch (QueryException) { - return $this->table() - ->where('key', $key) - ->where('expiration', '<=', $this->getTime()) - ->update([ - 'value' => $value, - 'expiration' => $expiration, - ]) >= 1; + // ... } + + return false; } /** diff --git a/tests/Integration/Database/DatabaseCacheStoreTest.php b/tests/Integration/Database/DatabaseCacheStoreTest.php index 1f5bac9835d0..86264fb81891 100644 --- a/tests/Integration/Database/DatabaseCacheStoreTest.php +++ b/tests/Integration/Database/DatabaseCacheStoreTest.php @@ -41,6 +41,66 @@ public function testValueCanUpdateExistCacheInTransaction() $this->assertSame('new-bar', $store->get('foo')); } + public function testAddOperationCanStoreNewCache() + { + $store = $this->getStore(); + + $result = $store->add('foo', 'bar', 60); + + $this->assertTrue($result); + $this->assertSame('bar', $store->get('foo')); + } + + public function testAddOperationShouldNotUpdateExistCache() + { + $store = $this->getStore(); + + $store->add('foo', 'bar', 60); + $result = $store->add('foo', 'new-bar', 60); + + $this->assertFalse($result); + $this->assertSame('bar', $store->get('foo')); + } + + public function testAddOperationShouldNotUpdateExistCacheInTransaction() + { + $store = $this->getStore(); + + $store->add('foo', 'bar', 60); + + DB::beginTransaction(); + $result = $store->add('foo', 'new-bar', 60); + DB::commit(); + + $this->assertFalse($result); + $this->assertSame('bar', $store->get('foo')); + } + + public function testAddOperationCanUpdateIfCacheExpired() + { + $store = $this->getStore(); + + $store->add('foo', 'bar', 0); + $result = $store->add('foo', 'new-bar', 60); + + $this->assertTrue($result); + $this->assertSame('new-bar', $store->get('foo')); + } + + public function testAddOperationCanUpdateIfCacheExpiredInTransaction() + { + $store = $this->getStore(); + + $store->add('foo', 'bar', 0); + + DB::beginTransaction(); + $result = $store->add('foo', 'new-bar', 60); + DB::commit(); + + $this->assertTrue($result); + $this->assertSame('new-bar', $store->get('foo')); + } + protected function getStore() { return Cache::store('database'); From e275b87eb9d2314c10d8fd5b8a4024de3066d4e8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 17 Nov 2023 08:31:55 -0600 Subject: [PATCH 065/207] [10.x] Support asserting against chained batches (#49003) * support asserting against chained batches * work on chained batch truth tests * work on chained batch truth tests * more work on chained batch truth tests * continue working on chained batch truth tests * Apply fixes from StyleCI * attempting to unify chained batch testing * simplify code * add another test * simplify code --------- Co-authored-by: StyleCI Bot --- src/Illuminate/Bus/ChainedBatch.php | 24 ++++-- .../Support/Testing/Fakes/BusFake.php | 74 ++++++++++--------- .../Testing/Fakes/ChainedBatchTruthTest.php | 37 ++++++++++ tests/Support/SupportTestingBusFakeTest.php | 50 +++++++++++++ 4 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php diff --git a/src/Illuminate/Bus/ChainedBatch.php b/src/Illuminate/Bus/ChainedBatch.php index 6d4f92107400..903c6684599f 100644 --- a/src/Illuminate/Bus/ChainedBatch.php +++ b/src/Illuminate/Bus/ChainedBatch.php @@ -72,7 +72,19 @@ public static function prepareNestedBatches(Collection $jobs): Collection */ public function handle() { - $batch = new PendingBatch(Container::getInstance(), $this->jobs); + $this->attachRemainderOfChainToEndOfBatch( + $this->toPendingBatch() + )->dispatch(); + } + + /** + * Convert the chained batch instance into a pending batch. + * + * @return \Illuminate\Bus\PendingBatch + */ + public function toPendingBatch() + { + $batch = Container::getInstance()->make(Dispatcher::class)->batch($this->jobs); $batch->name = $this->name; $batch->options = $this->options; @@ -85,8 +97,6 @@ public function handle() $batch->onConnection($this->connection); } - $this->dispatchRemainderOfChainAfterBatch($batch); - foreach ($this->chainCatchCallbacks ?? [] as $callback) { $batch->catch(function (Batch $batch, ?Throwable $exception) use ($callback) { if (! $batch->allowsFailures()) { @@ -95,16 +105,16 @@ public function handle() }); } - $batch->dispatch(); + return $batch; } /** * Move the remainder of the chain to a "finally" batch callback. * * @param \Illuminate\Bus\PendingBatch $batch - * @return + * @return \Illuminate\Bus\PendingBatch */ - protected function dispatchRemainderOfChainAfterBatch(PendingBatch $batch) + protected function attachRemainderOfChainToEndOfBatch(PendingBatch $batch) { if (! empty($this->chained)) { $next = unserialize(array_shift($this->chained)); @@ -126,5 +136,7 @@ protected function dispatchRemainderOfChainAfterBatch(PendingBatch $batch) $this->chained = []; } + + return $batch; } } diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index fb6209f01fdc..3d717eae6231 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -334,6 +334,12 @@ public function assertChained(array $expectedChain) if ($command instanceof Closure) { [$command, $callback] = [$this->firstClosureParameterType($command), $command]; + } elseif ($command instanceof ChainedBatchTruthTest) { + $instance = $command; + + $command = ChainedBatch::class; + + $callback = fn ($job) => $instance($job->toPendingBatch()); } elseif (! is_string($command)) { $instance = $command; @@ -349,9 +355,7 @@ public function assertChained(array $expectedChain) "The expected [{$command}] job was not dispatched." ); - $this->isChainOfObjects($expectedChain) - ? $this->assertDispatchedWithChainOfObjects($command, $expectedChain, $callback) - : $this->assertDispatchedWithChainOfClasses($command, $expectedChain, $callback); + $this->assertDispatchedWithChainOfObjects($command, $expectedChain, $callback); } /** @@ -388,7 +392,7 @@ public function assertDispatchedWithoutChain($command, $callback = null) "The expected [{$command}] job was not dispatched." ); - $this->assertDispatchedWithChainOfClasses($command, [], $callback); + $this->assertDispatchedWithChainOfObjects($command, [], $callback); } /** @@ -401,48 +405,46 @@ public function assertDispatchedWithoutChain($command, $callback = null) */ protected function assertDispatchedWithChainOfObjects($command, $expectedChain, $callback) { - $chain = collect($expectedChain)->map(fn ($job) => serialize($job))->all(); + $chain = $expectedChain; PHPUnit::assertTrue( - $this->dispatched($command, $callback)->filter( - fn ($job) => $job->chained == $chain - )->isNotEmpty(), + $this->dispatched($command, $callback)->filter(function ($job) use ($chain) { + if (count($chain) !== count($job->chained)) { + return false; + } + + foreach ($job->chained as $index => $serializedChainedJob) { + if ($chain[$index] instanceof ChainedBatchTruthTest) { + $chainedBatch = unserialize($serializedChainedJob); + + if (! $chainedBatch instanceof ChainedBatch || + ! $chain[$index]($chainedBatch->toPendingBatch())) { + return false; + } + } elseif (is_string($chain[$index])) { + if ($chain[$index] != get_class(unserialize($serializedChainedJob))) { + return false; + } + } elseif (serialize($chain[$index]) != $serializedChainedJob) { + return false; + } + } + + return true; + })->isNotEmpty(), 'The expected chain was not dispatched.' ); } /** - * Assert if a job was dispatched with chained jobs based on a truth-test callback. + * Create a new assertion about a chained batch. * - * @param string $command - * @param array $expectedChain - * @param callable|null $callback - * @return void - */ - protected function assertDispatchedWithChainOfClasses($command, $expectedChain, $callback) - { - $matching = $this->dispatched($command, $callback)->map->chained->map(function ($chain) { - return collect($chain)->map( - fn ($job) => get_class(unserialize($job)) - ); - })->filter( - fn ($chain) => $chain->all() === $expectedChain - ); - - PHPUnit::assertTrue( - $matching->isNotEmpty(), 'The expected chain was not dispatched.' - ); - } - - /** - * Determine if the given chain is entirely composed of objects. - * - * @param array $chain - * @return bool + * @param \Closure $callback + * @return \Illuminate\Support\Testing\Fakes\ChainedBatchTruthTest */ - protected function isChainOfObjects($chain) + public function chainedBatch(Closure $callback) { - return ! collect($chain)->contains(fn ($job) => ! is_object($job)); + return new ChainedBatchTruthTest($callback); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php b/src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php new file mode 100644 index 000000000000..4d1cec732ed3 --- /dev/null +++ b/src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php @@ -0,0 +1,37 @@ +callback = $callback; + } + + /** + * Invoke the truth test with the given pending batch. + * + * @param \Illuminate\Bus\PendingBatch + * @return bool + */ + public function __invoke($pendingBatch) + { + return call_user_func($this->callback, $pendingBatch); + } +} diff --git a/tests/Support/SupportTestingBusFakeTest.php b/tests/Support/SupportTestingBusFakeTest.php index 9377070f0212..b1d6d8bfd952 100644 --- a/tests/Support/SupportTestingBusFakeTest.php +++ b/tests/Support/SupportTestingBusFakeTest.php @@ -4,6 +4,8 @@ use Illuminate\Bus\Batch; use Illuminate\Bus\Queueable; +use Illuminate\Container\Container; +use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\QueueingDispatcher; use Illuminate\Support\Testing\Fakes\BatchRepositoryFake; use Illuminate\Support\Testing\Fakes\BusFake; @@ -468,6 +470,10 @@ public function testAssertNothingDispatched() public function testAssertChained() { + Container::setInstance($container = new Container); + + $container->instance(Dispatcher::class, $this->fake); + $this->fake->chain([ new ChainedJobStub, ])->dispatch(); @@ -485,6 +491,50 @@ public function testAssertChained() ChainedJobStub::class, OtherBusJobStub::class, ]); + + $this->fake->chain([ + new ChainedJobStub, + $this->fake->batch([ + new OtherBusJobStub, + new OtherBusJobStub, + ]), + new ChainedJobStub, + ])->dispatch(); + + $this->fake->assertChained([ + ChainedJobStub::class, + $this->fake->chainedBatch(function ($pendingBatch) { + return $pendingBatch->jobs->count() === 2; + }), + ChainedJobStub::class, + ]); + + $this->fake->assertChained([ + new ChainedJobStub, + $this->fake->chainedBatch(function ($pendingBatch) { + return $pendingBatch->jobs->count() === 2; + }), + new ChainedJobStub, + ]); + + $this->fake->chain([ + $this->fake->batch([ + new OtherBusJobStub, + new OtherBusJobStub, + ]), + new ChainedJobStub, + new ChainedJobStub, + ])->dispatch(); + + $this->fake->assertChained([ + $this->fake->chainedBatch(function ($pendingBatch) { + return $pendingBatch->jobs->count() === 2; + }), + ChainedJobStub::class, + ChainedJobStub::class, + ]); + + Container::setInstance(null); } public function testAssertDispatchedWithIgnoreClass() From 9874d6b6f5a056d7dc5040795243d69e03db3141 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Fri, 17 Nov 2023 14:32:29 +0000 Subject: [PATCH 066/207] 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 08eddf794437..13fe1c9fedfc 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -33,6 +33,7 @@ * @method static void assertNotDispatchedAfterResponse(string|\Closure $command, callable|null $callback = null) * @method static void assertChained(array $expectedChain) * @method static void assertDispatchedWithoutChain(string|\Closure $command, callable|null $callback = null) + * @method static \Illuminate\Support\Testing\Fakes\ChainedBatchTruthTest chainedBatch(\Closure $callback) * @method static void assertBatched(callable $callback) * @method static void assertBatchCount(int $count) * @method static void assertNothingBatched() From 59c509b298f755d066b8423ed7fb193fb44f6553 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 17 Nov 2023 08:35:01 -0600 Subject: [PATCH 067/207] rename methods --- src/Illuminate/Support/Number.php | 6 +- tests/Support/SupportNumberTest.php | 88 ++++++++++++++--------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index f86e681ecb0a..49feddf71eeb 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -41,7 +41,7 @@ public static function format(int|float $number, ?string $locale = null) * @param ?string $locale * @return string|false */ - public static function toPercentage(int|float $number, int $precision = 0, ?string $locale = null) + public static function percentage(int|float $number, int $precision = 0, ?string $locale = null) { static::ensureIntlExtensionIsInstalled(); @@ -60,7 +60,7 @@ public static function toPercentage(int|float $number, int $precision = 0, ?stri * @param ?string $locale * @return string|false */ - public static function toCurrency(int|float $number, string $currency = 'USD', ?string $locale = null) + public static function currency(int|float $number, string $currency = 'USD', ?string $locale = null) { static::ensureIntlExtensionIsInstalled(); @@ -76,7 +76,7 @@ public static function toCurrency(int|float $number, string $currency = 'USD', ? * @param int $precision * @return string */ - public static function toFileSize(int|float $bytes, int $precision = 0) + public static function fileSize(int|float $bytes, int $precision = 0) { $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index b04d0edd2f6d..73787bae682a 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -60,69 +60,69 @@ public function testToPercent() { $this->needsIntlExtension(); - $this->assertSame('0%', Number::toPercentage(0, precision: 0)); - $this->assertSame('0%', Number::toPercentage(0)); - $this->assertSame('1%', Number::toPercentage(1)); - $this->assertSame('10.00%', Number::toPercentage(10, precision: 2)); - $this->assertSame('100%', Number::toPercentage(100)); - $this->assertSame('100.00%', Number::toPercentage(100, precision: 2)); - - $this->assertSame('300%', Number::toPercentage(300)); - $this->assertSame('1,000%', Number::toPercentage(1000)); - - $this->assertSame('2%', Number::toPercentage(1.75)); - $this->assertSame('1.75%', Number::toPercentage(1.75, precision: 2)); - $this->assertSame('1.750%', Number::toPercentage(1.75, precision: 3)); - $this->assertSame('0%', Number::toPercentage(0.12345)); - $this->assertSame('0.12%', Number::toPercentage(0.12345, precision: 2)); - $this->assertSame('0.1235%', Number::toPercentage(0.12345, precision: 4)); + $this->assertSame('0%', Number::percentage(0, precision: 0)); + $this->assertSame('0%', Number::percentage(0)); + $this->assertSame('1%', Number::percentage(1)); + $this->assertSame('10.00%', Number::percentage(10, precision: 2)); + $this->assertSame('100%', Number::percentage(100)); + $this->assertSame('100.00%', Number::percentage(100, precision: 2)); + + $this->assertSame('300%', Number::percentage(300)); + $this->assertSame('1,000%', Number::percentage(1000)); + + $this->assertSame('2%', Number::percentage(1.75)); + $this->assertSame('1.75%', Number::percentage(1.75, precision: 2)); + $this->assertSame('1.750%', Number::percentage(1.75, precision: 3)); + $this->assertSame('0%', Number::percentage(0.12345)); + $this->assertSame('0.12%', Number::percentage(0.12345, precision: 2)); + $this->assertSame('0.1235%', Number::percentage(0.12345, precision: 4)); } public function testToCurrency() { $this->needsIntlExtension(); - $this->assertSame('$0.00', Number::toCurrency(0)); - $this->assertSame('$1.00', Number::toCurrency(1)); - $this->assertSame('$10.00', Number::toCurrency(10)); + $this->assertSame('$0.00', Number::currency(0)); + $this->assertSame('$1.00', Number::currency(1)); + $this->assertSame('$10.00', Number::currency(10)); - $this->assertSame('€0.00', Number::toCurrency(0, 'EUR')); - $this->assertSame('€1.00', Number::toCurrency(1, 'EUR')); - $this->assertSame('€10.00', Number::toCurrency(10, 'EUR')); + $this->assertSame('€0.00', Number::currency(0, 'EUR')); + $this->assertSame('€1.00', Number::currency(1, 'EUR')); + $this->assertSame('€10.00', Number::currency(10, 'EUR')); - $this->assertSame('-$5.00', Number::toCurrency(-5)); - $this->assertSame('$5.00', Number::toCurrency(5.00)); - $this->assertSame('$5.32', Number::toCurrency(5.325)); + $this->assertSame('-$5.00', Number::currency(-5)); + $this->assertSame('$5.00', Number::currency(5.00)); + $this->assertSame('$5.32', Number::currency(5.325)); } public function testToCurrencyWithDifferentLocale() { $this->needsIntlExtension(); - $this->assertSame('1,00 €', Number::toCurrency(1, 'EUR', 'de')); - $this->assertSame('1,00 $', Number::toCurrency(1, 'USD', 'de')); - $this->assertSame('1,00 £', Number::toCurrency(1, 'GBP', 'de')); + $this->assertSame('1,00 €', Number::currency(1, 'EUR', 'de')); + $this->assertSame('1,00 $', Number::currency(1, 'USD', 'de')); + $this->assertSame('1,00 £', Number::currency(1, 'GBP', 'de')); - $this->assertSame('123.456.789,12 $', Number::toCurrency(123456789.12345, 'USD', 'de')); - $this->assertSame('123.456.789,12 €', Number::toCurrency(123456789.12345, 'EUR', 'de')); - $this->assertSame('1 234,56 $US', Number::toCurrency(1234.56, 'USD', 'fr')); + $this->assertSame('123.456.789,12 $', Number::currency(123456789.12345, 'USD', 'de')); + $this->assertSame('123.456.789,12 €', Number::currency(123456789.12345, 'EUR', 'de')); + $this->assertSame('1 234,56 $US', Number::currency(1234.56, 'USD', 'fr')); } public function testBytesToHuman() { - $this->assertSame('0 B', Number::toFileSize(0)); - $this->assertSame('1 B', Number::toFileSize(1)); - $this->assertSame('1 KB', Number::toFileSize(1024)); - $this->assertSame('2 KB', Number::toFileSize(2048)); - $this->assertSame('2.00 KB', Number::toFileSize(2048, precision: 2)); - $this->assertSame('1.23 KB', Number::toFileSize(1264, precision: 2)); - $this->assertSame('1.234 KB', Number::toFileSize(1264, 3)); - $this->assertSame('5 GB', Number::toFileSize(1024 * 1024 * 1024 * 5)); - $this->assertSame('10 TB', Number::toFileSize((1024 ** 4) * 10)); - $this->assertSame('10 PB', Number::toFileSize((1024 ** 5) * 10)); - $this->assertSame('1 ZB', Number::toFileSize(1024 ** 7)); - $this->assertSame('1 YB', Number::toFileSize(1024 ** 8)); - $this->assertSame('1,024 YB', Number::toFileSize(1024 ** 9)); + $this->assertSame('0 B', Number::fileSize(0)); + $this->assertSame('1 B', Number::fileSize(1)); + $this->assertSame('1 KB', Number::fileSize(1024)); + $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('1.234 KB', Number::fileSize(1264, 3)); + $this->assertSame('5 GB', Number::fileSize(1024 * 1024 * 1024 * 5)); + $this->assertSame('10 TB', Number::fileSize((1024 ** 4) * 10)); + $this->assertSame('10 PB', Number::fileSize((1024 ** 5) * 10)); + $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)); } public function testToHuman() From 708a90b4480fb42e548717de887a3ffa156b8bf6 Mon Sep 17 00:00:00 2001 From: xdevor <0.yu.zhuang@gmail.com> Date: Fri, 17 Nov 2023 22:55:25 +0800 Subject: [PATCH 068/207] [10.x] Prevent DB `Cache::get()` occur race condition (#49031) * [10.x] Prevent Cache::get() occur race condition * Fix test * Add Cache Get and ForgetIfExpired integration * Use Cache::put() to store expired cache * Fix test * Add Cache Get and ForgetIfExpired integration test * Test Use Cache::put() to store expired cache * Fix wrong test statement * Fix test * Fix test * [10.x] Fix DatabaseStore `add()` failed due to incorrect key * Style fix * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Cache/DatabaseStore.php | 26 +++++- src/Illuminate/Support/Number.php | 2 +- tests/Cache/CacheDatabaseStoreTest.php | 4 +- .../Database/DatabaseCacheStoreTest.php | 93 ++++++++++++++++++- 4 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 17340240a88c..e5b72107ce96 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -115,7 +115,7 @@ public function get($key) // item from the cache. Then we will return a null value since the cache is // expired. We will use "Carbon" to make this comparison with the column. if ($this->currentTime() >= $cache->expiration) { - $this->forget($key); + $this->forgetIfExpired($key); return; } @@ -150,14 +150,14 @@ public function put($key, $value, $seconds) */ public function add($key, $value, $seconds) { - $key = $this->prefix.$key; - $value = $this->serialize($value); - $expiration = $this->getTime() + $seconds; - if (! is_null($this->get($key))) { return false; } + $key = $this->prefix.$key; + $value = $this->serialize($value); + $expiration = $this->getTime() + $seconds; + $doesntSupportInsertOrIgnore = [SqlServerConnection::class]; if (! in_array(get_class($this->getConnection()), $doesntSupportInsertOrIgnore)) { @@ -316,6 +316,22 @@ public function forget($key) return true; } + /** + * Remove an item from the cache if it is expired. + * + * @param string $key + * @return bool + */ + public function forgetIfExpired($key) + { + $this->table() + ->where('key', '=', $this->prefix.$key) + ->where('expiration', '<=', $this->getTime()) + ->delete(); + + return true; + } + /** * Remove all items from the cache. * diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 49feddf71eeb..5e0f488d2145 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -117,7 +117,7 @@ public static function forHumans(int|float $number, int $precision = 0) $displayExponent = $numberExponent - ($numberExponent % 3); $number /= pow(10, $displayExponent); - return trim(sprintf('%s %s', number_format($number, $precision), $units[$displayExponent])); + return trim(sprintf('%s %s', number_format($number, $precision), $units[$displayExponent] ?? '')); } /** diff --git a/tests/Cache/CacheDatabaseStoreTest.php b/tests/Cache/CacheDatabaseStoreTest.php index 48f838d4c6e6..4a2bfaa7fbf7 100755 --- a/tests/Cache/CacheDatabaseStoreTest.php +++ b/tests/Cache/CacheDatabaseStoreTest.php @@ -30,12 +30,12 @@ public function testNullIsReturnedWhenItemNotFound() public function testNullIsReturnedAndItemDeletedWhenItemIsExpired() { - $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['forget'])->setConstructorArgs($this->getMocks())->getMock(); + $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['forgetIfExpired'])->setConstructorArgs($this->getMocks())->getMock(); $table = m::mock(stdClass::class); $store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table); $table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table); $table->shouldReceive('first')->once()->andReturn((object) ['expiration' => 1]); - $store->expects($this->once())->method('forget')->with($this->equalTo('foo'))->willReturn(null); + $store->expects($this->once())->method('forgetIfExpired')->with($this->equalTo('foo'))->willReturn(null); $this->assertNull($store->get('foo')); } diff --git a/tests/Integration/Database/DatabaseCacheStoreTest.php b/tests/Integration/Database/DatabaseCacheStoreTest.php index 86264fb81891..f9d7d440ab9a 100644 --- a/tests/Integration/Database/DatabaseCacheStoreTest.php +++ b/tests/Integration/Database/DatabaseCacheStoreTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Database; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Orchestra\Testbench\Attributes\WithMigration; @@ -18,6 +19,15 @@ public function testValueCanStoreNewCache() $this->assertSame('bar', $store->get('foo')); } + public function testPutOperationShouldNotStoreExpired() + { + $store = $this->getStore(); + + $store->put('foo', 'bar', 0); + + $this->assertDatabaseMissing($this->getCacheTableName(), ['key' => $this->withCachePrefix('foo')]); + } + public function testValueCanUpdateExistCache() { $store = $this->getStore(); @@ -41,6 +51,16 @@ public function testValueCanUpdateExistCacheInTransaction() $this->assertSame('new-bar', $store->get('foo')); } + public function testAddOperationShouldNotStoreExpired() + { + $store = $this->getStore(); + + $result = $store->add('foo', 'bar', 0); + + $this->assertFalse($result); + $this->assertDatabaseMissing($this->getCacheTableName(), ['key' => $this->withCachePrefix('foo')]); + } + public function testAddOperationCanStoreNewCache() { $store = $this->getStore(); @@ -80,7 +100,7 @@ public function testAddOperationCanUpdateIfCacheExpired() { $store = $this->getStore(); - $store->add('foo', 'bar', 0); + $this->insertToCacheTable('foo', 'bar', 0); $result = $store->add('foo', 'new-bar', 60); $this->assertTrue($result); @@ -91,7 +111,7 @@ public function testAddOperationCanUpdateIfCacheExpiredInTransaction() { $store = $this->getStore(); - $store->add('foo', 'bar', 0); + $this->insertToCacheTable('foo', 'bar', 0); DB::beginTransaction(); $result = $store->add('foo', 'new-bar', 60); @@ -101,8 +121,77 @@ public function testAddOperationCanUpdateIfCacheExpiredInTransaction() $this->assertSame('new-bar', $store->get('foo')); } + public function testGetOperationReturnNullIfExpired() + { + $store = $this->getStore(); + + $this->insertToCacheTable('foo', 'bar', 0); + + $result = $store->get('foo'); + + $this->assertNull($result); + } + + public function testGetOperationCanDeleteExpired() + { + $store = $this->getStore(); + + $this->insertToCacheTable('foo', 'bar', 0); + + $store->get('foo'); + + $this->assertDatabaseMissing($this->getCacheTableName(), ['key' => $this->withCachePrefix('foo')]); + } + + public function testForgetIfExpiredOperationCanDeleteExpired() + { + $store = $this->getStore(); + + $this->insertToCacheTable('foo', 'bar', 0); + + $store->forgetIfExpired('foo'); + + $this->assertDatabaseMissing($this->getCacheTableName(), ['key' => $this->withCachePrefix('foo')]); + } + + public function testForgetIfExpiredOperationShouldNotDeleteUnExpired() + { + $store = $this->getStore(); + + $store->put('foo', 'bar', 60); + + $store->forgetIfExpired('foo'); + + $this->assertDatabaseHas($this->getCacheTableName(), ['key' => $this->withCachePrefix('foo')]); + } + + /** + * @return \Illuminate\Cache\DatabaseStore + */ protected function getStore() { return Cache::store('database'); } + + protected function getCacheTableName() + { + return config('cache.stores.database.table'); + } + + protected function withCachePrefix(string $key) + { + return config('cache.prefix').$key; + } + + protected function insertToCacheTable(string $key, $value, $ttl = 60) + { + DB::table($this->getCacheTableName()) + ->insert( + [ + 'key' => $this->withCachePrefix($key), + 'value' => $value, + 'expiration' => Carbon::now()->addSeconds($ttl)->getTimestamp(), + ] + ); + } } From 8ab218449481f6f7d71f47e7ec4a3e4cf2ddc05f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 17 Nov 2023 09:31:45 -0600 Subject: [PATCH 069/207] working on number class --- src/Illuminate/Support/Number.php | 39 ++++++++++++++++++++--------- tests/Support/SupportNumberTest.php | 29 +++++++++++++++++---- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 49feddf71eeb..cca9795ff0e6 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -21,15 +21,23 @@ class Number * Format the given number according to the current locale. * * @param int|float $number + * @param int|null $precision + * @param int|null $maxPrecision * @param ?string $locale * @return string|false */ - public static function format(int|float $number, ?string $locale = null) + public static function format(int|float $number, ?int $precision = null, ?int $maxPrecision = null, ?string $locale = null) { static::ensureIntlExtensionIsInstalled(); $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::DECIMAL); + if (! is_null($maxPrecision)) { + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $maxPrecision); + } elseif (! is_null($precision)) { + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); + } + return $formatter->format($number); } @@ -38,16 +46,21 @@ public static function format(int|float $number, ?string $locale = null) * * @param int|float $number * @param int $precision + * @param int|null $maxPrecision * @param ?string $locale * @return string|false */ - public static function percentage(int|float $number, int $precision = 0, ?string $locale = null) + public static function percentage(int|float $number, int $precision = 0, ?int $maxPrecision = null, ?string $locale = null) { static::ensureIntlExtensionIsInstalled(); $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::PERCENT); - $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); + if (! is_null($maxPrecision)) { + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $maxPrecision); + } else { + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); + } return $formatter->format($number / 100); } @@ -56,17 +69,17 @@ public static function percentage(int|float $number, int $precision = 0, ?string * Convert the given number to its currency equivalent. * * @param int|float $number - * @param string $currency + * @param string $in * @param ?string $locale * @return string|false */ - public static function currency(int|float $number, string $currency = 'USD', ?string $locale = null) + public static function currency(int|float $number, string $in = 'USD', ?string $locale = null) { static::ensureIntlExtensionIsInstalled(); $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::CURRENCY); - return $formatter->formatCurrency($number, $currency); + return $formatter->formatCurrency($number, $in); } /** @@ -74,9 +87,10 @@ public static function currency(int|float $number, string $currency = 'USD', ?st * * @param int|float $bytes * @param int $precision + * @param int|null $maxPrecision * @return string */ - public static function fileSize(int|float $bytes, int $precision = 0) + public static function fileSize(int|float $bytes, int $precision = 0, ?int $maxPrecision = null) { $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; @@ -84,7 +98,7 @@ public static function fileSize(int|float $bytes, int $precision = 0) $bytes /= 1024; } - return sprintf('%s %s', number_format($bytes, $precision), $units[$i]); + return sprintf('%s %s', static::format($bytes, $precision, $maxPrecision), $units[$i]); } /** @@ -92,9 +106,10 @@ public static function fileSize(int|float $bytes, int $precision = 0) * * @param int $number * @param int $precision + * @param int|null $maxPrecision * @return string */ - public static function forHumans(int|float $number, int $precision = 0) + public static function forHumans(int|float $number, int $precision = 0, ?int $maxPrecision = null) { $units = [ 3 => 'thousand', @@ -108,16 +123,16 @@ public static function forHumans(int|float $number, int $precision = 0) case $number === 0: return '0'; case $number < 0: - return sprintf('-%s', static::forHumans(abs($number), $precision)); + return sprintf('-%s', static::forHumans(abs($number), $precision, $maxPrecision)); case $number >= 1e15: - return sprintf('%s quadrillion', static::forHumans($number / 1e15, $precision)); + return sprintf('%s quadrillion', static::forHumans($number / 1e15, $precision, $maxPrecision)); } $numberExponent = floor(log10($number)); $displayExponent = $numberExponent - ($numberExponent % 3); $number /= pow(10, $displayExponent); - return trim(sprintf('%s %s', number_format($number, $precision), $units[$displayExponent])); + return trim(sprintf('%s %s', static::format($number, $precision, $maxPrecision), $units[$displayExponent] ?? '')); } /** diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 73787bae682a..58470731e70b 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -17,6 +17,10 @@ public function testFormat() $this->assertSame('25', Number::format(25)); $this->assertSame('100', Number::format(100)); $this->assertSame('100,000', Number::format(100000)); + $this->assertSame('100,000.00', Number::format(100000, precision: 2)); + $this->assertSame('100,000.12', Number::format(100000.123, precision: 2)); + $this->assertSame('100,000.123', Number::format(100000.1234, maxPrecision: 3)); + $this->assertSame('100,000.124', Number::format(100000.1236, maxPrecision: 3)); $this->assertSame('123,456,789', Number::format(123456789)); $this->assertSame('-1', Number::format(-1)); @@ -24,6 +28,8 @@ public function testFormat() $this->assertSame('-25', Number::format(-25)); $this->assertSame('0.2', Number::format(0.2)); + $this->assertSame('0.20', Number::format(0.2, precision: 2)); + $this->assertSame('0.123', Number::format(0.1234, maxPrecision: 3)); $this->assertSame('1.23', Number::format(1.23)); $this->assertSame('-1.23', Number::format(-1.23)); $this->assertSame('123.456', Number::format(123.456)); @@ -36,11 +42,11 @@ public function testFormatWithDifferentLocale() { $this->needsIntlExtension(); - $this->assertSame('123,456,789', Number::format(123456789, 'en')); - $this->assertSame('123.456.789', Number::format(123456789, 'de')); - $this->assertSame('123 456 789', Number::format(123456789, 'fr')); - $this->assertSame('123 456 789', Number::format(123456789, 'ru')); - $this->assertSame('123 456 789', Number::format(123456789, 'sv')); + $this->assertSame('123,456,789', Number::format(123456789, locale: 'en')); + $this->assertSame('123.456.789', Number::format(123456789, locale: 'de')); + $this->assertSame('123 456 789', Number::format(123456789, locale: 'fr')); + $this->assertSame('123 456 789', Number::format(123456789, locale: 'ru')); + $this->assertSame('123 456 789', Number::format(123456789, locale: 'sv')); } public function testFormatWithAppLocale() @@ -66,6 +72,7 @@ public function testToPercent() $this->assertSame('10.00%', Number::percentage(10, precision: 2)); $this->assertSame('100%', Number::percentage(100)); $this->assertSame('100.00%', Number::percentage(100, precision: 2)); + $this->assertSame('100.123%', Number::percentage(100.1234, maxPrecision: 3)); $this->assertSame('300%', Number::percentage(300)); $this->assertSame('1,000%', Number::percentage(1000)); @@ -74,6 +81,7 @@ public function testToPercent() $this->assertSame('1.75%', Number::percentage(1.75, precision: 2)); $this->assertSame('1.750%', Number::percentage(1.75, precision: 3)); $this->assertSame('0%', Number::percentage(0.12345)); + $this->assertSame('0.00%', Number::percentage(0, precision: 2)); $this->assertSame('0.12%', Number::percentage(0.12345, precision: 2)); $this->assertSame('0.1235%', Number::percentage(0.12345, precision: 4)); } @@ -111,11 +119,13 @@ public function testToCurrencyWithDifferentLocale() public function testBytesToHuman() { $this->assertSame('0 B', Number::fileSize(0)); + $this->assertSame('0.00 B', Number::fileSize(0, precision: 2)); $this->assertSame('1 B', Number::fileSize(1)); $this->assertSame('1 KB', Number::fileSize(1024)); $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('1.234 KB', Number::fileSize(1264.12345, maxPrecision: 3)); $this->assertSame('1.234 KB', Number::fileSize(1264, 3)); $this->assertSame('5 GB', Number::fileSize(1024 * 1024 * 1024 * 5)); $this->assertSame('10 TB', Number::fileSize((1024 ** 4) * 10)); @@ -128,9 +138,14 @@ public function testBytesToHuman() public function testToHuman() { $this->assertSame('1', Number::forHumans(1)); + $this->assertSame('1.00', Number::forHumans(1, precision: 2)); $this->assertSame('10', Number::forHumans(10)); $this->assertSame('100', Number::forHumans(100)); $this->assertSame('1 thousand', Number::forHumans(1000)); + $this->assertSame('1.00 thousand', Number::forHumans(1000, precision: 2)); + $this->assertSame('1 thousand', Number::forHumans(1000, maxPrecision: 2)); + $this->assertSame('1 thousand', Number::forHumans(1230)); + $this->assertSame('1.2 thousand', Number::forHumans(1230, maxPrecision: 1)); $this->assertSame('1 million', Number::forHumans(1000000)); $this->assertSame('1 billion', Number::forHumans(1000000000)); $this->assertSame('1 trillion', Number::forHumans(1000000000000)); @@ -159,12 +174,16 @@ public function testToHuman() $this->assertSame('0', Number::forHumans(0)); $this->assertSame('-1', Number::forHumans(-1)); + $this->assertSame('-1.00', Number::forHumans(-1, precision: 2)); $this->assertSame('-10', Number::forHumans(-10)); $this->assertSame('-100', Number::forHumans(-100)); $this->assertSame('-1 thousand', Number::forHumans(-1000)); + $this->assertSame('-1.23 thousand', Number::forHumans(-1234, precision: 2)); + $this->assertSame('-1.2 thousand', Number::forHumans(-1234, maxPrecision: 1)); $this->assertSame('-1 million', Number::forHumans(-1000000)); $this->assertSame('-1 billion', Number::forHumans(-1000000000)); $this->assertSame('-1 trillion', Number::forHumans(-1000000000000)); + $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)); } From d9c328ce85b28b01823dcb94b0ba99cb1e3ea0f1 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 17 Nov 2023 10:34:55 -0600 Subject: [PATCH 070/207] add spell and ordinal --- src/Illuminate/Support/Number.php | 32 +++++++++++++++++++++++++++++ tests/Support/SupportNumberTest.php | 20 ++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index cca9795ff0e6..6053f0a66f6e 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -41,6 +41,38 @@ public static function format(int|float $number, ?int $precision = null, ?int $m return $formatter->format($number); } + /** + * Spell out the given number in the given locale. + * + * @param int|float $number + * @param ?string $locale + * @return string + */ + public static function spell(int|float $number, ?string $locale = null) + { + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::SPELLOUT); + + return $formatter->format($number); + } + + /** + * Convert the given number to ordinal form. + * + * @param int|float $number + * @param ?string $locale + * @return string + */ + public static function ordinal(int|float $number, ?string $locale = null) + { + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::ORDINAL); + + return $formatter->format($number); + } + /** * Convert the given number to its percentage equivalent. * diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 58470731e70b..e758ecabdede 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -62,6 +62,26 @@ public function testFormatWithAppLocale() Number::useLocale('en'); } + public function testSpellout() + { + $this->assertSame('ten', Number::spell(10)); + $this->assertSame('one point two', Number::spell(1.2)); + } + + public function testSpelloutWithLocale() + { + $this->needsIntlExtension(); + + $this->assertSame('trois', Number::spell(3, 'fr')); + } + + public function testOrdinal() + { + $this->assertSame('1st', Number::ordinal(1)); + $this->assertSame('2nd', Number::ordinal(2)); + $this->assertSame('3rd', Number::ordinal(3)); + } + public function testToPercent() { $this->needsIntlExtension(); From 4bc11bb07e343faaa502b3b80827460dea170280 Mon Sep 17 00:00:00 2001 From: Joel Male Date: Fri, 17 Nov 2023 08:39:20 -0800 Subject: [PATCH 071/207] [10.x] Fix notifications being counted as sent without a "shouldSend" method (#49030) * [10.x] Fix notifications being counted as sent without a "shouldSend" method * Fix style issue --- src/Illuminate/Support/Testing/Fakes/NotificationFake.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php index 12794711b091..df00c465ff3c 100644 --- a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php +++ b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php @@ -306,10 +306,10 @@ public function sendNow($notifiables, $notification, array $channels = null) $notifiableChannels, fn ($channel) => $notification->shouldSend($notifiable, $channel) !== false ); + } - if (empty($notifiableChannels)) { - continue; - } + if (empty($notifiableChannels)) { + continue; } $this->notifications[get_class($notifiable)][$notifiable->getKey()][get_class($notification)][] = [ From 5fca4afa51c9edd00b7c1ba92802bddbc478c4db Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 19 Nov 2023 01:28:51 +0330 Subject: [PATCH 072/207] [10.x] Fix tests failure on Windows (#49037) * add `intl` PHP extension on Windows * re-run tests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d4ce95e4008c..8c27dfb9f27b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -128,7 +128,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, gd, pdo_mysql, fileinfo, ftp, redis, memcached, gmp + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, gd, pdo_mysql, fileinfo, ftp, redis, memcached, gmp, intl tools: composer:v2 coverage: none From e5f0354b13480a330695fa4ba5264b2e47d01d9c Mon Sep 17 00:00:00 2001 From: Michael Nabil <46572405+michaelnabil230@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:33:39 +0200 Subject: [PATCH 073/207] [10.x] Add unless conditional on validation rules (#49048) * Add unless * Add tests * Formatting * Update Rule.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Validation/Rule.php | 15 ++++++- tests/Validation/ValidationRuleParserTest.php | 42 ++++++++++++------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 7f20be3e6b64..1f8286cc375f 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -34,7 +34,7 @@ public static function can($ability, ...$arguments) } /** - * Create a new conditional rule set. + * Apply the given rules if the given condition is truthy. * * @param callable|bool $condition * @param array|string|\Closure $rules @@ -46,6 +46,19 @@ public static function when($condition, $rules, $defaultRules = []) return new ConditionalRules($condition, $rules, $defaultRules); } + /** + * Apply the given rules if the given condition is falsy. + * + * @param callable|bool $condition + * @param array|string|\Closure $rules + * @param array|string|\Closure $defaultRules + * @return \Illuminate\Validation\ConditionalRules + */ + public static function unless($condition, $rules, $defaultRules = []) + { + return new ConditionalRules(! $condition, $rules, $defaultRules); + } + /** * Create a new nested rule set. * diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php index acf9a4cd926e..2ead8eecbb9d 100644 --- a/tests/Validation/ValidationRuleParserTest.php +++ b/tests/Validation/ValidationRuleParserTest.php @@ -11,19 +11,21 @@ class ValidationRuleParserTest extends TestCase { public function testConditionalRulesAreProperlyExpandedAndFiltered() { + $isAdmin = true; + $rules = ValidationRuleParser::filterConditionalRules([ - 'name' => Rule::when(true, ['required', 'min:2']), - 'email' => Rule::when(false, ['required', 'min:2']), - 'password' => Rule::when(true, 'required|min:2'), - 'username' => ['required', Rule::when(true, ['min:2'])], - 'address' => ['required', Rule::when(false, ['min:2'])], + 'name' => Rule::when($isAdmin, ['required', 'min:2']), + 'email' => Rule::unless($isAdmin, ['required', 'min:2']), + 'password' => Rule::when($isAdmin, 'required|min:2'), + 'username' => ['required', Rule::when($isAdmin, ['min:2'])], + 'address' => ['required', Rule::unless($isAdmin, ['min:2'])], 'city' => ['required', Rule::when(function (Fluent $input) { return true; }, ['min:2'])], - 'state' => ['required', Rule::when(true, function (Fluent $input) { + 'state' => ['required', Rule::when($isAdmin, function (Fluent $input) { return 'min:2'; })], - 'zip' => ['required', Rule::when(false, [], function (Fluent $input) { + 'zip' => ['required', Rule::when($isAdmin, function (Fluent $input) { return ['min:2']; })], ]); @@ -42,16 +44,20 @@ public function testConditionalRulesAreProperlyExpandedAndFiltered() public function testEmptyRulesArePreserved() { + $isAdmin = true; + $rules = ValidationRuleParser::filterConditionalRules([ 'name' => [], 'email' => '', - 'password' => Rule::when(true, 'required|min:2'), + 'password' => Rule::when($isAdmin, 'required|min:2'), + 'gender' => Rule::unless($isAdmin, 'required'), ]); $this->assertEquals([ 'name' => [], 'email' => '', 'password' => ['required', 'min:2'], + 'gender' => [], ], $rules); } @@ -64,12 +70,14 @@ public function testEmptyRulesCanBeExploded() public function testConditionalRulesWithDefault() { + $isAdmin = true; + $rules = ValidationRuleParser::filterConditionalRules([ - 'name' => Rule::when(true, ['required', 'min:2'], ['string', 'max:10']), - 'email' => Rule::when(false, ['required', 'min:2'], ['string', 'max:10']), - 'password' => Rule::when(false, 'required|min:2', 'string|max:10'), - 'username' => ['required', Rule::when(true, ['min:2'], ['string', 'max:10'])], - 'address' => ['required', Rule::when(false, ['min:2'], ['string', 'max:10'])], + 'name' => Rule::when($isAdmin, ['required', 'min:2'], ['string', 'max:10']), + 'email' => Rule::unless($isAdmin, ['required', 'min:2'], ['string', 'max:10']), + 'password' => Rule::unless($isAdmin, 'required|min:2', 'string|max:10'), + 'username' => ['required', Rule::when($isAdmin, ['min:2'], ['string', 'max:10'])], + 'address' => ['required', Rule::unless($isAdmin, ['min:2'], ['string', 'max:10'])], ]); $this->assertEquals([ @@ -83,10 +91,12 @@ public function testConditionalRulesWithDefault() public function testEmptyConditionalRulesArePreserved() { + $isAdmin = true; + $rules = ValidationRuleParser::filterConditionalRules([ - 'name' => Rule::when(true, '', ['string', 'max:10']), - 'email' => Rule::when(false, ['required', 'min:2'], []), - 'password' => Rule::when(false, 'required|min:2', 'string|max:10'), + 'name' => Rule::when($isAdmin, '', ['string', 'max:10']), + 'email' => Rule::unless($isAdmin, ['required', 'min:2']), + 'password' => Rule::unless($isAdmin, 'required|min:2', 'string|max:10'), ]); $this->assertEquals([ From 7af87335260dde9aa01b1f913f18ce43b82080dd Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 21 Nov 2023 02:34:03 +1100 Subject: [PATCH 074/207] Handle string based payloads (#49047) --- src/Illuminate/Routing/RoutingServiceProvider.php | 12 +++++++++--- .../Integration/Foundation/Fixtures/laravel.txt.gz | Bin 0 -> 40 bytes .../Foundation/RoutingServiceProviderTest.php | 12 ++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/Foundation/Fixtures/laravel.txt.gz diff --git a/src/Illuminate/Routing/RoutingServiceProvider.php b/src/Illuminate/Routing/RoutingServiceProvider.php index a2ca675da4e2..547e195e3866 100755 --- a/src/Illuminate/Routing/RoutingServiceProvider.php +++ b/src/Illuminate/Routing/RoutingServiceProvider.php @@ -138,9 +138,15 @@ protected function registerPsrRequest() $psr17Factory = new Psr17Factory; return with((new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory)) - ->createRequest($illuminateRequest = $app->make('request')), fn ($request) => $request->withParsedBody( - array_merge($request->getParsedBody() ?? [], $illuminateRequest->getPayload()->all()) - )); + ->createRequest($illuminateRequest = $app->make('request')), function (ServerRequestInterface $request) use ($illuminateRequest) { + if ($illuminateRequest->getContentTypeFormat() !== 'json' && $illuminateRequest->request->count() === 0) { + return $request; + } + + return $request->withParsedBody( + array_merge($request->getParsedBody() ?? [], $illuminateRequest->getPayload()->all()) + ); + }); } throw new BindingResolutionException('Unable to resolve PSR request. Please install the symfony/psr-http-message-bridge and nyholm/psr7 packages.'); diff --git a/tests/Integration/Foundation/Fixtures/laravel.txt.gz b/tests/Integration/Foundation/Fixtures/laravel.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..636ea645842c9b739c587339b0cd3dc14dbd907b GIT binary patch literal 40 wcmb2|=HM`z7?sMvoRe6TSeBZjS5i^J@Yz$xOUL`{LnemFZF~N5FfcFx00?RgXaE2J literal 0 HcmV?d00001 diff --git a/tests/Integration/Foundation/RoutingServiceProviderTest.php b/tests/Integration/Foundation/RoutingServiceProviderTest.php index 4753f03eda02..5825bbd626eb 100644 --- a/tests/Integration/Foundation/RoutingServiceProviderTest.php +++ b/tests/Integration/Foundation/RoutingServiceProviderTest.php @@ -122,6 +122,18 @@ public function testItIncludesMergedDataInServerRequestInterfaceInstancesUsingPo 'request-data' => 'request-data', ]); } + + public function testItHandlesGzippedBodyPayloadsWhenCreatingServerRequestInterfaceInstances() + { + Route::post('test-route', function (ServerRequestInterface $request) { + return gzdecode((string) $request->getBody()); + }); + + $response = $this->call('POST', 'test-route', content: file_get_contents(__DIR__.'/Fixtures/laravel.txt.gz')); + + $response->assertOk(); + $response->assertContent("Laravel\n"); + } } class MergeDataMiddleware From 61f13544b4a79af929f0886178316e7b7910b1d0 Mon Sep 17 00:00:00 2001 From: Iman Date: Mon, 20 Nov 2023 19:09:38 +0330 Subject: [PATCH 075/207] Fix directory separator CMD display on windows (#49045) --- src/Illuminate/Console/GeneratorCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index 4afcb4306113..f061dc67d384 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -187,6 +187,10 @@ public function handle() } } + if (windows_os()) { + $path = str_replace('/', '\\', $path); + } + $this->components->info(sprintf('%s [%s] created successfully.', $info, $path)); } From 48aa5747c50d42210213772f7bb6d5090585f720 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 21 Nov 2023 02:45:45 +1100 Subject: [PATCH 076/207] [10.x] Fix mapSpread doc (#48941) * Fix mapSpread doc * formatting --- 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 18af3f18954e..b9aee097b123 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -352,7 +352,7 @@ public function isNotEmpty() * * @template TMapSpreadValue * - * @param callable(mixed): TMapSpreadValue $callback + * @param callable(mixed...): TMapSpreadValue $callback * @return static */ public function mapSpread(callable $callback) From 5f103f07cf832dc18566f413250642cfb98e614e Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 20 Nov 2023 13:41:16 -0500 Subject: [PATCH 077/207] Create proper collection instance in test (#49053) --- tests/Support/SupportCollectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 0c8ebb4aacc0..6efeeba1be36 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -2333,7 +2333,7 @@ public function testPluckWithDotNotation($collection) */ public function testPluckDuplicateKeysExist($collection) { - $data = new collection([ + $data = new $collection([ ['brand' => 'Tesla', 'color' => 'red'], ['brand' => 'Pagani', 'color' => 'white'], ['brand' => 'Tesla', 'color' => 'black'], From 85fcda7c132316f886933a43bc53d60b0a52b103 Mon Sep 17 00:00:00 2001 From: Niko Peikrishvili Date: Mon, 20 Nov 2023 23:14:37 +0400 Subject: [PATCH 078/207] [10.x] Feat: Add color_hex validation rule (#49056) * Add color_hex validation rule * fix: styleci * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Translation/lang/en/validation.php | 1 + .../Validation/Concerns/ValidatesAttributes.php | 12 ++++++++++++ tests/Validation/ValidationValidatorTest.php | 13 +++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index bd3eecd7d014..52cc1412858a 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -66,6 +66,7 @@ 'numeric' => 'The :attribute field must be greater than or equal to :value.', 'string' => 'The :attribute field must be greater than or equal to :value characters.', ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', 'image' => 'The :attribute field must be an image.', 'in' => 'The selected :attribute is invalid.', 'in_array' => 'The :attribute field must exist in :other.', diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index df5bb1fc2005..5c04f1a337b5 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1270,6 +1270,18 @@ public function validateUppercase($attribute, $value, $parameters) return Str::upper($value) === $value; } + /** + * Validate that an attribute is a valid HEX color. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function validateHexColor($attribute, $value) + { + return preg_match('/^#(?:[0-9a-fA-F]{3}){1,2}$/', $value) === 1; + } + /** * Validate the MIME type of a file is an image MIME type. * diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 925f463b7da6..0f6acb08dde0 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1925,6 +1925,19 @@ public function testValidateInArray() $this->assertSame('The value of foo.2 does not exist in bar.*.', $v->messages()->first('foo.2')); } + public function testValidateHexColor() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['color'=> '#FFFFFF'], ['color'=>'hex_color']); + $this->assertTrue($v->passes()); + $v = new Validator($trans, ['color'=> '#FFF'], ['color'=>'hex_color']); + $this->assertTrue($v->passes()); + $v = new Validator($trans, ['color'=> '#GGG'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); + $v = new Validator($trans, ['color'=> '#GGGGGGG'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); + } + public function testValidateConfirmed() { $trans = $this->getIlluminateArrayTranslator(); From 0f4fb720b0cccd401c05448e6a9ca60fbe8d7030 Mon Sep 17 00:00:00 2001 From: DeanWunder <30644242+DeanWunder@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:28:58 +1100 Subject: [PATCH 079/207] [10.x] Handle missing translation strings using callback (#49040) * Add the ability to handle missing translation strings with a user-defined callback to facilitate customising missing translation behaviour. Callback can return a string which is subsequently returned by the Translator get method. * Add functionality to avoid infinite loops when the handle missing translation callback itself tries to translation a missing translation. * formatting * formatting * add test * formatting * Update Translator.php --------- Co-authored-by: Dean Wunder Co-authored-by: Taylor Otwell --- src/Illuminate/Translation/Translator.php | 60 +++++++++++++++++++ .../Translation/TranslatorTest.php | 15 +++++ 2 files changed, 75 insertions(+) diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index e29ba93a2dca..41fb2c94922b 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -65,6 +65,20 @@ class Translator extends NamespacedItemResolver implements TranslatorContract */ protected $stringableHandlers = []; + /** + * The callback that is responsible for handling missing translation keys. + * + * @var callable|null + */ + protected $missingTranslationKeyCallback; + + /** + * Indicates whether missing translation keys should be handled. + * + * @var bool + */ + protected $handleMissingTranslationKeys = true; + /** * Create a new translator instance. * @@ -153,6 +167,10 @@ public function get($key, array $replace = [], $locale = null, $fallback = true) return $line; } } + + $key = $this->handleMissingTranslationKey( + $key, $replace, $locale, $fallback + ); } // If the line doesn't exist, we will return back the key which was requested as @@ -308,6 +326,48 @@ protected function isLoaded($namespace, $group, $locale) return isset($this->loaded[$namespace][$group][$locale]); } + /** + * Handle a missing translation key. + * + * @param string $key + * @param array $replace + * @param string|null $locale + * @param bool $fallback + * @return string + */ + protected function handleMissingTranslationKey($key, $replace, $locale, $fallback) + { + if (! $this->handleMissingTranslationKeys || + ! isset($this->missingTranslationKeyCallback)) { + return $key; + } + + // Prevent infinite loops... + $this->handleMissingTranslationKeys = false; + + $key = call_user_func( + $this->missingTranslationKeyCallback, + $key, $replace, $locale, $fallback + ); + + $this->handleMissingTranslationKeys = true; + + return $key; + } + + /** + * Register a callback that is responsible for handling missing translation keys. + * + * @param callable|null $callback + * @return static + */ + public function handleMissingKeysUsing(?callable $callback) + { + $this->missingTranslationKeyCallback = $callback; + + return $this; + } + /** * Add a new namespace to the loader. * diff --git a/tests/Integration/Translation/TranslatorTest.php b/tests/Integration/Translation/TranslatorTest.php index bb84bb5c17f9..4ebf8dcecaae 100644 --- a/tests/Integration/Translation/TranslatorTest.php +++ b/tests/Integration/Translation/TranslatorTest.php @@ -40,4 +40,19 @@ public function testItCanCheckLanguageExistsHasFromLocaleForJson() $this->assertFalse($this->app['translator']->hasForLocale('1 Day')); $this->assertTrue($this->app['translator']->hasForLocale('30 Days')); } + + public function testItCanHandleMissingKeysUsingCallback() + { + $this->app['translator']->handleMissingKeysUsing(function ($key) { + $_SERVER['__missing_translation_key'] = $key; + return 'callback key'; + }); + + $key = $this->app['translator']->get('some missing key'); + + $this->assertSame('callback key', $key); + $this->assertSame('some missing key', $_SERVER['__missing_translation_key']); + + $this->app['translator']->handleMissingKeysUsing(null); + } } From 1441408f06b3c99c03f4c2c45338e3e36e9d8cb3 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 20 Nov 2023 20:29:21 +0000 Subject: [PATCH 080/207] Apply fixes from StyleCI --- tests/Integration/Translation/TranslatorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Integration/Translation/TranslatorTest.php b/tests/Integration/Translation/TranslatorTest.php index 4ebf8dcecaae..11f449dd487d 100644 --- a/tests/Integration/Translation/TranslatorTest.php +++ b/tests/Integration/Translation/TranslatorTest.php @@ -45,6 +45,7 @@ public function testItCanHandleMissingKeysUsingCallback() { $this->app['translator']->handleMissingKeysUsing(function ($key) { $_SERVER['__missing_translation_key'] = $key; + return 'callback key'; }); From c3844dcb9e2b4d9ba8e2e20f063a49b4c4fee9a6 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Mon, 20 Nov 2023 20:29:51 +0000 Subject: [PATCH 081/207] Update facade docblocks --- src/Illuminate/Support/Facades/Lang.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Lang.php b/src/Illuminate/Support/Facades/Lang.php index 992282cb2b0f..cdaad3d0fd16 100755 --- a/src/Illuminate/Support/Facades/Lang.php +++ b/src/Illuminate/Support/Facades/Lang.php @@ -9,6 +9,7 @@ * @method static string choice(string $key, \Countable|int|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) + * @method static \Illuminate\Translation\Translator handleMissingKeysUsing(callable|null $callback) * @method static void addNamespace(string $namespace, string $hint) * @method static void addJsonPath(string $path) * @method static array parseKey(string $key) From 276d6eb38f9f164b3c4d2768f467c3505fff23a8 Mon Sep 17 00:00:00 2001 From: Dwight Watson Date: Wed, 22 Nov 2023 01:39:42 +1100 Subject: [PATCH 082/207] [10.x] Add Str::transliterate to Stringable (#49065) * Add Str::transliterate to Stringable * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Support/Stringable.php | 12 ++++++++++++ tests/Support/SupportStringableTest.php | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index 7d3558c0fdc9..018416d9ac73 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -797,6 +797,18 @@ public function title() return new static(Str::title($this->value)); } + /** + * Transliterate a string to its closest ASCII representation. + * + * @param string|null $unknown + * @param bool|null $strict + * @return static + */ + public function transliterate($unknown = '?', $strict = false) + { + return new static(Str::transliterate($this->value, $unknown, $strict)); + } + /** * Convert the given string to title case for each word. * diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 39506394a044..5cf433fa4973 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -527,6 +527,12 @@ public function testAscii() $this->assertSame('u', (string) $this->stringable('ü')->ascii()); } + public function testTransliterate() + { + $this->assertSame('HHH', (string) $this->stringable('🎂🚧🏆')->transliterate('H')); + $this->assertSame('Hello', (string) $this->stringable('🎂')->transliterate('Hello')); + } + public function testNewLine() { $this->assertSame('Laravel'.PHP_EOL, (string) $this->stringable('Laravel')->newLine()); From 6a68969a0d21ffee0e17162de5489b3911310744 Mon Sep 17 00:00:00 2001 From: Andy Hinkle Date: Tue, 21 Nov 2023 08:40:13 -0600 Subject: [PATCH 083/207] Add Alpha Channel support to Hex validation rule (#49069) * Support Alpha hexadecimal * Add more supporting tests --- .../Concerns/ValidatesAttributes.php | 2 +- tests/Validation/ValidationValidatorTest.php | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 5c04f1a337b5..ea62107bcd0c 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1279,7 +1279,7 @@ public function validateUppercase($attribute, $value, $parameters) */ public function validateHexColor($attribute, $value) { - return preg_match('/^#(?:[0-9a-fA-F]{3}){1,2}$/', $value) === 1; + return preg_match('/^#(?:[0-9a-fA-F]{3}){1,2}(?:[0-9a-fA-F]{2})?$|^#(?:[0-9a-fA-F]{4}){1,2}$/', $value) === 1; } /** diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 0f6acb08dde0..5146996a7d22 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1928,14 +1928,30 @@ public function testValidateInArray() public function testValidateHexColor() { $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['color'=> '#FFF'], ['color'=>'hex_color']); + $this->assertTrue($v->passes()); + $v = new Validator($trans, ['color'=> '#FFFF'], ['color'=>'hex_color']); + $this->assertTrue($v->passes()); $v = new Validator($trans, ['color'=> '#FFFFFF'], ['color'=>'hex_color']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['color'=> '#FFF'], ['color'=>'hex_color']); + $v = new Validator($trans, ['color'=> '#FF000080'], ['color'=>'hex_color']); + $this->assertTrue($v->passes()); + $v = new Validator($trans, ['color'=> '#FF000080'], ['color'=>'hex_color']); + $this->assertTrue($v->passes()); + $v = new Validator($trans, ['color'=> '#00FF0080'], ['color'=>'hex_color']); $this->assertTrue($v->passes()); $v = new Validator($trans, ['color'=> '#GGG'], ['color'=>'hex_color']); $this->assertFalse($v->passes()); + $v = new Validator($trans, ['color'=> '#GGGG'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); + $v = new Validator($trans, ['color'=> '#GGGGGG'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); $v = new Validator($trans, ['color'=> '#GGGGGGG'], ['color'=>'hex_color']); $this->assertFalse($v->passes()); + $v = new Validator($trans, ['color'=> '#FFGG00FF'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); + $v = new Validator($trans, ['color'=> '#00FF008X'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); } public function testValidateConfirmed() From 4536872e3e5b6be51b1f655dafd12c9a4fa0cfe8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 21 Nov 2023 08:49:31 -0600 Subject: [PATCH 084/207] version --- 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 bf1b10d4c0a3..1a87faace3f2 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.32.1'; + const VERSION = '10.33.0'; /** * The base path for the Laravel installation. From 84c3b9eef2083bda9fcfb7d561fe8859385c20a7 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 21 Nov 2023 14:57:17 +0000 Subject: [PATCH 085/207] Update CHANGELOG --- CHANGELOG.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2cc6f052a1d..e9fe5be197a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.32.1...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.33.0...10.x) + +## [v10.33.0](https://github.com/laravel/framework/compare/v10.32.1...v10.33.0) - 2023-11-21 + +- [10.x] Fix wrong parameter passing and add these rules to dependent rules by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/49008 +- [10.x] Make Validator::getValue() public by [@shinsenter](https://github.com/shinsenter) in https://github.com/laravel/framework/pull/49007 +- [10.x] Custom messages for `Password` validation rule by [@rcknr](https://github.com/rcknr) in https://github.com/laravel/framework/pull/48928 +- [10.x] Round milliseconds in database seeder console output runtime by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/49014 +- [10.x] Add a `Number` utility class by [@caendesilva](https://github.com/caendesilva) in https://github.com/laravel/framework/pull/48845 +- [10.x] Fix the replace() method in DefaultService class by [@jonagoldman](https://github.com/jonagoldman) in https://github.com/laravel/framework/pull/49022 +- [10.x] Pass the property $validator as a parameter to the $callback Closure by [@shinsenter](https://github.com/shinsenter) in https://github.com/laravel/framework/pull/49015 +- [10.x] Fix Cache DatabaseStore::add() error occur on Postgres within transaction by [@xdevor](https://github.com/xdevor) in https://github.com/laravel/framework/pull/49025 +- [10.x] Support asserting against chained batches by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/49003 +- [10.x] Prevent DB `Cache::get()` occur race condition by [@xdevor](https://github.com/xdevor) in https://github.com/laravel/framework/pull/49031 +- [10.x] Fix notifications being counted as sent without a "shouldSend" method by [@joelwmale](https://github.com/joelwmale) in https://github.com/laravel/framework/pull/49030 +- [10.x] Fix tests failure on Windows by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49037 +- [10.x] Add unless conditional on validation rules by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/49048 +- [10.x] Handle string based payloads that are not JSON or form data when creating PSR request instances by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/49047 +- [10.x] Fix directory separator CMD display on windows by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/49045 +- [10.x] Fix mapSpread doc by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/48941 +- [10.x] Tiny `Support\Collection` test fix - Unused data provider parameter by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/49053 +- [10.x] Feat: Add color_hex validation rule by [@nikopeikrishvili](https://github.com/nikopeikrishvili) in https://github.com/laravel/framework/pull/49056 +- [10.x] Handle missing translation strings using callback by [@DeanWunder](https://github.com/DeanWunder) in https://github.com/laravel/framework/pull/49040 +- [10.x] Add Str::transliterate to Stringable by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/49065 +- Add Alpha Channel support to Hex validation rule by [@ahinkle](https://github.com/ahinkle) in https://github.com/laravel/framework/pull/49069 ## [v10.32.1](https://github.com/laravel/framework/compare/v10.32.0...v10.32.1) - 2023-11-14 From 99d73951168202349231f1f926c7d3a0e873d019 Mon Sep 17 00:00:00 2001 From: Mohd Hafizuddin M Marzuki Date: Wed, 22 Nov 2023 00:08:58 +0800 Subject: [PATCH 086/207] Fix `hex_color` rule (#49070) --- src/Illuminate/Validation/Concerns/ValidatesAttributes.php | 2 +- tests/Validation/ValidationValidatorTest.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index ea62107bcd0c..0f1804912612 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1279,7 +1279,7 @@ public function validateUppercase($attribute, $value, $parameters) */ public function validateHexColor($attribute, $value) { - return preg_match('/^#(?:[0-9a-fA-F]{3}){1,2}(?:[0-9a-fA-F]{2})?$|^#(?:[0-9a-fA-F]{4}){1,2}$/', $value) === 1; + return preg_match('/^#(?:(?:[0-9a-f]{3}){1,2}|(?:[0-9a-f]{4}){1,2})$/i', $value) === 1; } /** diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 5146996a7d22..93992089c37b 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1944,6 +1944,8 @@ public function testValidateHexColor() $this->assertFalse($v->passes()); $v = new Validator($trans, ['color'=> '#GGGG'], ['color'=>'hex_color']); $this->assertFalse($v->passes()); + $v = new Validator($trans, ['color'=> '#123AB'], ['color'=>'hex_color']); + $this->assertFalse($v->passes()); $v = new Validator($trans, ['color'=> '#GGGGGG'], ['color'=>'hex_color']); $this->assertFalse($v->passes()); $v = new Validator($trans, ['color'=> '#GGGGGGG'], ['color'=>'hex_color']); From 742f83518e6c427c32b74996b3bc6b789c7128d6 Mon Sep 17 00:00:00 2001 From: Rob <30407321+robtesch@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:21:31 +0000 Subject: [PATCH 087/207] [10.x] Prevent passing null to base64_decode in Encrypter (#49071) * Prevent passing null to base64_decode in Encrypter * Implement styleCI recommended changes * Update Encrypter.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Encryption/Encrypter.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 5a8d82ec9ec6..8a8c6d85b0fc 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -205,6 +205,10 @@ protected function hash($iv, $value) */ protected function getJsonPayload($payload) { + if (! is_string($payload)) { + throw new DecryptException('The payload is invalid.'); + } + $payload = json_decode(base64_decode($payload), true); // If the payload is not valid JSON or does not have the proper keys set we will From 2967d89906708cc7d619fc130e835c8002b7d3e3 Mon Sep 17 00:00:00 2001 From: Jamie York Date: Tue, 21 Nov 2023 19:09:37 +0000 Subject: [PATCH 088/207] [10.x] Alias Number class (#49073) * alias number class * alphabetical --- src/Illuminate/Support/Facades/Facade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Support/Facades/Facade.php b/src/Illuminate/Support/Facades/Facade.php index 80c58bba7b5b..1dbdc321543b 100755 --- a/src/Illuminate/Support/Facades/Facade.php +++ b/src/Illuminate/Support/Facades/Facade.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Js; +use Illuminate\Support\Number; use Illuminate\Support\Str; use Illuminate\Support\Testing\Fakes\Fake; use Mockery; @@ -293,6 +294,7 @@ public static function defaultAliases() 'Log' => Log::class, 'Mail' => Mail::class, 'Notification' => Notification::class, + 'Number' => Number::class, 'Password' => Password::class, 'Process' => Process::class, 'Queue' => Queue::class, From 4ae1ef68e4e443499dceaf6bb3605863df8e8b4e Mon Sep 17 00:00:00 2001 From: Lito Date: Wed, 22 Nov 2023 16:44:28 +0100 Subject: [PATCH 089/207] [10.x] Added File Validation `extensions` (#49082) * [10.x] Added Extension Validation * Updated Tests and Comments * formatting --------- Co-authored-by: Taylor Otwell --- .../Translation/lang/en/validation.php | 1 + .../Concerns/ReplacesAttributes.php | 14 ++++++ .../Concerns/ValidatesAttributes.php | 21 +++++++++ src/Illuminate/Validation/Rules/File.php | 25 ++++++++++ src/Illuminate/Validation/Validator.php | 1 + tests/Validation/ValidationFileRuleTest.php | 46 +++++++++++++++++++ tests/Validation/ValidationValidatorTest.php | 32 +++++++++++++ 7 files changed, 140 insertions(+) diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index 52cc1412858a..8dbe37f15ae9 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -52,6 +52,7 @@ 'ends_with' => 'The :attribute field must end with one of the following: :values.', 'enum' => 'The selected :attribute is invalid.', 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', 'file' => 'The :attribute field must be a file.', 'filled' => 'The :attribute field must have a value.', 'gt' => [ diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index 23efc2303115..eacbcc67889b 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -132,6 +132,20 @@ protected function replaceDigitsBetween($message, $attribute, $rule, $parameters return $this->replaceBetween($message, $attribute, $rule, $parameters); } + /** + * Replace all place-holders for the extensions rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceExtensions($message, $attribute, $rule, $parameters) + { + return str_replace(':values', implode(', ', $parameters), $message); + } + /** * Replace all place-holders for the min rule. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 0f1804912612..ce0ce658072c 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1076,6 +1076,27 @@ protected function getExtraConditions(array $segments) return $extra; } + /** + * Validate the extension of a file upload attribute is in a set of defined extensions. + * + * @param string $attribute + * @param mixed $value + * @param array $parameters + * @return bool + */ + public function validateExtensions($attribute, $value, $parameters) + { + if (! $this->isValidFileInstance($value)) { + return false; + } + + if ($this->shouldBlockPhpUpload($value, $parameters)) { + return false; + } + + return in_array(strtolower($value->getClientOriginalExtension()), $parameters); + } + /** * Validate the given value is a valid file. * diff --git a/src/Illuminate/Validation/Rules/File.php b/src/Illuminate/Validation/Rules/File.php index 38448b963c13..f7a881770c78 100644 --- a/src/Illuminate/Validation/Rules/File.php +++ b/src/Illuminate/Validation/Rules/File.php @@ -24,6 +24,13 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule */ protected $allowedMimetypes = []; + /** + * The extensions that the given file should match. + * + * @var array + */ + protected $allowedExtensions = []; + /** * The minimum size in kilobytes that the file can be. * @@ -129,6 +136,20 @@ public static function types($mimetypes) return tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes); } + + /** + * Limit the uploaded file to the given file extensions. + * + * @param string|array $extensions + * @return $this + */ + public function extensions($extensions) + { + $this->allowedExtensions = (array) $extensions; + + return $this; + } + /** * Indicate that the uploaded file should be exactly a certain size in kilobytes. * @@ -256,6 +277,10 @@ protected function buildValidationRules() $rules = array_merge($rules, $this->buildMimetypes()); + if (! empty($this->allowedExtensions)) { + $rules[] = 'extensions:'.implode(',', array_map('strtolower', $this->allowedExtensions)); + } + $rules[] = match (true) { is_null($this->minimumFileSize) && is_null($this->maximumFileSize) => null, is_null($this->maximumFileSize) => "min:{$this->minimumFileSize}", diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 5a6844c3c758..7bcdc97b8212 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -187,6 +187,7 @@ class Validator implements ValidatorContract protected $fileRules = [ 'Between', 'Dimensions', + 'Extensions', 'File', 'Image', 'Max', diff --git a/tests/Validation/ValidationFileRuleTest.php b/tests/Validation/ValidationFileRuleTest.php index 6ffa71e65bd9..838aa6615471 100644 --- a/tests/Validation/ValidationFileRuleTest.php +++ b/tests/Validation/ValidationFileRuleTest.php @@ -134,6 +134,52 @@ public function testMixOfMimetypesAndMimes() ); } + public function testSingleExtension() + { + $this->fails( + File::default()->extensions('png'), + UploadedFile::fake()->createWithContent('foo', file_get_contents(__DIR__.'/fixtures/image.png')), + ['validation.extensions'] + ); + + $this->fails( + File::default()->extensions('png'), + UploadedFile::fake()->createWithContent('foo.jpg', file_get_contents(__DIR__.'/fixtures/image.png')), + ['validation.extensions'] + ); + + $this->fails( + File::default()->extensions('jpeg'), + UploadedFile::fake()->createWithContent('foo.jpg', file_get_contents(__DIR__.'/fixtures/image.png')), + ['validation.extensions'] + ); + + $this->passes( + File::default()->extensions('png'), + UploadedFile::fake()->createWithContent('foo.png', file_get_contents(__DIR__.'/fixtures/image.png')), + ); + } + + public function testMultipleExtensions() + { + $this->fails( + File::default()->extensions(['png', 'jpeg', 'jpg']), + UploadedFile::fake()->createWithContent('foo', file_get_contents(__DIR__.'/fixtures/image.png')), + ['validation.extensions'] + ); + + $this->fails( + File::default()->extensions(['png', 'jpeg']), + UploadedFile::fake()->createWithContent('foo.jpg', file_get_contents(__DIR__.'/fixtures/image.png')), + ['validation.extensions'] + ); + + $this->passes( + File::default()->extensions(['png', 'jpeg', 'jpg']), + UploadedFile::fake()->createWithContent('foo.png', file_get_contents(__DIR__.'/fixtures/image.png')), + ); + } + public function testImage() { $this->fails( diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 93992089c37b..cc57af1f96d0 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -4697,6 +4697,38 @@ public function testValidateMime() $this->assertTrue($v->passes()); } + public function testValidateExtension() + { + $trans = $this->getIlluminateArrayTranslator(); + $uploadedFile = [__FILE__, '', null, null, true]; + + $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); + $file->expects($this->any())->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'); + $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'); + $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'); + $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'); + $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpg|extensions:jpg']); + $this->assertFalse($v->passes()); + } + public function testValidateMimeEnforcesPhpCheck() { $trans = $this->getIlluminateArrayTranslator(); From 2d558702b3afb1d8f54efcf431a92af9bc7b5e1c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 22 Nov 2023 15:45:06 +0000 Subject: [PATCH 090/207] Apply fixes from StyleCI --- src/Illuminate/Validation/Rules/File.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/File.php b/src/Illuminate/Validation/Rules/File.php index f7a881770c78..fb9a0603c2d6 100644 --- a/src/Illuminate/Validation/Rules/File.php +++ b/src/Illuminate/Validation/Rules/File.php @@ -136,7 +136,6 @@ public static function types($mimetypes) return tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes); } - /** * Limit the uploaded file to the given file extensions. * From 86a5976d1e1f35bebe6264858f6ebef6c5d7846a Mon Sep 17 00:00:00 2001 From: Iman Date: Wed, 22 Nov 2023 23:39:43 +0330 Subject: [PATCH 091/207] Add @throws in doc-blocks (#49091) --- src/Illuminate/Process/FakeProcessResult.php | 4 ++++ src/Illuminate/Process/InvokedProcess.php | 2 ++ src/Illuminate/Process/PendingProcess.php | 3 +++ src/Illuminate/Process/ProcessResult.php | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/src/Illuminate/Process/FakeProcessResult.php b/src/Illuminate/Process/FakeProcessResult.php index f15fc034b26b..73efc697f14e 100644 --- a/src/Illuminate/Process/FakeProcessResult.php +++ b/src/Illuminate/Process/FakeProcessResult.php @@ -172,6 +172,8 @@ public function seeInErrorOutput(string $output) * * @param callable|null $callback * @return $this + * + * @throws \Illuminate\Process\Exceptions\ProcessFailedException */ public function throw(callable $callback = null) { @@ -194,6 +196,8 @@ public function throw(callable $callback = null) * @param bool $condition * @param callable|null $callback * @return $this + * + * @throws \Throwable */ public function throwIf(bool $condition, callable $callback = null) { diff --git a/src/Illuminate/Process/InvokedProcess.php b/src/Illuminate/Process/InvokedProcess.php index 21a0368fb815..f0d3feb7bfa3 100644 --- a/src/Illuminate/Process/InvokedProcess.php +++ b/src/Illuminate/Process/InvokedProcess.php @@ -105,6 +105,8 @@ public function latestErrorOutput() * * @param callable|null $output * @return \Illuminate\Process\ProcessResult + * + * @throws \Illuminate\Process\Exceptions\ProcessTimedOutException */ public function wait(callable $output = null) { diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 22475c94c0ed..320172bd266c 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -237,6 +237,9 @@ public function options(array $options) * @param array|string|null $command * @param callable|null $output * @return \Illuminate\Contracts\Process\ProcessResult + * + * @throws \Illuminate\Process\Exceptions\ProcessTimedOutException + * @throws \RuntimeException */ public function run(array|string $command = null, callable $output = null) { diff --git a/src/Illuminate/Process/ProcessResult.php b/src/Illuminate/Process/ProcessResult.php index 29d42df4ce4e..2d17d822afa5 100644 --- a/src/Illuminate/Process/ProcessResult.php +++ b/src/Illuminate/Process/ProcessResult.php @@ -113,6 +113,8 @@ public function seeInErrorOutput(string $output) * * @param callable|null $callback * @return $this + * + * @throws \Illuminate\Process\Exceptions\ProcessFailedException */ public function throw(callable $callback = null) { @@ -135,6 +137,8 @@ public function throw(callable $callback = null) * @param bool $condition * @param callable|null $callback * @return $this + * + * @throws \Throwable */ public function throwIf(bool $condition, callable $callback = null) { From 60f753f68cdedc223ffae4c46be58fc2f0a03e13 Mon Sep 17 00:00:00 2001 From: Dwight Watson Date: Thu, 23 Nov 2023 07:47:06 +1100 Subject: [PATCH 092/207] Update docblocks for consistency (#49092) --- src/Illuminate/Support/Number.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 6053f0a66f6e..47d218ffcd47 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -23,7 +23,7 @@ class Number * @param int|float $number * @param int|null $precision * @param int|null $maxPrecision - * @param ?string $locale + * @param string|null $locale * @return string|false */ public static function format(int|float $number, ?int $precision = null, ?int $maxPrecision = null, ?string $locale = null) @@ -45,7 +45,7 @@ public static function format(int|float $number, ?int $precision = null, ?int $m * Spell out the given number in the given locale. * * @param int|float $number - * @param ?string $locale + * @param string|null $locale * @return string */ public static function spell(int|float $number, ?string $locale = null) @@ -61,7 +61,7 @@ public static function spell(int|float $number, ?string $locale = null) * Convert the given number to ordinal form. * * @param int|float $number - * @param ?string $locale + * @param string|null $locale * @return string */ public static function ordinal(int|float $number, ?string $locale = null) @@ -79,7 +79,7 @@ public static function ordinal(int|float $number, ?string $locale = null) * @param int|float $number * @param int $precision * @param int|null $maxPrecision - * @param ?string $locale + * @param string|null $locale * @return string|false */ public static function percentage(int|float $number, int $precision = 0, ?int $maxPrecision = null, ?string $locale = null) @@ -102,7 +102,7 @@ public static function percentage(int|float $number, int $precision = 0, ?int $m * * @param int|float $number * @param string $in - * @param ?string $locale + * @param string|null $locale * @return string|false */ public static function currency(int|float $number, string $in = 'USD', ?string $locale = null) From b99d035026437a7a561d2d415dd90881703ceea0 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 23 Nov 2023 22:47:13 +0800 Subject: [PATCH 093/207] [10.x] Throw exception when trying to initiate `Collection` using `WeakMap` (#49095) * [10.x] Throw exception when trying to initiate `Collection` using `WeakMap` fixes laravel/framework#49089 Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Update SupportCollectionTest.php * Update EnumeratesValues.php --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Collections/Traits/EnumeratesValues.php | 3 +++ tests/Support/SupportCollectionTest.php | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index b9aee097b123..b42edb49d808 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -11,11 +11,13 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\HigherOrderCollectionProxy; +use InvalidArgumentException; use JsonSerializable; use Symfony\Component\VarDumper\VarDumper; use Traversable; use UnexpectedValueException; use UnitEnum; +use WeakMap; /** * @template TKey of array-key @@ -1019,6 +1021,7 @@ protected function getArrayableItems($items) } return match (true) { + $items instanceof WeakMap => throw new InvalidArgumentException('Collections can not be created using instances of WeakMap.'), $items instanceof Enumerable => $items->all(), $items instanceof Arrayable => $items->toArray(), $items instanceof Traversable => iterator_to_array($items), diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 6efeeba1be36..55fb1359d19c 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -19,12 +19,14 @@ use IteratorAggregate; use JsonSerializable; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; use Symfony\Component\VarDumper\VarDumper; use Traversable; use UnexpectedValueException; +use WeakMap; include_once 'Enums.php'; @@ -2957,6 +2959,19 @@ public function testConstructMethodFromObject($collection) $this->assertEquals(['foo' => 'bar'], $data->all()); } + #[DataProvider('collectionClassProvider')] + public function testConstructMethodFromWeakMap($collection) + { + $this->expectException('InvalidArgumentException'); + + $map = new WeakMap(); + $object = new stdClass; + $object->foo = 'bar'; + $map[$object] = 3; + + $data = new $collection($map); + } + public function testSplice() { $data = new Collection(['foo', 'baz']); From 2d2d82b338f23da57d379542ee6056fbf5f69e25 Mon Sep 17 00:00:00 2001 From: Martin Hansen Date: Thu, 23 Nov 2023 16:30:10 +0100 Subject: [PATCH 094/207] [10.x] Only stage committed transactions (#49093) * Only stage committed transactions Updates the TransactionManager's stageTransactions method to accept a $level parameter, allowing it to only stage the transactions that will actually be committed. The previous implementation staged all existing transactions, which caused the manager's pendingTransactions and committedTransactions properties to be incorrect, leading to unexpected behaviour after a child transaction was committed but before the parent transaction had been committed. * Fix styleci * push failing test * fix failing test * add another failing test * rename test * fix formatting * add another test * rename test * rename test * refactor to parents * small fix * check connection * key current transactions by connection * another fix * call values * formatting * add another test * comment --------- Co-authored-by: Taylor Otwell --- .../Database/Concerns/ManagesTransactions.php | 4 +- .../Database/DatabaseTransactionRecord.php | 11 +- .../Database/DatabaseTransactionsManager.php | 125 +++++++++++++--- .../DatabaseTransactionsManagerTest.php | 25 +++- tests/Database/DatabaseTransactionsTest.php | 12 +- .../DatabaseTransactionsManagerTest.php | 2 +- .../ShouldDispatchAfterCommitEventTest.php | 133 ++++++++++++++++++ 7 files changed, 277 insertions(+), 35 deletions(-) diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 0f6aa0239a71..32bd999b1726 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -47,7 +47,7 @@ public function transaction(Closure $callback, $attempts = 1) $this->getPdo()->commit(); } - $this->transactionsManager?->stageTransactions($this->getName()); + $this->transactionsManager?->stageTransactions($this->getName(), $this->transactions); $this->transactions = max(0, $this->transactions - 1); @@ -196,7 +196,7 @@ public function commit() $this->getPdo()->commit(); } - $this->transactionsManager?->stageTransactions($this->getName()); + $this->transactionsManager?->stageTransactions($this->getName(), $this->transactions); $this->transactions = max(0, $this->transactions - 1); diff --git a/src/Illuminate/Database/DatabaseTransactionRecord.php b/src/Illuminate/Database/DatabaseTransactionRecord.php index 4736ee9224d2..c35acb184aa5 100755 --- a/src/Illuminate/Database/DatabaseTransactionRecord.php +++ b/src/Illuminate/Database/DatabaseTransactionRecord.php @@ -18,6 +18,13 @@ class DatabaseTransactionRecord */ public $level; + /** + * The parent instance of this transaction. + * + * @var \Illuminate\Database\DatabaseTransactionRecord + */ + public $parent; + /** * The callbacks that should be executed after committing. * @@ -30,12 +37,14 @@ class DatabaseTransactionRecord * * @param string $connection * @param int $level + * @param \Illuminate\Database\DatabaseTransactionRecord|null $parent * @return void */ - public function __construct($connection, $level) + public function __construct($connection, $level, ?DatabaseTransactionRecord $parent = null) { $this->connection = $connection; $this->level = $level; + $this->parent = $parent; } /** diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index 2914464858e6..a8e634e42ddf 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -20,6 +20,13 @@ class DatabaseTransactionsManager */ protected $pendingTransactions; + /** + * The current transaction. + * + * @var array + */ + protected $currentTransaction = []; + /** * Create a new database transactions manager instance. * @@ -41,32 +48,57 @@ public function __construct() public function begin($connection, $level) { $this->pendingTransactions->push( - new DatabaseTransactionRecord($connection, $level) + $newTransaction = new DatabaseTransactionRecord( + $connection, + $level, + $this->currentTransaction[$connection] ?? null + ) ); + + $this->currentTransaction[$connection] = $newTransaction; } /** - * Rollback the active database transaction. + * Move relevant pending transactions to a committed state. * * @param string $connection - * @param int $level + * @param int $levelBeingCommitted * @return void */ - public function rollback($connection, $level) + public function stageTransactions($connection, $levelBeingCommitted) { + $this->committedTransactions = $this->committedTransactions->merge( + $this->pendingTransactions->filter( + fn ($transaction) => $transaction->connection === $connection && + $transaction->level >= $levelBeingCommitted + ) + ); + $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection == $connection && $transaction->level > $level - )->values(); + fn ($transaction) => $transaction->connection === $connection && + $transaction->level >= $levelBeingCommitted + ); + + if (isset($this->currentTransaction[$connection])) { + $this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent; + } } /** - * Commit the active database transaction. + * Commit the root database transaction and execute callbacks. * * @param string $connection * @return void */ public function commit($connection) { + // This method is only called when the root database transaction is committed so there + // shouldn't be any pending transactions, but going to clear them here anyways just + // in case. This method could be refactored to receive a level in the future too. + $this->pendingTransactions = $this->pendingTransactions->reject( + fn ($transaction) => $transaction->connection === $connection + )->values(); + [$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition( fn ($transaction) => $transaction->connection == $connection ); @@ -77,35 +109,88 @@ public function commit($connection) } /** - * Register a transaction callback. + * Rollback the active database transaction. * - * @param callable $callback + * @param string $connection + * @param int $newTransactionLevel * @return void */ - public function addCallback($callback) + public function rollback($connection, $newTransactionLevel) { - if ($current = $this->callbackApplicableTransactions()->last()) { - return $current->addCallback($callback); + if ($newTransactionLevel === 0) { + $this->removeAllTransactionsForConnection($connection); + } else { + $this->pendingTransactions = $this->pendingTransactions->reject( + fn ($transaction) => $transaction->connection == $connection && + $transaction->level > $newTransactionLevel + )->values(); + + if ($this->currentTransaction) { + do { + $this->removeCommittedTransactionsThatAreChildrenOf($this->currentTransaction[$connection]); + + $this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent; + } while ( + isset($this->currentTransaction[$connection]) && + $this->currentTransaction[$connection]->level > $newTransactionLevel + ); + } } - - $callback(); } /** - * Move all the pending transactions to a committed state. + * Remove all pending, completed, and current transactions for the given connection name. * * @param string $connection * @return void */ - public function stageTransactions($connection) + protected function removeAllTransactionsForConnection($connection) { - $this->committedTransactions = $this->committedTransactions->merge( - $this->pendingTransactions->filter(fn ($transaction) => $transaction->connection === $connection) - ); + $this->currentTransaction[$connection] = null; $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection === $connection + fn ($transaction) => $transaction->connection == $connection + )->values(); + + $this->committedTransactions = $this->committedTransactions->reject( + fn ($transaction) => $transaction->connection == $connection + )->values(); + } + + /** + * Remove all transactions that are children of the given transaction. + * + * @param \Illuminate\Database\DatabaseTransactionRecord $transaction + * @return void + */ + protected function removeCommittedTransactionsThatAreChildrenOf(DatabaseTransactionRecord $transaction) + { + [$removedTransactions, $this->committedTransactions] = $this->committedTransactions->partition( + fn ($committed) => $committed->connection == $transaction->connection && + $committed->parent === $transaction ); + + // There may be multiple deeply nested transactions that have already committed that we + // also need to remove. We will recurse down the children of all removed transaction + // instances until there are no more deeply nested child transactions for removal. + $removedTransactions->each( + fn ($transaction) => $this->removeCommittedTransactionsThatAreChildrenOf($transaction) + ); + } + + /** + * Register a transaction callback. + * + * @param callable $callback + * @return void + */ + public function addCallback($callback) + { + if ($current = $this->callbackApplicableTransactions()->last()) { + return $current->addCallback($callback); + } + + $callback(); } /** diff --git a/tests/Database/DatabaseTransactionsManagerTest.php b/tests/Database/DatabaseTransactionsManagerTest.php index e303b82d89cd..a62c03dfb5ef 100755 --- a/tests/Database/DatabaseTransactionsManagerTest.php +++ b/tests/Database/DatabaseTransactionsManagerTest.php @@ -67,8 +67,8 @@ public function testCommittingTransactions() $manager->begin('default', 2); $manager->begin('admin', 1); - $manager->stageTransactions('default'); - $manager->stageTransactions('admin'); + $manager->stageTransactions('default', 1); + $manager->stageTransactions('admin', 1); $manager->commit('default'); $this->assertCount(0, $manager->getPendingTransactions()); @@ -121,7 +121,7 @@ public function testCommittingTransactionsExecutesCallbacks() $manager->begin('admin', 1); - $manager->stageTransactions('default'); + $manager->stageTransactions('default', 1); $manager->commit('default'); $this->assertCount(2, $callbacks); @@ -148,7 +148,7 @@ public function testCommittingExecutesOnlyCallbacksOfTheConnection() $callbacks[] = ['admin', 1]; }); - $manager->stageTransactions('default'); + $manager->stageTransactions('default', 1); $manager->commit('default'); $this->assertCount(1, $callbacks); @@ -185,16 +185,29 @@ public function testStageTransactions() $this->assertEquals(1, $pendingTransactions[1]->level); $this->assertEquals('admin', $pendingTransactions[1]->connection); - $manager->stageTransactions('default'); + $manager->stageTransactions('default', 1); $this->assertCount(1, $manager->getPendingTransactions()); $this->assertCount(1, $manager->getCommittedTransactions()); $this->assertEquals('default', $manager->getCommittedTransactions()[0]->connection); - $manager->stageTransactions('admin'); + $manager->stageTransactions('admin', 1); $this->assertCount(0, $manager->getPendingTransactions()); $this->assertCount(2, $manager->getCommittedTransactions()); $this->assertEquals('admin', $manager->getCommittedTransactions()[1]->connection); } + + public function testStageTransactionsOnlyStagesTheTransactionsAtOrAboveTheGivenLevel() + { + $manager = (new DatabaseTransactionsManager); + + $manager->begin('default', 1); + $manager->begin('default', 2); + $manager->begin('default', 3); + $manager->stageTransactions('default', 2); + + $this->assertCount(1, $manager->getPendingTransactions()); + $this->assertCount(2, $manager->getCommittedTransactions()); + } } diff --git a/tests/Database/DatabaseTransactionsTest.php b/tests/Database/DatabaseTransactionsTest.php index 94998f010c01..384f4223fa81 100644 --- a/tests/Database/DatabaseTransactionsTest.php +++ b/tests/Database/DatabaseTransactionsTest.php @@ -64,7 +64,7 @@ public function testTransactionIsRecordedAndCommitted() { $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default'); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); $transactionManager->shouldReceive('commit')->once()->with('default'); $this->connection()->setTransactionManager($transactionManager); @@ -84,7 +84,7 @@ public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods() { $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default'); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); $transactionManager->shouldReceive('commit')->once()->with('default'); $this->connection()->setTransactionManager($transactionManager); @@ -105,7 +105,8 @@ public function testNestedTransactionIsRecordedAndCommitted() $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('default', 2); - $transactionManager->shouldReceive('stageTransactions')->twice()->with('default'); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 2); $transactionManager->shouldReceive('commit')->once()->with('default'); $this->connection()->setTransactionManager($transactionManager); @@ -133,8 +134,9 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 2); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default'); - $transactionManager->shouldReceive('stageTransactions')->twice()->with('second_connection'); + $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); + $transactionManager->shouldReceive('stageTransactions')->once()->with('second_connection', 1); + $transactionManager->shouldReceive('stageTransactions')->once()->with('second_connection', 2); $transactionManager->shouldReceive('commit')->once()->with('default'); $transactionManager->shouldReceive('commit')->once()->with('second_connection'); diff --git a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php index b0add2e650eb..f5655ebc3862 100644 --- a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php +++ b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php @@ -42,7 +42,7 @@ public function testItExecutesCallbacksForTheSecondTransaction() $this->assertFalse($testObject->ran); - $manager->stageTransactions('foo'); + $manager->stageTransactions('foo', 1); $manager->commit('foo'); $this->assertTrue($testObject->ran); $this->assertEquals(1, $testObject->runs); diff --git a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php index e20d65891a23..5627aaaeb395 100644 --- a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php +++ b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php @@ -130,6 +130,139 @@ public function testItOnlyDispatchesNestedTransactionsEventsAfterTheRootTransact $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); $this->assertTrue(AnotherShouldDispatchAfterCommitTestEvent::$ran); } + + public function testItDoesNotDispatchAfterCommitEventsImmediatelyIfASiblingTransactionIsCommittedFirst() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + DB::transaction(function () { + }); + + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + }); + + $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testChildEventsAreNotDispatchedIfParentTransactionFails() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + + try { + DB::transaction(function () { + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent); + }); + + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + + throw new \Exception; + }); + } catch (\Exception $e) { + // + } + + DB::transaction(fn () => true); + + // Should not have ran because parent transaction failed... + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testItHandlesNestedTransactionsWhereTheSecondOneFails() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + Event::listen(AnotherShouldDispatchAfterCommitTestEvent::class, AnotherShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent()); + }); + + try { + DB::transaction(function () { + // This event should not be dispatched since the transaction is going to fail. + Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); + throw new \Exception; + }); + } catch (\Exception $e) { + } + }); + + $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + $this->assertFalse(AnotherShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testChildCallbacksShouldNotBeDispatchedIfTheirParentFails() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + try { + DB::transaction(function () { + DB::transaction(function () { + Event::dispatch(new ShouldDispatchAfterCommitTestEvent()); + }); + + throw new \Exception; + }); + } catch (\Exception $e) { + // + } + }); + + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testItHandlesFailuresWithTransactionsTwoLevelsHigher() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + Event::listen(AnotherShouldDispatchAfterCommitTestEvent::class, AnotherShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { // lv 1 + DB::transaction(function () { // lv 2 + DB::transaction(fn () => Event::dispatch(new ShouldDispatchAfterCommitTestEvent())); + // lv 2 + }); + + try { + DB::transaction(function () { // lv 2 + // This event should not be dispatched since the transaction is going to fail. + Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); + throw new \Exception; + }); + } catch (\Exception $e) { + } + }); + + $this->assertTrue(ShouldDispatchAfterCommitTestEvent::$ran); + $this->assertFalse(AnotherShouldDispatchAfterCommitTestEvent::$ran); + } + + public function testCommittedTransactionThatWasDeeplyNestedIsRemovedIfTopLevelFails() + { + Event::listen(ShouldDispatchAfterCommitTestEvent::class, ShouldDispatchAfterCommitListener::class); + + DB::transaction(function () { + try { + DB::transaction(function () { + DB::transaction(function () { + DB::transaction(function () { + DB::transaction(fn () => Event::dispatch(new ShouldDispatchAfterCommitTestEvent())); + }); + }); + + throw new \Exception; + }); + } catch (\Exception $e) { + + } + }); + + $this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran); + } } class TransactionUnawareTestEvent From 99d21f37f100b0c5b6a8d09ab0ed86aad3f4a6c8 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 23 Nov 2023 15:30:34 +0000 Subject: [PATCH 095/207] Apply fixes from StyleCI --- .../Integration/Events/ShouldDispatchAfterCommitEventTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php index 5627aaaeb395..6b11d4f9916e 100644 --- a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php +++ b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php @@ -224,7 +224,7 @@ public function testItHandlesFailuresWithTransactionsTwoLevelsHigher() DB::transaction(function () { // lv 1 DB::transaction(function () { // lv 2 DB::transaction(fn () => Event::dispatch(new ShouldDispatchAfterCommitTestEvent())); - // lv 2 + // lv 2 }); try { @@ -257,7 +257,6 @@ public function testCommittedTransactionThatWasDeeplyNestedIsRemovedIfTopLevelFa throw new \Exception; }); } catch (\Exception $e) { - } }); From 0b475bbe658884389f65ff3daf89456ea89c3ea2 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 23 Nov 2023 17:45:07 -0600 Subject: [PATCH 096/207] Better transaction manager object design (#49103) * better object design * Apply fixes from StyleCI * use method for testing * add or * fix condition --------- Co-authored-by: StyleCI Bot --- .../Database/Concerns/ManagesTransactions.php | 40 ++++++------- .../Database/DatabaseTransactionsManager.php | 57 +++++++++++-------- tests/Database/DatabaseConnectionTest.php | 15 ----- .../DatabaseTransactionsManagerTest.php | 32 +++++++---- tests/Database/DatabaseTransactionsTest.php | 21 +++---- .../DatabaseTransactionsManagerTest.php | 4 +- 6 files changed, 81 insertions(+), 88 deletions(-) diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 32bd999b1726..99670cf0949c 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -47,13 +47,16 @@ public function transaction(Closure $callback, $attempts = 1) $this->getPdo()->commit(); } - $this->transactionsManager?->stageTransactions($this->getName(), $this->transactions); - - $this->transactions = max(0, $this->transactions - 1); - - if ($this->afterCommitCallbacksShouldBeExecuted()) { - $this->transactionsManager?->commit($this->getName()); - } + [$levelBeingCommitted, $this->transactions] = [ + $this->transactions, + max(0, $this->transactions - 1), + ]; + + $this->transactionsManager?->commit( + $this->getName(), + $levelBeingCommitted, + $this->transactions + ); } catch (Throwable $e) { $this->handleCommitTransactionException( $e, $currentAttempt, $attempts @@ -196,27 +199,18 @@ public function commit() $this->getPdo()->commit(); } - $this->transactionsManager?->stageTransactions($this->getName(), $this->transactions); + [$levelBeingCommitted, $this->transactions] = [ + $this->transactions, + max(0, $this->transactions - 1), + ]; - $this->transactions = max(0, $this->transactions - 1); - - if ($this->afterCommitCallbacksShouldBeExecuted()) { - $this->transactionsManager?->commit($this->getName()); - } + $this->transactionsManager?->commit( + $this->getName(), $levelBeingCommitted, $this->transactions + ); $this->fireConnectionEvent('committed'); } - /** - * Determine if after commit callbacks should be executed. - * - * @return bool - */ - protected function afterCommitCallbacksShouldBeExecuted() - { - return $this->transactionsManager?->afterCommitCallbacksShouldBeExecuted($this->transactions) || $this->transactions == 0; - } - /** * Handle an exception encountered when committing a transaction. * diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index a8e634e42ddf..c730dc503ac2 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -59,39 +59,26 @@ public function begin($connection, $level) } /** - * Move relevant pending transactions to a committed state. + * Commit the root database transaction and execute callbacks. * * @param string $connection * @param int $levelBeingCommitted - * @return void + * @param int $newTransactionLevel + * @return array */ - public function stageTransactions($connection, $levelBeingCommitted) + public function commit($connection, $levelBeingCommitted, $newTransactionLevel) { - $this->committedTransactions = $this->committedTransactions->merge( - $this->pendingTransactions->filter( - fn ($transaction) => $transaction->connection === $connection && - $transaction->level >= $levelBeingCommitted - ) - ); - - $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection === $connection && - $transaction->level >= $levelBeingCommitted - ); + $this->stageTransactions($connection, $levelBeingCommitted); if (isset($this->currentTransaction[$connection])) { $this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent; } - } - /** - * Commit the root database transaction and execute callbacks. - * - * @param string $connection - * @return void - */ - public function commit($connection) - { + if (! $this->afterCommitCallbacksShouldBeExecuted($newTransactionLevel) && + $newTransactionLevel !== 0) { + return []; + } + // This method is only called when the root database transaction is committed so there // shouldn't be any pending transactions, but going to clear them here anyways just // in case. This method could be refactored to receive a level in the future too. @@ -106,6 +93,30 @@ public function commit($connection) $this->committedTransactions = $forOtherConnections->values(); $forThisConnection->map->executeCallbacks(); + + return $forThisConnection; + } + + /** + * Move relevant pending transactions to a committed state. + * + * @param string $connection + * @param int $levelBeingCommitted + * @return void + */ + public function stageTransactions($connection, $levelBeingCommitted) + { + $this->committedTransactions = $this->committedTransactions->merge( + $this->pendingTransactions->filter( + fn ($transaction) => $transaction->connection === $connection && + $transaction->level >= $levelBeingCommitted + ) + ); + + $this->pendingTransactions = $this->pendingTransactions->reject( + fn ($transaction) => $transaction->connection === $connection && + $transaction->level >= $levelBeingCommitted + ); } /** diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index b91db617a9b5..8709e339c226 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -7,7 +7,6 @@ use Exception; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Connection; -use Illuminate\Database\DatabaseTransactionsManager; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Database\Events\TransactionBeginning; use Illuminate\Database\Events\TransactionCommitted; @@ -290,20 +289,6 @@ public function testCommittingFiresEventsIfSet() $connection->commit(); } - public function testAfterCommitIsExecutedOnFinalCommit() - { - $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit'])->getMock(); - $transactionsManager = $this->getMockBuilder(DatabaseTransactionsManager::class)->onlyMethods(['afterCommitCallbacksShouldBeExecuted'])->getMock(); - $transactionsManager->expects($this->once())->method('afterCommitCallbacksShouldBeExecuted')->with(0)->willReturn(true); - - $connection = $this->getMockConnection([], $pdo); - $connection->setTransactionManager($transactionsManager); - - $connection->transaction(function () { - // do nothing - }); - } - public function testRollBackedFiresEventsIfSet() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); diff --git a/tests/Database/DatabaseTransactionsManagerTest.php b/tests/Database/DatabaseTransactionsManagerTest.php index a62c03dfb5ef..6edc932bcf29 100755 --- a/tests/Database/DatabaseTransactionsManagerTest.php +++ b/tests/Database/DatabaseTransactionsManagerTest.php @@ -66,16 +66,24 @@ public function testCommittingTransactions() $manager->begin('default', 1); $manager->begin('default', 2); $manager->begin('admin', 1); + $manager->begin('admin', 2); - $manager->stageTransactions('default', 1); - $manager->stageTransactions('admin', 1); - $manager->commit('default'); + $manager->commit('default', 2, 1); + $executedTransactions = $manager->commit('default', 1, 0); - $this->assertCount(0, $manager->getPendingTransactions()); - $this->assertCount(1, $manager->getCommittedTransactions()); + $executedAdminTransactions = $manager->commit('admin', 2, 1); + + $this->assertCount(1, $manager->getPendingTransactions()); // One pending "admin" transaction left... + $this->assertCount(2, $executedTransactions); // Two committed tranasctions on "default" + $this->assertCount(0, $executedAdminTransactions); // Zero executed committed tranasctions on "default" + // Level 2 "admin" callback has been staged... $this->assertSame('admin', $manager->getCommittedTransactions()[0]->connection); - $this->assertEquals(1, $manager->getCommittedTransactions()[0]->level); + $this->assertEquals(2, $manager->getCommittedTransactions()[0]->level); + + // Level 1 "admin" callback still pending... + $this->assertSame('admin', $manager->getPendingTransactions()[0]->connection); + $this->assertEquals(1, $manager->getPendingTransactions()[0]->level); } public function testCallbacksAreAddedToTheCurrentTransaction() @@ -121,12 +129,12 @@ public function testCommittingTransactionsExecutesCallbacks() $manager->begin('admin', 1); - $manager->stageTransactions('default', 1); - $manager->commit('default'); + $manager->commit('default', 2, 1); + $manager->commit('default', 1, 0); $this->assertCount(2, $callbacks); - $this->assertEquals(['default', 1], $callbacks[0]); - $this->assertEquals(['default', 2], $callbacks[1]); + $this->assertEquals(['default', 2], $callbacks[0]); + $this->assertEquals(['default', 1], $callbacks[1]); } public function testCommittingExecutesOnlyCallbacksOfTheConnection() @@ -148,8 +156,8 @@ public function testCommittingExecutesOnlyCallbacksOfTheConnection() $callbacks[] = ['admin', 1]; }); - $manager->stageTransactions('default', 1); - $manager->commit('default'); + $manager->commit('default', 2, 1); + $manager->commit('default', 1, 0); $this->assertCount(1, $callbacks); $this->assertEquals(['default', 1], $callbacks[0]); diff --git a/tests/Database/DatabaseTransactionsTest.php b/tests/Database/DatabaseTransactionsTest.php index 384f4223fa81..3affe52a8a00 100644 --- a/tests/Database/DatabaseTransactionsTest.php +++ b/tests/Database/DatabaseTransactionsTest.php @@ -64,8 +64,7 @@ public function testTransactionIsRecordedAndCommitted() { $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); - $transactionManager->shouldReceive('commit')->once()->with('default'); + $transactionManager->shouldReceive('commit')->once()->with('default', 1, 0); $this->connection()->setTransactionManager($transactionManager); @@ -84,8 +83,7 @@ public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods() { $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); - $transactionManager->shouldReceive('commit')->once()->with('default'); + $transactionManager->shouldReceive('commit')->once()->with('default', 1, 0); $this->connection()->setTransactionManager($transactionManager); @@ -105,9 +103,8 @@ public function testNestedTransactionIsRecordedAndCommitted() $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('default', 2); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 2); - $transactionManager->shouldReceive('commit')->once()->with('default'); + $transactionManager->shouldReceive('commit')->once()->with('default', 2, 1); + $transactionManager->shouldReceive('commit')->once()->with('default', 1, 0); $this->connection()->setTransactionManager($transactionManager); @@ -134,11 +131,9 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 2); - $transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('second_connection', 1); - $transactionManager->shouldReceive('stageTransactions')->once()->with('second_connection', 2); - $transactionManager->shouldReceive('commit')->once()->with('default'); - $transactionManager->shouldReceive('commit')->once()->with('second_connection'); + $transactionManager->shouldReceive('commit')->once()->with('default', 1, 0); + $transactionManager->shouldReceive('commit')->once()->with('second_connection', 2, 1); + $transactionManager->shouldReceive('commit')->once()->with('second_connection', 1, 0); $this->connection()->setTransactionManager($transactionManager); $this->connection('second_connection')->setTransactionManager($transactionManager); @@ -196,7 +191,7 @@ public function testTransactionIsRolledBackUsingSeparateMethods() $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('rollback')->once()->with('default', 0); - $transactionManager->shouldNotReceive('commit'); + $transactionManager->shouldNotReceive('commit', 1, 0); $this->connection()->setTransactionManager($transactionManager); diff --git a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php index f5655ebc3862..feac68b8e5bc 100644 --- a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php +++ b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php @@ -42,8 +42,8 @@ public function testItExecutesCallbacksForTheSecondTransaction() $this->assertFalse($testObject->ran); - $manager->stageTransactions('foo', 1); - $manager->commit('foo'); + $manager->commit('foo', 2, 1); + $manager->commit('foo', 1, 0); $this->assertTrue($testObject->ran); $this->assertEquals(1, $testObject->runs); } From f0ac18b9b4323e80fb45f81bd47747445e5e885b Mon Sep 17 00:00:00 2001 From: Clemens Bastian <8781699+amacado@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:50:40 +0100 Subject: [PATCH 097/207] use php83 mb_str_pad() for Str::pad* (#49108) --- src/Illuminate/Support/Str.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 39cefe6054fd..e3a4019c2c13 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -750,6 +750,10 @@ public static function matchAll($pattern, $subject) */ public static function padBoth($value, $length, $pad = ' ') { + if (function_exists('mb_str_pad')) { + return mb_str_pad($value, $length, $pad, STR_PAD_BOTH); + } + $short = max(0, $length - mb_strlen($value)); $shortLeft = floor($short / 2); $shortRight = ceil($short / 2); @@ -769,6 +773,10 @@ public static function padBoth($value, $length, $pad = ' ') */ public static function padLeft($value, $length, $pad = ' ') { + if (function_exists('mb_str_pad')) { + return mb_str_pad($value, $length, $pad, STR_PAD_LEFT); + } + $short = max(0, $length - mb_strlen($value)); return mb_substr(str_repeat($pad, $short), 0, $short).$value; @@ -784,6 +792,10 @@ public static function padLeft($value, $length, $pad = ' ') */ public static function padRight($value, $length, $pad = ' ') { + if (function_exists('mb_str_pad')) { + return mb_str_pad($value, $length, $pad, STR_PAD_RIGHT); + } + $short = max(0, $length - mb_strlen($value)); return $value.mb_substr(str_repeat($pad, $short), 0, $short); From 38b897f5d3c97cca88078b6a37878d812aeb79de Mon Sep 17 00:00:00 2001 From: Noboru Shiroiwa <14008307+nshiro@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:39:25 +0900 Subject: [PATCH 098/207] [10.x] Add Conditionable to TestResponse (#49112) --- src/Illuminate/Testing/TestResponse.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 879db5fac8af..80060e6c7f01 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -15,6 +15,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; use Illuminate\Support\Traits\Tappable; use Illuminate\Support\ViewErrorBag; @@ -32,7 +33,7 @@ */ class TestResponse implements ArrayAccess { - use Concerns\AssertsStatusCodes, Tappable, Macroable { + use Concerns\AssertsStatusCodes, Conditionable, Tappable, Macroable { __call as macroCall; } From b166e82d6fc1d1f63de24a998cdd90839942caca Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sun, 26 Nov 2023 23:53:20 +0800 Subject: [PATCH 099/207] Apply `#[\Override]` attribute on methods extended from Symfony (#49130) * Apply `#[\Override]` attribute when applicable. Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Console/Application.php | 2 ++ src/Illuminate/Console/BufferedConsoleOutput.php | 1 + src/Illuminate/Console/Command.php | 4 ++++ src/Illuminate/Console/OutputStyle.php | 4 ++++ src/Illuminate/Console/QuestionHelper.php | 1 + src/Illuminate/Http/JsonResponse.php | 3 +++ src/Illuminate/Http/Request.php | 4 ++++ src/Illuminate/Http/Response.php | 1 + 8 files changed, 20 insertions(+) diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index f594258e9e76..9bfe73b9b43e 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -208,6 +208,7 @@ public function output() * @param \Symfony\Component\Console\Command\Command $command * @return \Symfony\Component\Console\Command\Command|null */ + #[\Override] public function add(SymfonyCommand $command): ?SymfonyCommand { if ($command instanceof Command) { @@ -285,6 +286,7 @@ public function setContainerCommandLoader() * * @return \Symfony\Component\Console\Input\InputDefinition */ + #[\Override] protected function getDefaultInputDefinition(): InputDefinition { return tap(parent::getDefaultInputDefinition(), function ($definition) { diff --git a/src/Illuminate/Console/BufferedConsoleOutput.php b/src/Illuminate/Console/BufferedConsoleOutput.php index d4ee3954f393..0cb40487ba27 100644 --- a/src/Illuminate/Console/BufferedConsoleOutput.php +++ b/src/Illuminate/Console/BufferedConsoleOutput.php @@ -28,6 +28,7 @@ public function fetch() /** * {@inheritdoc} */ + #[\Override] protected function doWrite(string $message, bool $newline): void { $this->buffer .= $message; diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index 9e0290ebcc6d..4fc256a796ad 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -166,6 +166,7 @@ protected function configureIsolation() * @param \Symfony\Component\Console\Output\OutputInterface $output * @return int */ + #[\Override] public function run(InputInterface $input, OutputInterface $output): int { $this->output = $output instanceof OutputStyle ? $output : $this->laravel->make( @@ -191,6 +192,7 @@ public function run(InputInterface $input, OutputInterface $output): int * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output */ + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { if ($this instanceof Isolatable && $this->option('isolated') !== false && @@ -257,6 +259,7 @@ protected function resolveCommand($command) * * @return bool */ + #[\Override] public function isHidden(): bool { return $this->hidden; @@ -265,6 +268,7 @@ public function isHidden(): bool /** * {@inheritdoc} */ + #[\Override] public function setHidden(bool $hidden = true): static { parent::setHidden($this->hidden = $hidden); diff --git a/src/Illuminate/Console/OutputStyle.php b/src/Illuminate/Console/OutputStyle.php index b0d6f94e8b17..65b918c685d1 100644 --- a/src/Illuminate/Console/OutputStyle.php +++ b/src/Illuminate/Console/OutputStyle.php @@ -52,6 +52,7 @@ public function __construct(InputInterface $input, OutputInterface $output) /** * {@inheritdoc} */ + #[\Override] public function askQuestion(Question $question): mixed { try { @@ -64,6 +65,7 @@ public function askQuestion(Question $question): mixed /** * {@inheritdoc} */ + #[\Override] public function write(string|iterable $messages, bool $newline = false, int $options = 0): void { $this->newLinesWritten = $this->trailingNewLineCount($messages) + (int) $newline; @@ -75,6 +77,7 @@ public function write(string|iterable $messages, bool $newline = false, int $opt /** * {@inheritdoc} */ + #[\Override] public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void { $this->newLinesWritten = $this->trailingNewLineCount($messages) + 1; @@ -86,6 +89,7 @@ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORM /** * {@inheritdoc} */ + #[\Override] public function newLine(int $count = 1): void { $this->newLinesWritten += $count; diff --git a/src/Illuminate/Console/QuestionHelper.php b/src/Illuminate/Console/QuestionHelper.php index a3bf62a520ab..6d8a648fde70 100644 --- a/src/Illuminate/Console/QuestionHelper.php +++ b/src/Illuminate/Console/QuestionHelper.php @@ -17,6 +17,7 @@ class QuestionHelper extends SymfonyQuestionHelper * * @return void */ + #[\Override] protected function writePrompt(OutputInterface $output, Question $question): void { $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); diff --git a/src/Illuminate/Http/JsonResponse.php b/src/Illuminate/Http/JsonResponse.php index 25006c3f65bb..dd5ce0ef5f25 100755 --- a/src/Illuminate/Http/JsonResponse.php +++ b/src/Illuminate/Http/JsonResponse.php @@ -37,6 +37,7 @@ public function __construct($data = null, $status = 200, $headers = [], $options * * @return static */ + #[\Override] public static function fromJsonString(?string $data = null, int $status = 200, array $headers = []): static { return new static($data, $status, $headers, 0, true); @@ -70,6 +71,7 @@ public function getData($assoc = false, $depth = 512) * * @return static */ + #[\Override] public function setData($data = []): static { $this->original = $data; @@ -116,6 +118,7 @@ protected function hasValidJson($jsonError) * * @return static */ + #[\Override] public function setEncodingOptions($options): static { $this->encodingOptions = (int) $options; diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index b7d2c02f3b07..4cbe95c94161 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -389,6 +389,7 @@ public function replace(array $input) * @param mixed $default * @return mixed */ + #[\Override] public function get(string $key, mixed $default = null): mixed { return parent::get($key, $default); @@ -499,6 +500,7 @@ public static function createFromBase(SymfonyRequest $request) * * @return static */ + #[\Override] public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null): static { return parent::duplicate($query, $request, $attributes, $cookies, $this->filterFiles($files), $server); @@ -532,6 +534,7 @@ protected function filterFiles($files) /** * {@inheritdoc} */ + #[\Override] public function hasSession(bool $skipIfUninitialized = false): bool { return $this->session instanceof SymfonySessionDecorator; @@ -540,6 +543,7 @@ public function hasSession(bool $skipIfUninitialized = false): bool /** * {@inheritdoc} */ + #[\Override] public function getSession(): SessionInterface { return $this->hasSession() diff --git a/src/Illuminate/Http/Response.php b/src/Illuminate/Http/Response.php index dad783fcd67a..7e3f0d480e2e 100755 --- a/src/Illuminate/Http/Response.php +++ b/src/Illuminate/Http/Response.php @@ -45,6 +45,7 @@ public function __construct($content = '', $status = 200, array $headers = []) * * @throws \InvalidArgumentException */ + #[\Override] public function setContent(mixed $content): static { $this->original = $content; From d3e3667cb6a1513a702713170e7d05cc649202ed Mon Sep 17 00:00:00 2001 From: Ash Allen Date: Sun, 26 Nov 2023 15:55:12 +0000 Subject: [PATCH 100/207] [10.x] Allow multiple types in Collection's `ensure` method (#49127) * Add ability to pass multiple types to Collection "ensure" method. * Update docblock. * Fixed typo. --- .../Collections/Traits/EnumeratesValues.php | 16 +++++++++++----- tests/Support/SupportCollectionTest.php | 13 +++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index b42edb49d808..ceaed53c1ba4 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -321,7 +321,7 @@ public function value($key, $default = null) * * @template TEnsureOfType * - * @param class-string $type + * @param class-string|array> $type * @return static * * @throws \UnexpectedValueException @@ -331,11 +331,17 @@ public function ensure($type) return $this->each(function ($item) use ($type) { $itemType = get_debug_type($item); - if ($itemType !== $type && ! $item instanceof $type) { - throw new UnexpectedValueException( - sprintf("Collection should only include '%s' items, but '%s' found.", $type, $itemType) - ); + $allowedTypes = is_array($type) ? $type : [$type]; + + foreach ($allowedTypes as $allowedType) { + if ($itemType === $allowedType || $item instanceof $allowedType) { + return true; + } } + + throw new UnexpectedValueException( + sprintf("Collection should only include [%s] items, but '%s' found.", implode(', ', $allowedTypes), $itemType) + ); }); } diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 55fb1359d19c..dc82699b9329 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -5647,6 +5647,19 @@ public function testEnsureForInheritance($collection) $data->ensure(\Throwable::class); } + /** + * @dataProvider collectionClassProvider + */ + public function testEnsureForMultipleTypes($collection) + { + $data = $collection::make([new \Error, 123]); + $data->ensure([\Throwable::class, 'int']); + + $data = $collection::make([new \Error, new \Error, new $collection]); + $this->expectException(UnexpectedValueException::class); + $data->ensure([\Throwable::class, 'int']); + } + /** * @dataProvider collectionClassProvider */ From 6b01dc46093e8d3755e92f15af69539e5447fc11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Birkl=C3=A9?= Date: Mon, 27 Nov 2023 15:33:02 +0100 Subject: [PATCH 101/207] Fix middleware "SetCacheHeaders" with download responses (#49138) --- .../Http/Middleware/SetCacheHeaders.php | 3 ++- tests/Http/Middleware/CacheTest.php | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Middleware/SetCacheHeaders.php b/src/Illuminate/Http/Middleware/SetCacheHeaders.php index 2a7a13baa135..6229e4cfd171 100644 --- a/src/Illuminate/Http/Middleware/SetCacheHeaders.php +++ b/src/Illuminate/Http/Middleware/SetCacheHeaders.php @@ -6,6 +6,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; class SetCacheHeaders { @@ -41,7 +42,7 @@ public function handle($request, Closure $next, $options = []) { $response = $next($request); - if (! $request->isMethodCacheable() || (! $response->getContent() && ! $response instanceof BinaryFileResponse)) { + if (! $request->isMethodCacheable() || (! $response->getContent() && ! $response instanceof BinaryFileResponse && ! $response instanceof StreamedResponse)) { return $response; } diff --git a/tests/Http/Middleware/CacheTest.php b/tests/Http/Middleware/CacheTest.php index d1f31a74c0a1..eb36c664624d 100644 --- a/tests/Http/Middleware/CacheTest.php +++ b/tests/Http/Middleware/CacheTest.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; class CacheTest extends TestCase { @@ -57,7 +58,7 @@ public function testDoNotSetHeaderWhenNoContent() $this->assertNull($response->getEtag()); } - public function testSetHeaderToFileEvenWithNoContent() + public function testSetHeaderToFileResponseEvenWithNoContent() { $response = (new Cache)->handle(new Request, function () { $filePath = __DIR__.'/../fixtures/test.txt'; @@ -68,6 +69,18 @@ public function testSetHeaderToFileEvenWithNoContent() $this->assertNotNull($response->getMaxAge()); } + public function testSetHeaderToDownloadResponseEvenWithNoContent() + { + $response = (new Cache)->handle(new Request, function () { + return new StreamedResponse(function () { + $filePath = __DIR__.'/../fixtures/test.txt'; + readfile($filePath); + }); + }, 'max_age=120;s_maxage=60'); + + $this->assertNotNull($response->getMaxAge()); + } + public function testAddHeaders() { $response = (new Cache)->handle(new Request, function () { From 9bc5b59c3fde9e93ecca749923905a80388159f6 Mon Sep 17 00:00:00 2001 From: Krystian Marcisz Date: Mon, 27 Nov 2023 15:35:29 +0100 Subject: [PATCH 102/207] [10.x][Cache] Fix handling of `false` values in apc (#49145) * [10.x][Cache] Fix handling of `false` values in apc This commit addresses an issue in the `ApcWrapper` and `ApcStore` components of the caching system, particularly when dealing with a cached value of `false`. Previously, the `ApcWrapper`'s default behavior was to simply retrieve and return values from the APC cache store. However, in `ApcStore::get()`, there was a check to determine if the returned value was not `false`. If it wasn't, the value would be returned. This posed a problem where a `false` value is legitimately stored in the cache, but the existing check led to unnecessary cache misses and repeated `get` & `store` operations. To resolve this, we have modified the implementation. Now, we leverage the second parameter of `apc_fetch`, which indicates whether the fetch operation was successful. This allows `ApcStore` to accurately differentiate between a successful fetch of a `false` value and a failed fetch operation. * Add missing test --- src/Illuminate/Cache/ApcStore.php | 6 +----- src/Illuminate/Cache/ApcWrapper.php | 4 +++- tests/Cache/CacheApcStoreTest.php | 8 ++++++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Cache/ApcStore.php b/src/Illuminate/Cache/ApcStore.php index 90132c16f45f..5779635b203b 100755 --- a/src/Illuminate/Cache/ApcStore.php +++ b/src/Illuminate/Cache/ApcStore.php @@ -41,11 +41,7 @@ public function __construct(ApcWrapper $apc, $prefix = '') */ public function get($key) { - $value = $this->apc->get($this->prefix.$key); - - if ($value !== false) { - return $value; - } + return $this->apc->get($this->prefix.$key); } /** diff --git a/src/Illuminate/Cache/ApcWrapper.php b/src/Illuminate/Cache/ApcWrapper.php index 6c129c633288..8f6ce488180f 100755 --- a/src/Illuminate/Cache/ApcWrapper.php +++ b/src/Illuminate/Cache/ApcWrapper.php @@ -29,7 +29,9 @@ public function __construct() */ public function get($key) { - return $this->apcu ? apcu_fetch($key) : apc_fetch($key); + $fetchedValue = $this->apcu ? apcu_fetch($key, $success) : apc_fetch($key, $success); + + return $success ? $fetchedValue : null; } /** diff --git a/tests/Cache/CacheApcStoreTest.php b/tests/Cache/CacheApcStoreTest.php index fed3a9dc8cc1..e13d2adf63ec 100755 --- a/tests/Cache/CacheApcStoreTest.php +++ b/tests/Cache/CacheApcStoreTest.php @@ -25,6 +25,14 @@ public function testAPCValueIsReturned() $this->assertSame('bar', $store->get('foo')); } + public function testAPCFalseValueIsReturned() + { + $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['get'])->getMock(); + $apc->expects($this->once())->method('get')->willReturn(false); + $store = new ApcStore($apc); + $this->assertFalse($store->get('foo')); + } + public function testGetMultipleReturnsNullWhenNotFoundAndValueWhenFound() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['get'])->getMock(); From d8a0931bfba4ca17418caae5173e0d1796e2b41c Mon Sep 17 00:00:00 2001 From: Mohd Hafizuddin M Marzuki Date: Mon, 27 Nov 2023 22:44:10 +0800 Subject: [PATCH 103/207] [10.x] Reset numeric rules after each attribute's validation (#49142) * Reset numeric rules after each attribute's validation * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Validation/Validator.php | 9 + tests/Validation/ValidationValidatorTest.php | 182 ++++++++++++++++++- 2 files changed, 183 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 7bcdc97b8212..3894e6e141ba 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -290,6 +290,13 @@ class Validator implements ValidatorContract */ protected $numericRules = ['Numeric', 'Integer', 'Decimal']; + /** + * The default numeric related validation rules. + * + * @var string[] + */ + protected $defaultNumericRules = ['Numeric', 'Integer', 'Decimal']; + /** * The current placeholder for dots in rule keys. * @@ -641,6 +648,8 @@ protected function validateAttribute($attribute, $rule) $method = "validate{$rule}"; + $this->numericRules = $this->defaultNumericRules; + if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) { $this->addFailure($attribute, $rule, $parameters); } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index cc57af1f96d0..6ce65cc2b6d5 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -3955,6 +3955,166 @@ public function testValidationExistsIsNotCalledUnnecessarily() $this->assertTrue($v->passes()); } + public function testValidateGtMessagesAreCorrect() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines([ + 'validation.gt.numeric' => 'The :attribute field must be greater than :value.', + 'validation.gt.string' => 'The :attribute field must be greater than :value characters.', + 'validation.gt.file' => 'The :attribute field must be greater than :value kilobytes.', + 'validation.gt.array' => 'The :attribute field must have more than :value items.', + ], '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); + $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); + + $v = new Validator($trans, [ + 'numeric' => 7, + 'string' => 'abcd', + 'file' => $file, + 'array' => [1, 2, 3], + 'other_numeric' => 10, + 'other_string' => 'abcde', + 'other_file' => $otherFile, + 'other_array' => [1, 2, 3, 4], + ], [ + 'numeric' => 'gt:other_numeric', + 'string' => 'gt:other_string', + 'file' => 'gt:other_file', + 'array' => 'array|gt:other_array', + ]); + + $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')); + } + + public function testValidateGteMessagesAreCorrect() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines([ + 'validation.gte.numeric' => 'The :attribute field must be greater than or equal to :value.', + 'validation.gte.string' => 'The :attribute field must be greater than or equal to :value characters.', + 'validation.gte.file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'validation.gte.array' => 'The :attribute field must have :value items or more.', + ], '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); + $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); + + $v = new Validator($trans, [ + 'numeric' => 7, + 'string' => 'abcd', + 'file' => $file, + 'array' => [1, 2, 3], + 'other_numeric' => 10, + 'other_string' => 'abcde', + 'other_file' => $otherFile, + 'other_array' => [1, 2, 3, 4], + ], [ + 'numeric' => 'gte:other_numeric', + 'string' => 'gte:other_string', + 'file' => 'gte:other_file', + 'array' => 'array|gte:other_array', + ]); + + $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')); + } + + public function testValidateLtMessagesAreCorrect() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines([ + 'validation.lt.numeric' => 'The :attribute field must be less than :value.', + 'validation.lt.string' => 'The :attribute field must be less than :value characters.', + 'validation.lt.file' => 'The :attribute field must be less than :value kilobytes.', + 'validation.lt.array' => 'The :attribute field must have less than :value items.', + ], '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); + $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); + + $v = new Validator($trans, [ + 'numeric' => 7, + 'string' => 'abcd', + 'file' => $file, + 'array' => [1, 2, 3], + 'other_numeric' => 5, + 'other_string' => 'abc', + 'other_file' => $otherFile, + 'other_array' => [1, 2], + ], [ + 'numeric' => 'lt:other_numeric', + 'string' => 'lt:other_string', + 'file' => 'lt:other_file', + 'array' => 'array|lt:other_array', + ]); + + $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')); + } + + public function testValidateLteMessagesAreCorrect() + { + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines([ + 'validation.lte.numeric' => 'The :attribute field must be less than or equal to :value.', + 'validation.lte.string' => 'The :attribute field must be less than or equal to :value characters.', + 'validation.lte.file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'validation.lte.array' => 'The :attribute field must not have more than :value items.', + ], '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); + $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); + + $v = new Validator($trans, [ + 'numeric' => 7, + 'string' => 'abcd', + 'file' => $file, + 'array' => [1, 2, 3], + 'other_numeric' => 5, + 'other_string' => 'abc', + 'other_file' => $otherFile, + 'other_array' => [1, 2], + ], [ + 'numeric' => 'lte:other_numeric', + 'string' => 'lte:other_string', + 'file' => 'lte:other_file', + 'array' => 'array|lte:other_array', + ]); + + $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')); + } + public function testValidateIp() { $trans = $this->getIlluminateArrayTranslator(); @@ -8917,6 +9077,9 @@ public function testItTrimsSpaceFromParameters() 'foo' => '4', ' foo' => ' 5', ' foo ' => ' 6 ', + 'foo_str' => 'abcd', + ' foo_str' => ' abcd', + ' foo_str ' => ' abcd ', ], [ 'min' => 'numeric|min: 20', 'min_str' => 'min: 5', @@ -8925,16 +9088,16 @@ public function testItTrimsSpaceFromParameters() 'between_str' => "between:\t 5, 6\n", 'gt' => 'numeric|gt: 4', 'gt_field' => 'numeric|gt:foo', - 'gt_str' => 'gt:foo', + 'gt_str' => 'gt:foo_str', 'lt' => 'numeric|lt: 6', 'lt_field' => 'numeric|lt: foo ', - 'lt_str' => 'lt: foo ', + 'lt_str' => 'lt: foo_str ', 'gte' => 'numeric|gte: 5', 'gte_field' => 'numeric|gte: foo', - 'gte_str' => 'gte: foo', + 'gte_str' => 'gte: foo_str', 'lte' => 'numeric|lte: 5', 'lte_field' => 'numeric|lte: foo', - 'lte_str' => 'lte: foo', + 'lte_str' => 'lte: foo_str', 'max' => 'numeric|max: 20', 'max_str' => 'max: 5', 'size' => 'numeric|size: 20', @@ -8967,6 +9130,9 @@ public function testItTrimsSpaceFromParameters() 'foo' => '4', ' foo' => ' 5', ' foo ' => ' 6 ', + 'foo_str' => 'abcd', + ' foo_str' => ' abcd', + ' foo_str ' => ' abcd ', ], [ 'min' => 'numeric|min: 21', 'min_str' => 'min: 6', @@ -8975,16 +9141,16 @@ public function testItTrimsSpaceFromParameters() 'between_str' => "between:\t 6, 7\n", 'gt' => 'numeric|gt: 5', 'gt_field' => 'numeric|gt: foo ', - 'gt_str' => 'gt: foo', + 'gt_str' => 'gt: foo_str', 'lt' => 'numeric|lt: 5', 'lt_field' => 'numeric|lt: foo', - 'lt_str' => 'lt: foo', + 'lt_str' => 'lt: foo_str', 'gte' => 'numeric|gte: 6', 'gte_field' => 'numeric|gte: foo ', - 'gte_str' => 'gte: foo ', + 'gte_str' => 'gte: foo_str ', 'lte' => 'numeric|lte: 4', 'lte_field' => 'numeric|lte:foo', - 'lte_str' => 'lte:foo', + 'lte_str' => 'lte:foo_str', 'max' => 'numeric|max: 19', 'max_str' => 'max: 4', 'size' => 'numeric|size: 19', From 9337c0ecdb0dbd000e5f53300a2566054469581a Mon Sep 17 00:00:00 2001 From: Takayasu Oyama Date: Mon, 27 Nov 2023 23:45:18 +0900 Subject: [PATCH 104/207] [10.x] Extract dirty getter for performUpdate (#49141) --- .../Database/Eloquent/Concerns/HasAttributes.php | 10 ++++++++++ src/Illuminate/Database/Eloquent/Model.php | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index d4f86c52c2be..8385eef4eea9 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2038,6 +2038,16 @@ public function getDirty() return $dirty; } + /** + * Get the attributes that have been changed since the last sync for an update operation. + * + * @return array + */ + protected function getDirtyForUpdate() + { + return $this->getDirty(); + } + /** * Get the attributes that were changed when the model was last saved. * diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index c10fac5b9e8b..9648d6b99df4 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1207,7 +1207,7 @@ protected function performUpdate(Builder $query) // Once we have run the update operation, we will fire the "updated" event for // this model instance. This will allow developers to hook into these after // models are updated, giving them a chance to do any special processing. - $dirty = $this->getDirty(); + $dirty = $this->getDirtyForUpdate(); if (count($dirty) > 0) { $this->setKeysForSaveQuery($query)->update($dirty); From 2f746b2ce9e720c5103f6835ead39ad737b55f3f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 27 Nov 2023 22:45:44 +0800 Subject: [PATCH 105/207] [11.x] Add `symfony/polyfill-php83` to component using `#[\Override]` (#49140) Signed-off-by: Mior Muhammad Zaki --- composer.json | 1 + src/Illuminate/Console/composer.json | 1 + src/Illuminate/Http/composer.json | 1 + 3 files changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 439d904e1aa3..84c53c70428d 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "symfony/http-kernel": "^7.0", "symfony/mailer": "^7.0", "symfony/mime": "^7.0", + "symfony/polyfill-php83": "^1.28", "symfony/process": "^7.0", "symfony/routing": "^7.0", "symfony/uid": "^7.0", diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index 68e50920d317..c26420bcc122 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -24,6 +24,7 @@ "laravel/prompts": "^0.1.12", "nunomaduro/termwind": "^2.0", "symfony/console": "^7.0", + "symfony/polyfill-php83": "^1.28", "symfony/process": "^7.0" }, "autoload": { diff --git a/src/Illuminate/Http/composer.json b/src/Illuminate/Http/composer.json index 768d72bc6637..f683ad8e8390 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -24,6 +24,7 @@ "illuminate/support": "^11.0", "symfony/http-foundation": "^7.0", "symfony/http-kernel": "^7.0", + "symfony/polyfill-php83": "^1.28", "symfony/mime": "^7.0" }, "autoload": { From cc374af72f5d2dc3e4e43c2d4b806a3245787ce5 Mon Sep 17 00:00:00 2001 From: Lucas Michot Date: Mon, 27 Nov 2023 15:46:52 +0100 Subject: [PATCH 106/207] Resolve itemType outside the closure (#49137) --- src/Illuminate/Collections/Traits/EnumeratesValues.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index ceaed53c1ba4..dd4d5ab292a2 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -328,10 +328,10 @@ public function value($key, $default = null) */ public function ensure($type) { - return $this->each(function ($item) use ($type) { - $itemType = get_debug_type($item); + $allowedTypes = is_array($type) ? $type : [$type]; - $allowedTypes = is_array($type) ? $type : [$type]; + return $this->each(function ($item) use ($allowedTypes) { + $itemType = get_debug_type($item); foreach ($allowedTypes as $allowedType) { if ($itemType === $allowedType || $item instanceof $allowedType) { From e8465354aad6ec52a86d5bb916822869a048fca2 Mon Sep 17 00:00:00 2001 From: Ronald Edelschaap Date: Mon, 27 Nov 2023 16:30:57 +0100 Subject: [PATCH 107/207] Allow "missing" method to be used on route groups (#49144) * Allow missing method to be used on route groups * Code style --- src/Illuminate/Routing/RouteRegistrar.php | 2 ++ tests/Routing/RouteRegistrarTest.php | 4 ++-- tests/Routing/RoutingRouteTest.php | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Routing/RouteRegistrar.php b/src/Illuminate/Routing/RouteRegistrar.php index 51a7bd74e27e..f38301866e6a 100644 --- a/src/Illuminate/Routing/RouteRegistrar.php +++ b/src/Illuminate/Routing/RouteRegistrar.php @@ -20,6 +20,7 @@ * @method \Illuminate\Routing\RouteRegistrar controller(string $controller) * @method \Illuminate\Routing\RouteRegistrar domain(string $value) * @method \Illuminate\Routing\RouteRegistrar middleware(array|string|null $middleware) + * @method \Illuminate\Routing\RouteRegistrar missing(\Closure $missing) * @method \Illuminate\Routing\RouteRegistrar name(string $value) * @method \Illuminate\Routing\RouteRegistrar namespace(string|null $value) * @method \Illuminate\Routing\RouteRegistrar prefix(string $prefix) @@ -65,6 +66,7 @@ class RouteRegistrar 'controller', 'domain', 'middleware', + 'missing', 'name', 'namespace', 'prefix', diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index cc0a366d45cc..9171119f0741 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -498,9 +498,9 @@ public function testRouteGroupChaining() public function testRegisteringNonApprovedAttributesThrows() { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('Method Illuminate\Routing\RouteRegistrar::missing does not exist.'); + $this->expectExceptionMessage('Method Illuminate\Routing\RouteRegistrar::unsupportedMethod does not exist.'); - $this->router->domain('foo')->missing('bar')->group(function ($router) { + $this->router->domain('foo')->unsupportedMethod('bar')->group(function ($router) { // }); } diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index d5766ed5f67b..da8318a04aee 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -1883,6 +1883,29 @@ public function testImplicitBindingsWithMissingModelHandledByMissing() $this->assertEquals(302, $response->getStatusCode()); } + public function testImplicitBindingsWithMissingModelHandledByMissingOnGroupLevel() + { + $router = $this->getRouter(); + $router->as('foo.') + ->missing(fn () => new RedirectResponse('/', 302)) + ->group(function () use ($router) { + $router->get('foo/{bar}', [ + 'middleware' => SubstituteBindings::class, + 'uses' => function (RouteModelBindingNullStub $bar = null) { + $this->assertInstanceOf(RouteModelBindingNullStub::class, $bar); + + return $bar->first(); + }, + ]); + }); + + $request = Request::create('foo/taylor', 'GET'); + + $response = $router->dispatch($request); + $this->assertTrue($response->isRedirect('/')); + $this->assertEquals(302, $response->getStatusCode()); + } + public function testImplicitBindingsWithOptionalParameterWithNoKeyInUri() { $router = $this->getRouter(); From 2fd25f628111f97bac20b8aca6cac43ddf8b4d5f Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Mon, 27 Nov 2023 15:31:31 +0000 Subject: [PATCH 108/207] Update facade docblocks --- src/Illuminate/Support/Facades/Route.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Route.php b/src/Illuminate/Support/Facades/Route.php index 6fdaf6ee11bc..db5c9a562cf2 100755 --- a/src/Illuminate/Support/Facades/Route.php +++ b/src/Illuminate/Support/Facades/Route.php @@ -90,6 +90,7 @@ * @method static \Illuminate\Routing\RouteRegistrar controller(string $controller) * @method static \Illuminate\Routing\RouteRegistrar domain(string $value) * @method static \Illuminate\Routing\RouteRegistrar middleware(array|string|null $middleware) + * @method static \Illuminate\Routing\RouteRegistrar missing(\Closure $missing) * @method static \Illuminate\Routing\RouteRegistrar name(string $value) * @method static \Illuminate\Routing\RouteRegistrar namespace(string|null $value) * @method static \Illuminate\Routing\RouteRegistrar prefix(string $prefix) From 50807dbcb79c863d5cb5da52de929ee9ca508d33 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 27 Nov 2023 19:47:31 +0330 Subject: [PATCH 109/207] [10.x] Get tables and views info (#49020) * add getTables and getViews * fix tests * fix a typo * add more integration tests * add more integration tests * remove redundant schema on mysql * formatting --------- Co-authored-by: Taylor Otwell --- .../Database/Query/Processors/Processor.php | 49 ++++++- src/Illuminate/Database/Schema/Builder.php | 60 ++++++-- .../Database/Schema/Grammars/MySqlGrammar.php | 79 ++++++++--- .../Schema/Grammars/PostgresGrammar.php | 71 +++++++--- .../Schema/Grammars/SQLiteGrammar.php | 83 ++++++++--- .../Schema/Grammars/SqlServerGrammar.php | 73 +++++++--- .../Database/Schema/MySqlBuilder.php | 97 ++++++------- .../Database/Schema/PostgresBuilder.php | 129 ++++++++++-------- .../Database/Schema/SQLiteBuilder.php | 66 +++++---- .../Database/Schema/SqlServerBuilder.php | 4 + src/Illuminate/Support/Facades/Schema.php | 3 +- .../DatabaseMySQLSchemaBuilderTest.php | 7 +- .../Database/DatabasePostgresBuilderTest.php | 38 ++++-- tests/Database/DatabaseSchemaBuilderTest.php | 8 +- .../Postgres/PostgresSchemaBuilderTest.php | 35 +++++ .../Database/SchemaBuilderTest.php | 37 +++++ 16 files changed, 595 insertions(+), 244 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 8fb8b0e7ab48..03f8ea982763 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -37,16 +37,44 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu } /** - * Process the results of a column listing query. + * Process the results of a tables query. * - * @deprecated Will be removed in a future Laravel version. + * @param array $results + * @return array + */ + public function processTables($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'schema' => $result->schema ?? null, // PostgreSQL and SQL Server + 'size' => isset($result->size) ? (int) $result->size : null, + 'comment' => $result->comment ?? null, // MySQL and PostgreSQL + 'collation' => $result->collation ?? null, // MySQL only + 'engine' => $result->engine ?? null, // MySQL only + ]; + }, $results); + } + + /** + * Process the results of a views query. * * @param array $results * @return array */ - public function processColumnListing($results) + public function processViews($results) { - return $results; + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'schema' => $result->schema ?? null, // PostgreSQL and SQL Server + 'definition' => $result->definition, + ]; + }, $results); } /** @@ -59,4 +87,17 @@ public function processColumns($results) { return $results; } + + /** + * Process the results of a column listing query. + * + * @deprecated Will be removed in a future Laravel version. + * + * @param array $results + * @return array + */ + public function processColumnListing($results) + { + return $results; + } } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 1c1e4ff3c8be..92c5f76884ee 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -159,9 +159,51 @@ public function hasTable($table) { $table = $this->connection->getTablePrefix().$table; - return count($this->connection->selectFromWriteConnection( - $this->grammar->compileTableExists(), [$table] - )) > 0; + foreach ($this->getTables() as $value) { + if (strtolower($table) === strtolower($value['name'])) { + return true; + } + } + + return false; + } + + /** + * Get the tables that belong to the database. + * + * @return array + */ + public function getTables() + { + return $this->connection->getPostProcessor()->processTables( + $this->connection->selectFromWriteConnection($this->grammar->compileTables()) + ); + } + + /** + * Get the views that belong to the database. + * + * @return array + */ + public function getViews() + { + return $this->connection->getPostProcessor()->processViews( + $this->connection->selectFromWriteConnection($this->grammar->compileViews()) + ); + } + + /** + * Get all of the table names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + * + * @throws \LogicException + */ + public function getAllTables() + { + throw new LogicException('This database driver does not support getting all tables.'); } /** @@ -385,18 +427,6 @@ public function dropAllTypes() throw new LogicException('This database driver does not support dropping all types.'); } - /** - * Get all of the table names for the database. - * - * @return array - * - * @throws \LogicException - */ - public function getAllTables() - { - throw new LogicException('This database driver does not support getting all tables.'); - } - /** * Rename a table on the schema. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 797a5897ff81..8ec5fb4e3040 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -68,6 +68,8 @@ public function compileDropDatabaseIfExists($name) /** * Compile the query to determine the list of tables. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileTableExists() @@ -75,6 +77,63 @@ public function compileTableExists() return "select * from information_schema.tables where table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"; } + /** + * Compile the query to determine the tables. + * + * @param string $database + * @return string + */ + public function compileTables($database) + { + return sprintf( + 'select table_name as `name`, (data_length + index_length) as `size`, ' + .'table_comment as `comment`, engine as `engine`, table_collation as `collation` ' + ."from information_schema.tables where table_schema = %s and table_type = 'BASE TABLE' " + .'order by table_name', + $this->quoteString($database) + ); + } + + /** + * Compile the query to determine the views. + * + * @param string $database + * @return string + */ + public function compileViews($database) + { + return sprintf( + 'select table_name as `name`, view_definition as `definition` ' + .'from information_schema.views where table_schema = %s ' + .'order by table_name', + $this->quoteString($database) + ); + } + + /** + * Compile the SQL needed to retrieve all table names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return string + */ + public function compileGetAllTables() + { + return 'SHOW FULL TABLES WHERE table_type = \'BASE TABLE\''; + } + + /** + * Compile the SQL needed to retrieve all view names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return string + */ + public function compileGetAllViews() + { + return 'SHOW FULL TABLES WHERE table_type = \'VIEW\''; + } + /** * Compile the query to determine the list of columns. * @@ -532,26 +591,6 @@ public function compileDropAllViews($views) return 'drop view '.implode(',', $this->wrapArray($views)); } - /** - * Compile the SQL needed to retrieve all table names. - * - * @return string - */ - public function compileGetAllTables() - { - return 'SHOW FULL TABLES WHERE table_type = \'BASE TABLE\''; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @return string - */ - public function compileGetAllViews() - { - return 'SHOW FULL TABLES WHERE table_type = \'VIEW\''; - } - /** * Compile the command to enable foreign key constraints. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index c8f19cac2b3b..061984bdbaed 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -78,6 +78,55 @@ public function compileTableExists() return "select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"; } + /** + * Compile the query to determine the tables. + * + * @return string + */ + public function compileTables() + { + return 'select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, ' + ."obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " + ."where c.relkind = 'r' and n.oid = c.relnamespace " + .'order by c.relname'; + } + + /** + * Compile the query to determine the views. + * + * @return string + */ + public function compileViews() + { + return 'select viewname as name, schemaname as schema, definition from pg_views order by viewname'; + } + + /** + * Compile the SQL needed to retrieve all table names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @param string|array $searchPath + * @return string + */ + public function compileGetAllTables($searchPath) + { + return "select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('".implode("','", (array) $searchPath)."')"; + } + + /** + * Compile the SQL needed to retrieve all view names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @param string|array $searchPath + * @return string + */ + public function compileGetAllViews($searchPath) + { + return "select viewname, concat('\"', schemaname, '\".\"', viewname, '\"') as qualifiedname from pg_catalog.pg_views where schemaname in ('".implode("','", (array) $searchPath)."')"; + } + /** * Compile the query to determine the list of columns. * @@ -398,28 +447,6 @@ public function compileDropAllTypes($types) return 'drop type '.implode(',', $this->escapeNames($types)).' cascade'; } - /** - * Compile the SQL needed to retrieve all table names. - * - * @param string|array $searchPath - * @return string - */ - public function compileGetAllTables($searchPath) - { - return "select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('".implode("','", (array) $searchPath)."')"; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @param string|array $searchPath - * @return string - */ - public function compileGetAllViews($searchPath) - { - return "select viewname, concat('\"', schemaname, '\".\"', viewname, '\"') as qualifiedname from pg_catalog.pg_views where schemaname in ('".implode("','", (array) $searchPath)."')"; - } - /** * Compile the SQL needed to retrieve all type names. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 05294f2151e2..02a75eeaf58c 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -29,6 +29,8 @@ class SQLiteGrammar extends Grammar /** * Compile the query to determine if a table exists. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileTableExists() @@ -36,6 +38,67 @@ public function compileTableExists() return "select * from sqlite_master where type = 'table' and name = ?"; } + /** + * Compile the query to determine if the dbstat table is available. + * + * @return string + */ + public function compileDbstatExists() + { + return "select exists (select 1 from pragma_compile_options where compile_options = 'ENABLE_DBSTAT_VTAB') as enabled"; + } + + /** + * Compile the query to determine the tables. + * + * @param bool $withSize + * @return string + */ + public function compileTables($withSize = false) + { + return $withSize + ? 'select m.tbl_name as name, sum(s.pgsize) as size from sqlite_master as m ' + .'join dbstat as s on s.name = m.name ' + ."where m.type in ('table', 'index') and m.tbl_name not like 'sqlite_%' " + .'group by m.tbl_name ' + .'order by m.tbl_name' + : "select name from sqlite_master where type = 'table' and name not like 'sqlite_%' order by name"; + } + + /** + * Compile the query to determine the views. + * + * @return string + */ + public function compileViews() + { + return "select name, sql as definition from sqlite_master where type = 'view' order by name"; + } + + /** + * Compile the SQL needed to retrieve all table names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return string + */ + public function compileGetAllTables() + { + return 'select type, name from sqlite_master where type = \'table\' and name not like \'sqlite_%\''; + } + + /** + * Compile the SQL needed to retrieve all view names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return string + */ + public function compileGetAllViews() + { + return 'select type, name from sqlite_master where type = \'view\''; + } + /** * Compile the query to determine the list of columns. * @@ -283,26 +346,6 @@ public function compileDropAllViews() return "delete from sqlite_master where type in ('view')"; } - /** - * Compile the SQL needed to retrieve all table names. - * - * @return string - */ - public function compileGetAllTables() - { - return 'select type, name from sqlite_master where type = \'table\' and name not like \'sqlite_%\''; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @return string - */ - public function compileGetAllViews() - { - return 'select type, name from sqlite_master where type = \'view\''; - } - /** * Compile the SQL needed to rebuild the database. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index ab1944a83341..084021aa05c6 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -69,6 +69,8 @@ public function compileDropDatabaseIfExists($name) /** * Compile the query to determine if a table exists. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileTableExists() @@ -76,6 +78,57 @@ public function compileTableExists() return "select * from sys.sysobjects where id = object_id(?) and xtype in ('U', 'V')"; } + /** + * Compile the query to determine the tables. + * + * @return string + */ + public function compileTables() + { + return 'select t.name as name, SCHEMA_NAME(t.schema_id) as [schema], sum(u.total_pages) * 8 * 1024 as size ' + .'from sys.tables as t ' + .'join sys.partitions as p on p.object_id = t.object_id ' + .'join sys.allocation_units as u on u.container_id = p.hobt_id ' + .'group by t.name, t.schema_id ' + .'order by t.name'; + } + + /** + * Compile the query to determine the views. + * + * @return string + */ + public function compileViews() + { + return 'select name, SCHEMA_NAME(v.schema_id) as [schema], definition from sys.views as v ' + .'inner join sys.sql_modules as m on v.object_id = m.object_id ' + .'order by name'; + } + + /** + * Compile the SQL needed to retrieve all table names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return string + */ + public function compileGetAllTables() + { + return "select name, type from sys.tables where type = 'U'"; + } + + /** + * Compile the SQL needed to retrieve all view names. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return string + */ + public function compileGetAllViews() + { + return "select name, type from sys.objects where type = 'V'"; + } + /** * Compile the query to determine the list of columns. * @@ -504,26 +557,6 @@ public function compileDropAllViews() EXEC sp_executesql @sql;"; } - /** - * Compile the SQL needed to retrieve all table names. - * - * @return string - */ - public function compileGetAllTables() - { - return "select name, type from sys.tables where type = 'U'"; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @return string - */ - public function compileGetAllViews() - { - return "select name, type from sys.objects where type = 'V'"; - } - /** * Create the column definition for a char type. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index 3fcb73764fbe..1c8c767bd997 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -31,18 +31,59 @@ public function dropDatabaseIfExists($name) } /** - * Determine if the given table exists. + * Get the tables for the database. * - * @param string $table - * @return bool + * @return array */ - public function hasTable($table) + public function getTables() { - $table = $this->connection->getTablePrefix().$table; + return $this->connection->getPostProcessor()->processTables( + $this->connection->selectFromWriteConnection( + $this->grammar->compileTables($this->connection->getDatabaseName()) + ) + ); + } + + /** + * Get the views for the database. + * + * @return array + */ + public function getViews() + { + return $this->connection->getPostProcessor()->processViews( + $this->connection->selectFromWriteConnection( + $this->grammar->compileViews($this->connection->getDatabaseName()) + ) + ); + } + + /** + * Get all of the table names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + */ + public function getAllTables() + { + return $this->connection->select( + $this->grammar->compileGetAllTables() + ); + } - return count($this->connection->selectFromWriteConnection( - $this->grammar->compileTableExists(), [$this->connection->getDatabaseName(), $table] - )) > 0; + /** + * Get all of the view names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + */ + public function getAllViews() + { + return $this->connection->select( + $this->grammar->compileGetAllViews() + ); } /** @@ -69,13 +110,7 @@ public function getColumns($table) */ public function dropAllTables() { - $tables = []; - - foreach ($this->getAllTables() as $row) { - $row = (array) $row; - - $tables[] = reset($row); - } + $tables = array_column($this->getTables(), 'name'); if (empty($tables)) { return; @@ -97,13 +132,7 @@ public function dropAllTables() */ public function dropAllViews() { - $views = []; - - foreach ($this->getAllViews() as $row) { - $row = (array) $row; - - $views[] = reset($row); - } + $views = array_column($this->getViews(), 'name'); if (empty($views)) { return; @@ -113,28 +142,4 @@ public function dropAllViews() $this->grammar->compileDropAllViews($views) ); } - - /** - * Get all of the table names for the database. - * - * @return array - */ - public function getAllTables() - { - return $this->connection->select( - $this->grammar->compileGetAllTables() - ); - } - - /** - * Get all of the view names for the database. - * - * @return array - */ - public function getAllViews() - { - return $this->connection->select( - $this->grammar->compileGetAllViews() - ); - } } diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 34a75ceb5fd5..2074e3b17cbb 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -53,6 +53,42 @@ public function hasTable($table) )) > 0; } + /** + * Get all of the table names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + */ + public function getAllTables() + { + return $this->connection->select( + $this->grammar->compileGetAllTables( + $this->parseSearchPath( + $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') + ) + ) + ); + } + + /** + * Get all of the view names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + */ + public function getAllViews() + { + return $this->connection->select( + $this->grammar->compileGetAllViews( + $this->parseSearchPath( + $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') + ) + ) + ); + } + /** * Drop all tables from the database. * @@ -66,11 +102,14 @@ public function dropAllTables() $this->connection->getConfig('dont_drop') ?? ['spatial_ref_sys'] ); - foreach ($this->getAllTables() as $row) { - $row = (array) $row; + $schemas = $this->grammar->escapeNames($this->getSchemas()); + + foreach ($this->getTables() as $table) { + $qualifiedName = $table['schema'].'.'.$table['name']; - if (empty(array_intersect($this->grammar->escapeNames($row), $excludedTables))) { - $tables[] = $row['qualifiedname'] ?? reset($row); + if (empty(array_intersect($this->grammar->escapeNames([$table['name'], $qualifiedName]), $excludedTables)) + && in_array($this->grammar->escapeNames([$table['schema']])[0], $schemas)) { + $tables[] = $qualifiedName; } } @@ -92,10 +131,12 @@ public function dropAllViews() { $views = []; - foreach ($this->getAllViews() as $row) { - $row = (array) $row; + $schemas = $this->grammar->escapeNames($this->getSchemas()); - $views[] = $row['qualifiedname'] ?? reset($row); + foreach ($this->getViews() as $view) { + if (in_array($this->grammar->escapeNames([$view['schema']])[0], $schemas)) { + $views[] = $view['schema'].'.'.$view['name']; + } } if (empty($views)) { @@ -107,6 +148,18 @@ public function dropAllViews() ); } + /** + * Get all of the type names for the database. + * + * @return array + */ + public function getAllTypes() + { + return $this->connection->select( + $this->grammar->compileGetAllTypes() + ); + } + /** * Drop all types from the database. * @@ -131,50 +184,6 @@ public function dropAllTypes() ); } - /** - * Get all of the table names for the database. - * - * @return array - */ - public function getAllTables() - { - return $this->connection->select( - $this->grammar->compileGetAllTables( - $this->parseSearchPath( - $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') - ) - ) - ); - } - - /** - * Get all of the view names for the database. - * - * @return array - */ - public function getAllViews() - { - return $this->connection->select( - $this->grammar->compileGetAllViews( - $this->parseSearchPath( - $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') - ) - ) - ); - } - - /** - * Get all of the type names for the database. - * - * @return array - */ - public function getAllTypes() - { - return $this->connection->select( - $this->grammar->compileGetAllTypes() - ); - } - /** * Get the columns for a given table. * @@ -195,17 +204,25 @@ public function getColumns($table) } /** - * Parse the database object reference and extract the database, schema, and table. + * Get the schemas for the connection. * - * @param string $reference * @return array */ - protected function parseSchemaAndTable($reference) + protected function getSchemas() { - $searchPath = $this->parseSearchPath( + return $this->parseSearchPath( $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') ?: 'public' ); + } + /** + * Parse the database object reference and extract the database, schema, and table. + * + * @param string $reference + * @return array + */ + protected function parseSchemaAndTable($reference) + { $parts = explode('.', $reference); $database = $this->connection->getConfig('database'); @@ -221,7 +238,7 @@ protected function parseSchemaAndTable($reference) // We will use the default schema unless the schema has been specified in the // query. If the schema has been specified in the query then we can use it // instead of a default schema configured in the connection search path. - $schema = $searchPath[0]; + $schema = $this->getSchemas()[0]; if (count($parts) === 2) { $schema = $parts[0]; diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 4e74f92d5802..9e7960ba3a65 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -30,6 +30,48 @@ public function dropDatabaseIfExists($name) : true; } + /** + * Get the tables for the database. + * + * @return array + */ + public function getTables() + { + $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); + + return $this->connection->getPostProcessor()->processTables( + $this->connection->selectFromWriteConnection($this->grammar->compileTables($withSize)) + ); + } + + /** + * Get all of the table names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + */ + public function getAllTables() + { + return $this->connection->select( + $this->grammar->compileGetAllTables() + ); + } + + /** + * Get all of the view names for the database. + * + * @deprecated Will be removed in a future Laravel version. + * + * @return array + */ + public function getAllViews() + { + return $this->connection->select( + $this->grammar->compileGetAllViews() + ); + } + /** * Drop all tables from the database. * @@ -66,30 +108,6 @@ public function dropAllViews() $this->connection->select($this->grammar->compileRebuild()); } - /** - * Get all of the table names for the database. - * - * @return array - */ - public function getAllTables() - { - return $this->connection->select( - $this->grammar->compileGetAllTables() - ); - } - - /** - * Get all of the view names for the database. - * - * @return array - */ - public function getAllViews() - { - return $this->connection->select( - $this->grammar->compileGetAllViews() - ); - } - /** * Empty the database file. * diff --git a/src/Illuminate/Database/Schema/SqlServerBuilder.php b/src/Illuminate/Database/Schema/SqlServerBuilder.php index c323e126a6d9..e7717534f803 100644 --- a/src/Illuminate/Database/Schema/SqlServerBuilder.php +++ b/src/Illuminate/Database/Schema/SqlServerBuilder.php @@ -55,6 +55,8 @@ public function dropAllViews() /** * Drop all tables from the database. * + * @deprecated Will be removed in a future Laravel version. + * * @return array */ public function getAllTables() @@ -67,6 +69,8 @@ public function getAllTables() /** * Get all of the view names for the database. * + * @deprecated Will be removed in a future Laravel version. + * * @return array */ public function getAllViews() diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 4a87bcc25cec..ddece45e6e56 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -11,6 +11,8 @@ * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) * @method static bool hasTable(string $table) + * @method static array getTables() + * @method static array getViews() * @method static bool hasColumn(string $table, string $column) * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) @@ -26,7 +28,6 @@ * @method static void dropAllTables() * @method static void dropAllViews() * @method static void dropAllTypes() - * @method static array getAllTables() * @method static void rename(string $from, string $to) * @method static bool enableForeignKeyConstraints() * @method static bool disableForeignKeyConstraints() diff --git a/tests/Database/DatabaseMySQLSchemaBuilderTest.php b/tests/Database/DatabaseMySQLSchemaBuilderTest.php index 2a89d31957c3..53869e6d5604 100755 --- a/tests/Database/DatabaseMySQLSchemaBuilderTest.php +++ b/tests/Database/DatabaseMySQLSchemaBuilderTest.php @@ -20,12 +20,15 @@ public function testHasTable() { $connection = m::mock(Connection::class); $grammar = m::mock(MySqlGrammar::class); + $processor = m::mock(MySqlProcessor::class); $connection->shouldReceive('getDatabaseName')->andReturn('db'); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); $builder = new MySqlBuilder($connection); - $grammar->shouldReceive('compileTableExists')->once()->andReturn('sql'); + $grammar->shouldReceive('compileTables')->once()->andReturn('sql'); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'prefix_table']]); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); - $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql', ['db', 'prefix_table'])->andReturn(['prefix_table']); + $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql')->andReturn([['name' => 'prefix_table']]); $this->assertTrue($builder->hasTable('table')); } diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php index 11c2b9ca41ac..4de2c649ebdd 100644 --- a/tests/Database/DatabasePostgresBuilderTest.php +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -237,12 +237,16 @@ public function testDropAllTablesWhenSearchPathIsString() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileGetAllTables')->with(['public'])->andReturn("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('public')"); - $connection->shouldReceive('select')->with("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('public')")->andReturn([['tablename' => 'users', 'qualifiedname' => '"public"."users"']]); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'public']]); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'public']]); + $grammar->shouldReceive('escapeNames')->with(['public'])->andReturn(['"public"']); $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); - $grammar->shouldReceive('escapeNames')->with(['tablename' => 'users', 'qualifiedname' => '"public"."users"'])->andReturn(['tablename' => '"users"', 'qualifiedname' => '"public"."users"']); - $grammar->shouldReceive('compileDropAllTables')->with(['"public"."users"'])->andReturn('drop table "public"."users" cascade'); + $grammar->shouldReceive('escapeNames')->with(['users', 'public.users'])->andReturn(['"users"', '"public"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['public.users'])->andReturn('drop table "public"."users" cascade'); $connection->shouldReceive('statement')->with('drop table "public"."users" cascade'); $builder = $this->getBuilder($connection); @@ -256,12 +260,17 @@ public function testDropAllTablesWhenSearchPathIsStringOfMany() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('"$user", public, foo_bar-Baz.Áüõß'); $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileGetAllTables')->with(['foouser', 'public', 'foo_bar-Baz.Áüõß'])->andReturn("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','public','foo_bar-Baz.Áüõß')"); - $connection->shouldReceive('select')->with("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','public','foo_bar-Baz.Áüõß')")->andReturn([['tablename' => 'users', 'qualifiedname' => '"foouser"."users"']]); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('escapeNames')->with(['foouser', 'public', 'foo_bar-Baz.Áüõß'])->andReturn(['"foouser"', '"public"', '"foo_bar-Baz"."Áüõß"']); $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); - $grammar->shouldReceive('escapeNames')->with(['tablename' => 'users', 'qualifiedname' => '"foouser"."users"'])->andReturn(['tablename' => '"users"', 'qualifiedname' => '"foouser"."users"']); - $grammar->shouldReceive('compileDropAllTables')->with(['"foouser"."users"'])->andReturn('drop table "foouser"."users" cascade'); + $grammar->shouldReceive('escapeNames')->with(['foouser'])->andReturn(['"foouser"']); + $grammar->shouldReceive('escapeNames')->with(['users', 'foouser.users'])->andReturn(['"users"', '"foouser"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['foouser.users'])->andReturn('drop table "foouser"."users" cascade'); $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); $builder = $this->getBuilder($connection); @@ -280,12 +289,17 @@ public function testDropAllTablesWhenSearchPathIsArrayOfMany() ]); $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileGetAllTables')->with(['foouser', 'dev', 'test', 'spaced schema'])->andReturn("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','dev','test','spaced schema')"); - $connection->shouldReceive('select')->with("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','dev','test','spaced schema')")->andReturn([['tablename' => 'users', 'qualifiedname' => '"foouser"."users"']]); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('escapeNames')->with(['foouser', 'dev', 'test', 'spaced schema'])->andReturn(['"foouser"', '"dev"', '"test"', '"spaced schema"']); $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); - $grammar->shouldReceive('escapeNames')->with(['tablename' => 'users', 'qualifiedname' => '"foouser"."users"'])->andReturn(['tablename' => '"users"', 'qualifiedname' => '"foouser"."users"']); - $grammar->shouldReceive('compileDropAllTables')->with(['"foouser"."users"'])->andReturn('drop table "foouser"."users" cascade'); + $grammar->shouldReceive('escapeNames')->with(['users', 'foouser.users'])->andReturn(['"users"', '"foouser"."users"']); + $grammar->shouldReceive('escapeNames')->with(['foouser'])->andReturn(['"foouser"']); + $grammar->shouldReceive('compileDropAllTables')->with(['foouser.users'])->andReturn('drop table "foouser"."users" cascade'); $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); $builder = $this->getBuilder($connection); diff --git a/tests/Database/DatabaseSchemaBuilderTest.php b/tests/Database/DatabaseSchemaBuilderTest.php index 2fdd98e1225b..e2619c667447 100755 --- a/tests/Database/DatabaseSchemaBuilderTest.php +++ b/tests/Database/DatabaseSchemaBuilderTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Processors\PostgresProcessor; use Illuminate\Database\Schema\Builder; use LogicException; use Mockery as m; @@ -46,11 +47,14 @@ public function testHasTableCorrectlyCallsGrammar() { $connection = m::mock(Connection::class); $grammar = m::mock(stdClass::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); $builder = new Builder($connection); - $grammar->shouldReceive('compileTableExists')->once()->andReturn('sql'); + $grammar->shouldReceive('compileTables')->once()->andReturn('sql'); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'prefix_table']]); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); - $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql', ['prefix_table'])->andReturn(['prefix_table']); + $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql')->andReturn([['name' => 'prefix_table']]); $this->assertTrue($builder->hasTable('table')); } diff --git a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php index 8efa387060ca..99240f81b665 100644 --- a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php +++ b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php @@ -130,6 +130,41 @@ public function testAddTableCommentOnExistingTable() $this->assertEquals('This is a new comment', DB::selectOne("select obj_description('public.posts'::regclass, 'pg_class')")->obj_description); } + public function testGetTables() + { + Schema::create('public.table', function (Blueprint $table) { + $table->string('name'); + }); + + Schema::create('private.table', function (Blueprint $table) { + $table->integer('votes'); + }); + + $tables = Schema::getTables(); + + $this->assertNotEmpty(array_filter($tables, function ($table) { + return $table['name'] === 'table' && $table['schema'] === 'public'; + })); + $this->assertNotEmpty(array_filter($tables, function ($table) { + return $table['name'] === 'table' && $table['schema'] === 'private'; + })); + } + + public function testGetViews() + { + DB::statement('create view public.foo (id) as select 1'); + DB::statement('create view private.foo (id) as select 1'); + + $views = Schema::getViews(); + + $this->assertNotEmpty(array_filter($views, function ($view) { + return $view['name'] === 'foo' && $view['schema'] === 'public'; + })); + $this->assertNotEmpty(array_filter($views, function ($view) { + return $view['name'] === 'foo' && $view['schema'] === 'private'; + })); + } + protected function hasView($schema, $table) { return DB::table('information_schema.views') diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 0a13f5812b18..38f2c34d6cc8 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -137,4 +137,41 @@ public function testChangeTextColumnToTextColumn() $this->assertEquals($expected, $queries); } } + + public function testGetTables() + { + Schema::create('foo', function (Blueprint $table) { + $table->comment('This is a comment'); + $table->increments('id'); + }); + + Schema::create('bar', function (Blueprint $table) { + $table->string('name'); + }); + + Schema::create('baz', function (Blueprint $table) { + $table->integer('votes'); + }); + + $tables = Schema::getTables(); + + $this->assertEmpty(array_diff(['foo', 'bar', 'baz'], array_column($tables, 'name'))); + + if (in_array($this->driver, ['mysql', 'pgsql'])) { + $this->assertNotEmpty(array_filter($tables, function ($table) { + return $table['name'] === 'foo' && $table['comment'] === 'This is a comment'; + })); + } + } + + public function testGetViews() + { + DB::statement('create view foo (id) as select 1'); + DB::statement('create view bar (name) as select 1'); + DB::statement('create view baz (votes) as select 1'); + + $views = Schema::getViews(); + + $this->assertEmpty(array_diff(['foo', 'bar', 'baz'], array_column($views, 'name'))); + } } From e533631dbfe880094bf3d55277eba15844b8fb8c Mon Sep 17 00:00:00 2001 From: Jeffrey Angenent <1571879+devfrey@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:31:58 +0100 Subject: [PATCH 110/207] Fix `MorphTo::associate()` PHPDoc parameter (#49162) --- src/Illuminate/Database/Eloquent/Relations/MorphTo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index 15de0bfe56dd..570287ac0b48 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -226,7 +226,7 @@ protected function matchToMorphParents($type, Collection $results) /** * Associate the model instance to the given parent. * - * @param \Illuminate\Database\Eloquent\Model $model + * @param \Illuminate\Database\Eloquent\Model|null $model * @return \Illuminate\Database\Eloquent\Model */ public function associate($model) From 8ae12c96b18f79f6a0e582e030c06e3bbb5810da Mon Sep 17 00:00:00 2001 From: Noboru Shiroiwa <14008307+nshiro@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:37:57 +0900 Subject: [PATCH 111/207] [10.x] Make test error messages more multi-byte readable (#49160) --- src/Illuminate/Testing/TestResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 80060e6c7f01..2a7945a72585 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -1663,7 +1663,7 @@ protected function appendExceptionToException($exceptionToAppend, $exception) protected function appendErrorsToException($errors, $exception, $json = false) { $errors = $json - ? json_encode($errors, JSON_PRETTY_PRINT) + ? json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : implode(PHP_EOL, Arr::flatten($errors)); // JSON error messages may already contain the errors, so we shouldn't duplicate them... From 48c6e43abc4a3461bef5286298cf8334fbd64970 Mon Sep 17 00:00:00 2001 From: Dave Wood Date: Tue, 28 Nov 2023 14:39:53 +0000 Subject: [PATCH 112/207] Generate a unique hash for anonymous components (#49156) --- .../View/Compilers/Concerns/CompilesComponents.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php index 67b2d3beb0ca..8e986553abb1 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString; use Illuminate\Support\Str; +use Illuminate\View\AnonymousComponent; use Illuminate\View\ComponentAttributeBag; trait CompilesComponents @@ -29,7 +30,9 @@ protected function compileComponent($expression) $component = trim($component, '\'"'); - $hash = static::newComponentHash($component); + $hash = static::newComponentHash( + $component === AnonymousComponent::class ? $component.':'.trim($alias, '\'"') : $component + ); if (Str::contains($component, ['::class', '\\'])) { return static::compileClassComponentOpening($component, $alias, $data, $hash); From 9a65168314f54bb37c3c1f172321425de9bcfdea Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 28 Nov 2023 22:50:41 +0800 Subject: [PATCH 113/207] [10.x] Improves output when using `php artisan about --json` (#49154) * [10.x] Improves output when using `php artisan about --json` Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * 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 * formatting --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Foundation/Console/AboutCommand.php | 70 +++++++++++++------ tests/Foundation/Console/AboutCommandTest.php | 42 +++++++++++ .../Foundation/Console/AboutCommandTest.php | 43 ++++++++++++ 3 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 tests/Foundation/Console/AboutCommandTest.php create mode 100644 tests/Integration/Foundation/Console/AboutCommandTest.php diff --git a/src/Illuminate/Foundation/Console/AboutCommand.php b/src/Illuminate/Foundation/Console/AboutCommand.php index 3b632b3ae36d..3b3c2cd958e8 100644 --- a/src/Illuminate/Foundation/Console/AboutCommand.php +++ b/src/Illuminate/Foundation/Console/AboutCommand.php @@ -2,6 +2,7 @@ namespace Illuminate\Foundation\Console; +use Closure; use Illuminate\Console\Command; use Illuminate\Support\Composer; use Illuminate\Support\Str; @@ -127,7 +128,7 @@ protected function displayDetail($data) $data->pipe(fn ($data) => $section !== 'Environment' ? $data->sort() : $data)->each(function ($detail) { [$label, $value] = $detail; - $this->components->twoColumnDetail($label, value($value)); + $this->components->twoColumnDetail($label, value($value, false)); }); }); } @@ -143,7 +144,7 @@ protected function displayJson($data) $output = $data->flatMap(function ($data, $section) { return [ (string) Str::of($section)->snake() => $data->mapWithKeys(fn ($item, $key) => [ - $this->toSearchKeyword($item[0]) => value($item[1]), + $this->toSearchKeyword($item[0]) => value($item[1], true), ]), ]; }); @@ -158,40 +159,48 @@ protected function displayJson($data) */ protected function gatherApplicationInformation() { + $formatEnabledStatus = fn ($value) => $value ? 'ENABLED' : 'OFF'; + $formatCachedStatus = fn ($value) => $value ? 'CACHED' : 'NOT CACHED'; + static::addToSection('Environment', fn () => [ 'Application Name' => config('app.name'), 'Laravel Version' => $this->laravel->version(), 'PHP Version' => phpversion(), 'Composer Version' => $this->composer->getVersion() ?? '-', 'Environment' => $this->laravel->environment(), - 'Debug Mode' => config('app.debug') ? 'ENABLED' : 'OFF', + 'Debug Mode' => static::format(config('app.debug'), console: $formatEnabledStatus), 'URL' => Str::of(config('app.url'))->replace(['http://', 'https://'], ''), - 'Maintenance Mode' => $this->laravel->isDownForMaintenance() ? 'ENABLED' : 'OFF', + 'Maintenance Mode' => static::format($this->laravel->isDownForMaintenance(), console: $formatEnabledStatus), ]); static::addToSection('Cache', fn () => [ - 'Config' => $this->laravel->configurationIsCached() ? 'CACHED' : 'NOT CACHED', - 'Events' => $this->laravel->eventsAreCached() ? 'CACHED' : 'NOT CACHED', - 'Routes' => $this->laravel->routesAreCached() ? 'CACHED' : 'NOT CACHED', - 'Views' => $this->hasPhpFiles($this->laravel->storagePath('framework/views')) ? 'CACHED' : 'NOT CACHED', + 'Config' => static::format($this->laravel->configurationIsCached(), console: $formatCachedStatus), + 'Events' => static::format($this->laravel->eventsAreCached(), console: $formatCachedStatus), + 'Routes' => static::format($this->laravel->routesAreCached(), console: $formatCachedStatus), + 'Views' => static::format($this->hasPhpFiles($this->laravel->storagePath('framework/views')), console: $formatCachedStatus), ]); - $logChannel = config('logging.default'); - - if (config('logging.channels.'.$logChannel.'.driver') === 'stack') { - $secondary = collect(config('logging.channels.'.$logChannel.'.channels')) - ->implode(', '); - - $logs = ''.$logChannel.' / '.$secondary; - } else { - $logs = $logChannel; - } - static::addToSection('Drivers', fn () => array_filter([ 'Broadcasting' => config('broadcasting.default'), 'Cache' => config('cache.default'), 'Database' => config('database.default'), - 'Logs' => $logs, + 'Logs' => function ($json) { + $logChannel = config('logging.default'); + + if (config('logging.channels.'.$logChannel.'.driver') === 'stack') { + $secondary = collect(config('logging.channels.'.$logChannel.'.channels')); + + return value(static::format( + value: $logChannel, + console: fn ($value) => ''.$value.' / '.$secondary->implode(', '), + json: fn () => $secondary->all(), + ), $json); + } else { + $logs = $logChannel; + } + + return $logs; + }, 'Mail' => config('mail.default'), 'Octane' => config('octane.server'), 'Queue' => config('queue.default'), @@ -260,6 +269,27 @@ protected function sections() ->all(); } + /** + * Materialize a function that formats a given value for CLI or JSON output. + * + * @param mixed $value + * @param (\Closure():(mixed))|null $console + * @param (\Closure():(mixed))|null $json + * @return \Closure(bool):mixed + */ + public static function format($value, Closure $console = null, Closure $json = null) + { + return function ($isJson) use ($value, $console, $json) { + if ($isJson === true && $json instanceof Closure) { + return value($json, $value); + } elseif ($isJson === false && $console instanceof Closure) { + return value($console, $value); + } + + return value($value); + }; + } + /** * Format the given string for searching. * diff --git a/tests/Foundation/Console/AboutCommandTest.php b/tests/Foundation/Console/AboutCommandTest.php new file mode 100644 index 000000000000..cec2ac591cd8 --- /dev/null +++ b/tests/Foundation/Console/AboutCommandTest.php @@ -0,0 +1,42 @@ +assertSame($expected, value($format, false)); + } + + public static function cliDataProvider() + { + yield [AboutCommand::format(true, console: fn ($value) => $value === true ? 'YES' : 'NO'), 'YES']; + yield [AboutCommand::format(false, console: fn ($value) => $value === true ? 'YES' : 'NO'), 'NO']; + } + + /** + * @param \Closure(bool):mixed $format + * @param mixed $expected + */ + #[DataProvider('jsonDataProvider')] + public function testItCanFormatForJsonInterface($format, $expected) + { + $this->assertSame($expected, value($format, true)); + } + + public static function jsonDataProvider() + { + yield [AboutCommand::format(true, json: fn ($value) => $value === true ? 'YES' : 'NO'), 'YES']; + yield [AboutCommand::format(false, json: fn ($value) => $value === true ? 'YES' : 'NO'), 'NO']; + } +} diff --git a/tests/Integration/Foundation/Console/AboutCommandTest.php b/tests/Integration/Foundation/Console/AboutCommandTest.php new file mode 100644 index 000000000000..6795d3bafeb2 --- /dev/null +++ b/tests/Integration/Foundation/Console/AboutCommandTest.php @@ -0,0 +1,43 @@ +mustRun(); + + tap(json_decode($process->getOutput(), true), function ($output) { + Assert::assertArraySubset([ + 'application_name' => 'Laravel', + 'php_version' => PHP_VERSION, + 'environment' => 'testing', + 'debug_mode' => true, + 'url' => 'localhost', + 'maintenance_mode' => false, + ], $output['environment']); + + Assert::assertArraySubset([ + 'config' => false, + 'events' => false, + 'routes' => false, + ], $output['cache']); + + Assert::assertArraySubset([ + 'broadcasting' => 'log', + 'cache' => 'file', + 'database' => 'testing', + 'logs' => ['single'], + 'mail' => 'smtp', + 'queue' => 'sync', + 'session' => 'file', + ], $output['drivers']); + }); + } +} From 2865eba0f884af60751cfb14817f528bae7c2da4 Mon Sep 17 00:00:00 2001 From: Orkhan Ahmadov Date: Tue, 28 Nov 2023 15:53:05 +0100 Subject: [PATCH 114/207] [10.x] Make fake instance inherit from `Vite` when using `withoutVite()` (#49150) * Extend from `Vite` * Fix tests --- .../Concerns/InteractsWithContainer.php | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index 49b688cf200f..7deafc636fde 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -113,14 +113,14 @@ protected function withoutVite() Facade::clearResolvedInstance(Vite::class); - $this->swap(Vite::class, new class + $this->swap(Vite::class, new class extends Vite { - public function __invoke() + public function __invoke($entrypoints, $buildDirectory = null) { return ''; } - public function __call($name, $arguments) + public function __call($method, $parameters) { return ''; } @@ -130,32 +130,32 @@ public function __toString() return ''; } - public function useIntegrityKey() + public function useIntegrityKey($key) { return $this; } - public function useBuildDirectory() + public function useBuildDirectory($path) { return $this; } - public function useHotFile() + public function useHotFile($path) { return $this; } - public function withEntryPoints() + public function withEntryPoints($entryPoints) { return $this; } - public function useScriptTagAttributes() + public function useScriptTagAttributes($attributes) { return $this; } - public function useStyleTagAttributes() + public function useStyleTagAttributes($attributes) { return $this; } @@ -164,6 +164,16 @@ public function preloadedAssets() { return []; } + + public function reactRefresh() + { + return ''; + } + + public function asset($asset, $buildDirectory = null) + { + return ''; + } }); return $this; From 92b78fdd1f386425a88f443a728efd176c666244 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 28 Nov 2023 09:01:38 -0600 Subject: [PATCH 115/207] version --- 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 1a87faace3f2..5de3243c9bc6 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.33.0'; + const VERSION = '10.34.0'; /** * The base path for the Laravel installation. From 77da0a7bfedbb2272a52df3d0891ed966575eb43 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 28 Nov 2023 15:16:05 +0000 Subject: [PATCH 116/207] Update CHANGELOG --- CHANGELOG.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9fe5be197a3..5355399203f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,33 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.33.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.34.0...10.x) + +## [v10.34.0](https://github.com/laravel/framework/compare/v10.33.0...v10.34.0) - 2023-11-28 + +* [10.x] Fix `hex_color` validation rule by [@apih](https://github.com/apih) in https://github.com/laravel/framework/pull/49070 +* [10.x] Prevent passing null to base64_decode in Encrypter by [@robtesch](https://github.com/robtesch) in https://github.com/laravel/framework/pull/49071 +* [10.x] Alias Number class by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/49073 +* [10.x] Added File Validation `extensions` by [@eusonlito](https://github.com/eusonlito) in https://github.com/laravel/framework/pull/49082 +* [10.x] Add [@throws](https://github.com/throws) in doc-blocks by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/49091 +* [10.x] Update docblocks for consistency by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/49092 +* [10.x] Throw exception when trying to initiate `Collection` using `WeakMap` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49095 +* [10.x] Only stage committed transactions by [@hansnn](https://github.com/hansnn) in https://github.com/laravel/framework/pull/49093 +* Better transaction manager object design by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/49103 +* [10.x] use php 8.3 `mb_str_pad()` for `Str::pad*` by [@amacado](https://github.com/amacado) in https://github.com/laravel/framework/pull/49108 +* [10.x] Add Conditionable to TestResponse by [@nshiro](https://github.com/nshiro) in https://github.com/laravel/framework/pull/49112 +* [10.x] Allow multiple types in Collection's `ensure` method by [@ash-jc-allen](https://github.com/ash-jc-allen) in https://github.com/laravel/framework/pull/49127 +* [10.x] Fix middleware "SetCacheHeaders" with download responses by [@clementbirkle](https://github.com/clementbirkle) in https://github.com/laravel/framework/pull/49138 +* [10.x][Cache] Fix handling of `false` values in apc by [@simivar](https://github.com/simivar) in https://github.com/laravel/framework/pull/49145 +* [10.x] Reset numeric rules after each attribute's validation by [@apih](https://github.com/apih) in https://github.com/laravel/framework/pull/49142 +* [10.x] Extract dirty getter for `performUpdate` by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/49141 +* [10.x] `ensure`: Resolve `$itemType` outside the closure by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/49137 +* Allow "missing" method to be used on route groups by [@redelschaap](https://github.com/redelschaap) in https://github.com/laravel/framework/pull/49144 +* [10.x] Get tables and views info by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49020 +* [10.x] Fix `MorphTo::associate()` PHPDoc parameter by [@devfrey](https://github.com/devfrey) in https://github.com/laravel/framework/pull/49162 +* [10.x] Make test error messages more multi-byte readable by [@nshiro](https://github.com/nshiro) in https://github.com/laravel/framework/pull/49160 +* [10.x] Generate a unique hash for anonymous components by [@billyonecan](https://github.com/billyonecan) in https://github.com/laravel/framework/pull/49156 +* [10.x] Improves output when using `php artisan about --json` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49154 +* [10.x] Make fake instance inherit from `Vite` when using `withoutVite()` by [@orkhanahmadov](https://github.com/orkhanahmadov) in https://github.com/laravel/framework/pull/49150 ## [v10.33.0](https://github.com/laravel/framework/compare/v10.32.1...v10.33.0) - 2023-11-21 From 1e03a4a9029513c359b44fd6150d7ef2b772da49 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 28 Nov 2023 23:22:12 +0800 Subject: [PATCH 117/207] [10.x] Streamline `DatabaseMigrations` and `RefreshDatabase` events (#49153) * [10.x] Test Improvements Use available method from `RefreshDatabase::afterRefreshingDatabase()` Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki --- .../Foundation/Testing/DatabaseMigrations.php | 38 +++++++++++++++++-- .../Database/DatabaseCustomCastsTest.php | 2 +- .../DatabaseEloquentBroadcastingTest.php | 2 +- ...abaseEloquentModelAttributeCastingTest.php | 2 +- ...DatabaseEloquentModelCustomCastingTest.php | 2 +- .../Database/EloquentAggregateTest.php | 2 +- .../Database/EloquentBelongsToManyTest.php | 2 +- .../Database/EloquentBelongsToTest.php | 2 +- .../Database/EloquentCollectionFreshTest.php | 2 +- .../EloquentCollectionLoadCountTest.php | 2 +- .../EloquentCollectionLoadMissingTest.php | 2 +- .../Database/EloquentCursorPaginateTest.php | 2 +- .../Database/EloquentCustomPivotCastTest.php | 2 +- .../Database/EloquentDeleteTest.php | 2 +- .../Database/EloquentHasManyTest.php | 2 +- .../Database/EloquentHasManyThroughTest.php | 2 +- .../Database/EloquentHasOneIsTest.php | 2 +- .../Database/EloquentHasOneOfManyTest.php | 2 +- .../Database/EloquentLazyEagerLoadingTest.php | 2 +- .../Database/EloquentMassPrunableTest.php | 2 +- .../EloquentModelCustomEventsTest.php | 2 +- .../Database/EloquentModelDateCastingTest.php | 2 +- .../EloquentModelDecimalCastingTest.php | 2 +- .../EloquentModelEncryptedCastingTest.php | 2 +- .../Database/EloquentModelEnumCastingTest.php | 2 +- .../EloquentModelHashedCastingTest.php | 2 +- .../EloquentModelImmutableDateCastingTest.php | 2 +- .../Database/EloquentModelJsonCastingTest.php | 2 +- .../Database/EloquentModelLoadCountTest.php | 2 +- .../Database/EloquentModelLoadMissingTest.php | 2 +- .../Database/EloquentModelRefreshTest.php | 2 +- .../EloquentModelStringCastingTest.php | 2 +- .../Database/EloquentModelTest.php | 2 +- .../EloquentModelWithoutEventsTest.php | 2 +- .../Database/EloquentMorphConstrainTest.php | 2 +- .../EloquentMorphCountEagerLoadingTest.php | 2 +- ...EloquentMorphCountLazyEagerLoadingTest.php | 2 +- .../EloquentMorphEagerLoadingTest.php | 2 +- .../EloquentMorphLazyEagerLoadingTest.php | 2 +- .../Database/EloquentMorphManyTest.php | 2 +- .../Database/EloquentMorphOneIsTest.php | 2 +- .../EloquentMorphToGlobalScopesTest.php | 2 +- .../Database/EloquentMorphToIsTest.php | 2 +- .../EloquentMorphToLazyEagerLoadingTest.php | 2 +- .../Database/EloquentMorphToSelectTest.php | 2 +- .../Database/EloquentMorphToTouchesTest.php | 2 +- ...tMultiDimensionalArrayEagerLoadingTest.php | 2 +- .../Database/EloquentPaginateTest.php | 2 +- .../Database/EloquentPivotEventsTest.php | 2 +- .../EloquentPivotSerializationTest.php | 2 +- .../Database/EloquentPivotTest.php | 2 +- .../Database/EloquentPrunableTest.php | 2 +- .../Integration/Database/EloquentPushTest.php | 2 +- .../Database/EloquentStrictLoadingTest.php | 2 +- ...EloquentTouchParentWithGlobalScopeTest.php | 2 +- .../EloquentUniqueStringPrimaryKeysTest.php | 2 +- .../Database/EloquentUpdateTest.php | 2 +- .../Database/EloquentWhereHasMorphTest.php | 2 +- .../Database/EloquentWhereHasTest.php | 2 +- .../Database/EloquentWhereTest.php | 2 +- .../Database/EloquentWithCountTest.php | 2 +- .../DatabaseEloquentMySqlIntegrationTest.php | 2 +- .../MySql/DatabaseMySqlConnectionTest.php | 2 +- ...SqlSchemaBuilderAlterTableWithEnumTest.php | 2 +- .../Database/MySql/EloquentCastTest.php | 2 +- .../Database/MySql/FulltextTest.php | 2 +- ...atabaseEloquentPostgresIntegrationTest.php | 2 +- .../DatabasePostgresConnectionTest.php | 2 +- .../Database/Postgres/FulltextTest.php | 2 +- .../Integration/Database/QueryBuilderTest.php | 2 +- .../Database/QueryingWithEnumsTest.php | 2 +- ...tabaseEloquentSqlServerIntegrationTest.php | 2 +- .../DatabaseSqlServerConnectionTest.php | 2 +- .../DatabaseSqlServerSchemaBuilderTest.php | 2 +- .../Sqlite/DatabaseSqliteConnectionTest.php | 2 +- .../DatabaseSqliteSchemaBuilderTest.php | 2 +- 76 files changed, 110 insertions(+), 78 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseMigrations.php b/src/Illuminate/Foundation/Testing/DatabaseMigrations.php index 10a3a7300af6..4301c24ad473 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseMigrations.php +++ b/src/Illuminate/Foundation/Testing/DatabaseMigrations.php @@ -16,9 +16,9 @@ trait DatabaseMigrations */ public function runDatabaseMigrations() { - $this->artisan('migrate:fresh', $this->migrateFreshUsing()); - - $this->app[Kernel::class]->setArtisan(null); + $this->beforeRefreshingDatabase(); + $this->refreshTestDatabase(); + $this->afterRefreshingDatabase(); $this->beforeApplicationDestroyed(function () { $this->artisan('migrate:rollback'); @@ -26,4 +26,36 @@ public function runDatabaseMigrations() RefreshDatabaseState::$migrated = false; }); } + + /** + * Refresh a conventional test database. + * + * @return void + */ + protected function refreshTestDatabase() + { + $this->artisan('migrate:fresh', $this->migrateFreshUsing()); + + $this->app[Kernel::class]->setArtisan(null); + } + + /** + * Perform any work that should take place before the database has started refreshing. + * + * @return void + */ + protected function beforeRefreshingDatabase() + { + // ... + } + + /** + * Perform any work that should take place once the database has finished refreshing. + * + * @return void + */ + protected function afterRefreshingDatabase() + { + // ... + } } diff --git a/tests/Integration/Database/DatabaseCustomCastsTest.php b/tests/Integration/Database/DatabaseCustomCastsTest.php index d38936cf7371..044f6ef11f84 100644 --- a/tests/Integration/Database/DatabaseCustomCastsTest.php +++ b/tests/Integration/Database/DatabaseCustomCastsTest.php @@ -13,7 +13,7 @@ class DatabaseCustomCastsTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_eloquent_model_with_custom_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php b/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php index 2f9f399d8bd0..f3fb028e7569 100644 --- a/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentBroadcastingTest.php @@ -18,7 +18,7 @@ class DatabaseEloquentBroadcastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_eloquent_broadcasting_users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php index a6263a096e91..060031684a0b 100644 --- a/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelAttributeCastingTest.php @@ -12,7 +12,7 @@ class DatabaseEloquentModelAttributeCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_eloquent_model_with_custom_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php index 2d578ab3de61..8bbdb68c0271 100644 --- a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php @@ -16,7 +16,7 @@ class DatabaseEloquentModelCustomCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_eloquent_model_with_custom_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentAggregateTest.php b/tests/Integration/Database/EloquentAggregateTest.php index 9d6206c810ce..4c0deb22e00c 100644 --- a/tests/Integration/Database/EloquentAggregateTest.php +++ b/tests/Integration/Database/EloquentAggregateTest.php @@ -8,7 +8,7 @@ class EloquentAggregateTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index 23e6eb518bb7..161fd4908376 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -22,7 +22,7 @@ protected function tearDown(): void Carbon::setTestNow(null); } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentBelongsToTest.php b/tests/Integration/Database/EloquentBelongsToTest.php index 984939fdad5b..13492efefb6c 100644 --- a/tests/Integration/Database/EloquentBelongsToTest.php +++ b/tests/Integration/Database/EloquentBelongsToTest.php @@ -10,7 +10,7 @@ class EloquentBelongsToTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentCollectionFreshTest.php b/tests/Integration/Database/EloquentCollectionFreshTest.php index 30acc2597112..23d50a980e3e 100644 --- a/tests/Integration/Database/EloquentCollectionFreshTest.php +++ b/tests/Integration/Database/EloquentCollectionFreshTest.php @@ -9,7 +9,7 @@ class EloquentCollectionFreshTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentCollectionLoadCountTest.php b/tests/Integration/Database/EloquentCollectionLoadCountTest.php index eea30996db73..d6734336e34c 100644 --- a/tests/Integration/Database/EloquentCollectionLoadCountTest.php +++ b/tests/Integration/Database/EloquentCollectionLoadCountTest.php @@ -12,7 +12,7 @@ class EloquentCollectionLoadCountTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentCollectionLoadMissingTest.php b/tests/Integration/Database/EloquentCollectionLoadMissingTest.php index 31e101afec1d..e95d7aca9b41 100644 --- a/tests/Integration/Database/EloquentCollectionLoadMissingTest.php +++ b/tests/Integration/Database/EloquentCollectionLoadMissingTest.php @@ -10,7 +10,7 @@ class EloquentCollectionLoadMissingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentCursorPaginateTest.php b/tests/Integration/Database/EloquentCursorPaginateTest.php index 76a7a806acd9..01fd4b49e8b8 100644 --- a/tests/Integration/Database/EloquentCursorPaginateTest.php +++ b/tests/Integration/Database/EloquentCursorPaginateTest.php @@ -10,7 +10,7 @@ class EloquentCursorPaginateTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentCustomPivotCastTest.php b/tests/Integration/Database/EloquentCustomPivotCastTest.php index 1a116d37eb9d..b5010ad130ff 100644 --- a/tests/Integration/Database/EloquentCustomPivotCastTest.php +++ b/tests/Integration/Database/EloquentCustomPivotCastTest.php @@ -9,7 +9,7 @@ class EloquentCustomPivotCastTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentDeleteTest.php b/tests/Integration/Database/EloquentDeleteTest.php index 07d7ec3903a6..bd0880767a52 100644 --- a/tests/Integration/Database/EloquentDeleteTest.php +++ b/tests/Integration/Database/EloquentDeleteTest.php @@ -11,7 +11,7 @@ class EloquentDeleteTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentHasManyTest.php b/tests/Integration/Database/EloquentHasManyTest.php index ec782d770830..b8add9b8a72e 100644 --- a/tests/Integration/Database/EloquentHasManyTest.php +++ b/tests/Integration/Database/EloquentHasManyTest.php @@ -11,7 +11,7 @@ class EloquentHasManyTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('eloquent_has_many_test_users', function ($table) { $table->id(); diff --git a/tests/Integration/Database/EloquentHasManyThroughTest.php b/tests/Integration/Database/EloquentHasManyThroughTest.php index 608a21126289..6dcdb9b8ae90 100644 --- a/tests/Integration/Database/EloquentHasManyThroughTest.php +++ b/tests/Integration/Database/EloquentHasManyThroughTest.php @@ -12,7 +12,7 @@ class EloquentHasManyThroughTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentHasOneIsTest.php b/tests/Integration/Database/EloquentHasOneIsTest.php index b61dbca0f796..e0672dcd8f73 100644 --- a/tests/Integration/Database/EloquentHasOneIsTest.php +++ b/tests/Integration/Database/EloquentHasOneIsTest.php @@ -9,7 +9,7 @@ class EloquentHasOneIsTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php index 4be71d3e5da7..c7a1af45dd53 100644 --- a/tests/Integration/Database/EloquentHasOneOfManyTest.php +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -10,7 +10,7 @@ class EloquentHasOneOfManyTest extends DatabaseTestCase { public $retrievedLogins; - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function ($table) { $table->id(); diff --git a/tests/Integration/Database/EloquentLazyEagerLoadingTest.php b/tests/Integration/Database/EloquentLazyEagerLoadingTest.php index dc6422daa3a4..3ab54c6ceae5 100644 --- a/tests/Integration/Database/EloquentLazyEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentLazyEagerLoadingTest.php @@ -10,7 +10,7 @@ class EloquentLazyEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('one', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMassPrunableTest.php b/tests/Integration/Database/EloquentMassPrunableTest.php index 990bd2191482..e3e1c0c36f34 100644 --- a/tests/Integration/Database/EloquentMassPrunableTest.php +++ b/tests/Integration/Database/EloquentMassPrunableTest.php @@ -28,7 +28,7 @@ protected function setUp(): void $container->alias(Dispatcher::class, 'events'); } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { collect([ 'mass_prunable_test_models', diff --git a/tests/Integration/Database/EloquentModelCustomEventsTest.php b/tests/Integration/Database/EloquentModelCustomEventsTest.php index f214ec40142b..6a85e3379ca4 100644 --- a/tests/Integration/Database/EloquentModelCustomEventsTest.php +++ b/tests/Integration/Database/EloquentModelCustomEventsTest.php @@ -19,7 +19,7 @@ protected function setUp(): void }); } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model1', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelDateCastingTest.php b/tests/Integration/Database/EloquentModelDateCastingTest.php index 71ce224bedc5..1dfa956ae34a 100644 --- a/tests/Integration/Database/EloquentModelDateCastingTest.php +++ b/tests/Integration/Database/EloquentModelDateCastingTest.php @@ -11,7 +11,7 @@ class EloquentModelDateCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model1', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelDecimalCastingTest.php b/tests/Integration/Database/EloquentModelDecimalCastingTest.php index 5b08124caaa0..105b328b0020 100644 --- a/tests/Integration/Database/EloquentModelDecimalCastingTest.php +++ b/tests/Integration/Database/EloquentModelDecimalCastingTest.php @@ -11,7 +11,7 @@ class EloquentModelDecimalCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model1', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php index cc9e087cede2..f4a00d3c98ca 100644 --- a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php +++ b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php @@ -27,7 +27,7 @@ protected function setUp(): void Model::$encrypter = null; } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('encrypted_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index 411a55d11acb..e948703f3e88 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -13,7 +13,7 @@ class EloquentModelEnumCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('enum_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelHashedCastingTest.php b/tests/Integration/Database/EloquentModelHashedCastingTest.php index 754d66b9a399..9d7bdba74ad7 100644 --- a/tests/Integration/Database/EloquentModelHashedCastingTest.php +++ b/tests/Integration/Database/EloquentModelHashedCastingTest.php @@ -10,7 +10,7 @@ class EloquentModelHashedCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('hashed_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php index 5d6a46865a64..5cdff915e94a 100644 --- a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php +++ b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php @@ -10,7 +10,7 @@ class EloquentModelImmutableDateCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model_immutable', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelJsonCastingTest.php b/tests/Integration/Database/EloquentModelJsonCastingTest.php index b81660aa818e..d7bcecf6bc58 100644 --- a/tests/Integration/Database/EloquentModelJsonCastingTest.php +++ b/tests/Integration/Database/EloquentModelJsonCastingTest.php @@ -11,7 +11,7 @@ class EloquentModelJsonCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('json_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelLoadCountTest.php b/tests/Integration/Database/EloquentModelLoadCountTest.php index 121a729f6d03..1024e214b99c 100644 --- a/tests/Integration/Database/EloquentModelLoadCountTest.php +++ b/tests/Integration/Database/EloquentModelLoadCountTest.php @@ -11,7 +11,7 @@ class EloquentModelLoadCountTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('base_models', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelLoadMissingTest.php b/tests/Integration/Database/EloquentModelLoadMissingTest.php index d68f3b4a03a3..eb4983b432a1 100644 --- a/tests/Integration/Database/EloquentModelLoadMissingTest.php +++ b/tests/Integration/Database/EloquentModelLoadMissingTest.php @@ -10,7 +10,7 @@ class EloquentModelLoadMissingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelRefreshTest.php b/tests/Integration/Database/EloquentModelRefreshTest.php index e51b12bdab3a..4bac91dbfe32 100644 --- a/tests/Integration/Database/EloquentModelRefreshTest.php +++ b/tests/Integration/Database/EloquentModelRefreshTest.php @@ -11,7 +11,7 @@ class EloquentModelRefreshTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelStringCastingTest.php b/tests/Integration/Database/EloquentModelStringCastingTest.php index 7ba208f37498..1b173ccd5df0 100644 --- a/tests/Integration/Database/EloquentModelStringCastingTest.php +++ b/tests/Integration/Database/EloquentModelStringCastingTest.php @@ -9,7 +9,7 @@ class EloquentModelStringCastingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('casting_table', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index 937a8a7b55c8..d4ad6c207437 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -10,7 +10,7 @@ class EloquentModelTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model1', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentModelWithoutEventsTest.php b/tests/Integration/Database/EloquentModelWithoutEventsTest.php index 07c4e68137ba..b5bc8725aed4 100644 --- a/tests/Integration/Database/EloquentModelWithoutEventsTest.php +++ b/tests/Integration/Database/EloquentModelWithoutEventsTest.php @@ -8,7 +8,7 @@ class EloquentModelWithoutEventsTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('auto_filled_models', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphConstrainTest.php b/tests/Integration/Database/EloquentMorphConstrainTest.php index 0f244b4d096b..b00d39230966 100644 --- a/tests/Integration/Database/EloquentMorphConstrainTest.php +++ b/tests/Integration/Database/EloquentMorphConstrainTest.php @@ -10,7 +10,7 @@ class EloquentMorphConstrainTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphCountEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphCountEagerLoadingTest.php index 2692e23f001c..f16bb18fac23 100644 --- a/tests/Integration/Database/EloquentMorphCountEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphCountEagerLoadingTest.php @@ -10,7 +10,7 @@ class EloquentMorphCountEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('likes', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphCountLazyEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphCountLazyEagerLoadingTest.php index 5ea6faf4621a..6e9640d0bc0f 100644 --- a/tests/Integration/Database/EloquentMorphCountLazyEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphCountLazyEagerLoadingTest.php @@ -9,7 +9,7 @@ class EloquentMorphCountLazyEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('likes', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphEagerLoadingTest.php index 058f981b4b1a..c251f0c105c7 100644 --- a/tests/Integration/Database/EloquentMorphEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphEagerLoadingTest.php @@ -11,7 +11,7 @@ class EloquentMorphEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphLazyEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphLazyEagerLoadingTest.php index 802083ae5c18..d7034e222b06 100644 --- a/tests/Integration/Database/EloquentMorphLazyEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphLazyEagerLoadingTest.php @@ -9,7 +9,7 @@ class EloquentMorphLazyEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphManyTest.php b/tests/Integration/Database/EloquentMorphManyTest.php index ba5b1a589887..b2dff448bba2 100644 --- a/tests/Integration/Database/EloquentMorphManyTest.php +++ b/tests/Integration/Database/EloquentMorphManyTest.php @@ -12,7 +12,7 @@ class EloquentMorphManyTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphOneIsTest.php b/tests/Integration/Database/EloquentMorphOneIsTest.php index c28699a6382e..f15e81aa5fda 100644 --- a/tests/Integration/Database/EloquentMorphOneIsTest.php +++ b/tests/Integration/Database/EloquentMorphOneIsTest.php @@ -9,7 +9,7 @@ class EloquentMorphOneIsTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphToGlobalScopesTest.php b/tests/Integration/Database/EloquentMorphToGlobalScopesTest.php index 6987d55f891e..34071c044627 100644 --- a/tests/Integration/Database/EloquentMorphToGlobalScopesTest.php +++ b/tests/Integration/Database/EloquentMorphToGlobalScopesTest.php @@ -11,7 +11,7 @@ class EloquentMorphToGlobalScopesTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphToIsTest.php b/tests/Integration/Database/EloquentMorphToIsTest.php index 41f80b82879f..152ec850a8a6 100644 --- a/tests/Integration/Database/EloquentMorphToIsTest.php +++ b/tests/Integration/Database/EloquentMorphToIsTest.php @@ -9,7 +9,7 @@ class EloquentMorphToIsTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphToLazyEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphToLazyEagerLoadingTest.php index b6727024853c..ca28006f74db 100644 --- a/tests/Integration/Database/EloquentMorphToLazyEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphToLazyEagerLoadingTest.php @@ -10,7 +10,7 @@ class EloquentMorphToLazyEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphToSelectTest.php b/tests/Integration/Database/EloquentMorphToSelectTest.php index 6b1b736aea7e..3a4cc258e99c 100644 --- a/tests/Integration/Database/EloquentMorphToSelectTest.php +++ b/tests/Integration/Database/EloquentMorphToSelectTest.php @@ -9,7 +9,7 @@ class EloquentMorphToSelectTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMorphToTouchesTest.php b/tests/Integration/Database/EloquentMorphToTouchesTest.php index a4b4211e62cf..f34f52093e2f 100644 --- a/tests/Integration/Database/EloquentMorphToTouchesTest.php +++ b/tests/Integration/Database/EloquentMorphToTouchesTest.php @@ -10,7 +10,7 @@ class EloquentMorphToTouchesTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentMultiDimensionalArrayEagerLoadingTest.php b/tests/Integration/Database/EloquentMultiDimensionalArrayEagerLoadingTest.php index e18aee10cc66..0f53b3c5e48e 100644 --- a/tests/Integration/Database/EloquentMultiDimensionalArrayEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMultiDimensionalArrayEagerLoadingTest.php @@ -10,7 +10,7 @@ class EloquentMultiDimensionalArrayEagerLoadingTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentPaginateTest.php b/tests/Integration/Database/EloquentPaginateTest.php index fa7768185e3c..2beade1644dd 100644 --- a/tests/Integration/Database/EloquentPaginateTest.php +++ b/tests/Integration/Database/EloquentPaginateTest.php @@ -8,7 +8,7 @@ class EloquentPaginateTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index e5463c7cecfc..7a9962982595 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -18,7 +18,7 @@ protected function setUp(): void PivotEventsTestCollaborator::$eventsCalled = []; } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentPivotSerializationTest.php b/tests/Integration/Database/EloquentPivotSerializationTest.php index 00899762b7a0..f308072f6004 100644 --- a/tests/Integration/Database/EloquentPivotSerializationTest.php +++ b/tests/Integration/Database/EloquentPivotSerializationTest.php @@ -12,7 +12,7 @@ class EloquentPivotSerializationTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentPivotTest.php b/tests/Integration/Database/EloquentPivotTest.php index 72bd8a687fdb..2757992f6927 100644 --- a/tests/Integration/Database/EloquentPivotTest.php +++ b/tests/Integration/Database/EloquentPivotTest.php @@ -9,7 +9,7 @@ class EloquentPivotTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentPrunableTest.php b/tests/Integration/Database/EloquentPrunableTest.php index b51c9f1cc350..22f48e9464fc 100644 --- a/tests/Integration/Database/EloquentPrunableTest.php +++ b/tests/Integration/Database/EloquentPrunableTest.php @@ -13,7 +13,7 @@ class EloquentPrunableTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { collect([ 'prunable_test_models', diff --git a/tests/Integration/Database/EloquentPushTest.php b/tests/Integration/Database/EloquentPushTest.php index 090d3bb6c63b..d1406d321b70 100644 --- a/tests/Integration/Database/EloquentPushTest.php +++ b/tests/Integration/Database/EloquentPushTest.php @@ -8,7 +8,7 @@ class EloquentPushTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentStrictLoadingTest.php b/tests/Integration/Database/EloquentStrictLoadingTest.php index d73a1fde0e43..f13f937aa09f 100644 --- a/tests/Integration/Database/EloquentStrictLoadingTest.php +++ b/tests/Integration/Database/EloquentStrictLoadingTest.php @@ -19,7 +19,7 @@ protected function setUp(): void Model::preventLazyLoading(); } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model1', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentTouchParentWithGlobalScopeTest.php b/tests/Integration/Database/EloquentTouchParentWithGlobalScopeTest.php index 551e568786d0..e29cabb591ee 100644 --- a/tests/Integration/Database/EloquentTouchParentWithGlobalScopeTest.php +++ b/tests/Integration/Database/EloquentTouchParentWithGlobalScopeTest.php @@ -10,7 +10,7 @@ class EloquentTouchParentWithGlobalScopeTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php b/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php index 755152bd6aed..83724b65267d 100644 --- a/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php +++ b/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php @@ -11,7 +11,7 @@ class EloquentUniqueStringPrimaryKeysTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->uuid('id')->primary(); diff --git a/tests/Integration/Database/EloquentUpdateTest.php b/tests/Integration/Database/EloquentUpdateTest.php index d229b129b8a6..68fdc26993a2 100644 --- a/tests/Integration/Database/EloquentUpdateTest.php +++ b/tests/Integration/Database/EloquentUpdateTest.php @@ -10,7 +10,7 @@ class EloquentUpdateTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('test_model1', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentWhereHasMorphTest.php b/tests/Integration/Database/EloquentWhereHasMorphTest.php index 73dffc85cb5f..d319317aaa63 100644 --- a/tests/Integration/Database/EloquentWhereHasMorphTest.php +++ b/tests/Integration/Database/EloquentWhereHasMorphTest.php @@ -12,7 +12,7 @@ class EloquentWhereHasMorphTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentWhereHasTest.php b/tests/Integration/Database/EloquentWhereHasTest.php index 22cfaf5d5a75..9a9d1e03ac70 100644 --- a/tests/Integration/Database/EloquentWhereHasTest.php +++ b/tests/Integration/Database/EloquentWhereHasTest.php @@ -12,7 +12,7 @@ class EloquentWhereHasTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentWhereTest.php b/tests/Integration/Database/EloquentWhereTest.php index 7f3314415c30..ff6c66595086 100644 --- a/tests/Integration/Database/EloquentWhereTest.php +++ b/tests/Integration/Database/EloquentWhereTest.php @@ -12,7 +12,7 @@ class EloquentWhereTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/EloquentWithCountTest.php b/tests/Integration/Database/EloquentWithCountTest.php index 5174177ab0c5..a323652c2b0e 100644 --- a/tests/Integration/Database/EloquentWithCountTest.php +++ b/tests/Integration/Database/EloquentWithCountTest.php @@ -9,7 +9,7 @@ class EloquentWithCountTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('one', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/MySql/DatabaseEloquentMySqlIntegrationTest.php b/tests/Integration/Database/MySql/DatabaseEloquentMySqlIntegrationTest.php index f7eaccdb274b..06d20b57d673 100644 --- a/tests/Integration/Database/MySql/DatabaseEloquentMySqlIntegrationTest.php +++ b/tests/Integration/Database/MySql/DatabaseEloquentMySqlIntegrationTest.php @@ -9,7 +9,7 @@ class DatabaseEloquentMySqlIntegrationTest extends MySqlTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable('database_eloquent_mysql_integration_users')) { Schema::create('database_eloquent_mysql_integration_users', function (Blueprint $table) { diff --git a/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php b/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php index a365157cf924..16b067eac3ca 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php @@ -17,7 +17,7 @@ class DatabaseMySqlConnectionTest extends MySqlTestCase const JSON_COL = 'json_col'; const FLOAT_VAL = 0.2; - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable(self::TABLE)) { Schema::create(self::TABLE, function (Blueprint $table) { diff --git a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php index 18b156dc98da..7c2ed52652c2 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php @@ -12,7 +12,7 @@ */ class DatabaseMySqlSchemaBuilderAlterTableWithEnumTest extends MySqlTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->integer('id'); diff --git a/tests/Integration/Database/MySql/EloquentCastTest.php b/tests/Integration/Database/MySql/EloquentCastTest.php index 651a1e6711da..c5a67bf7e937 100644 --- a/tests/Integration/Database/MySql/EloquentCastTest.php +++ b/tests/Integration/Database/MySql/EloquentCastTest.php @@ -12,7 +12,7 @@ class EloquentCastTest extends MySqlTestCase { protected $driver = 'mysql'; - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function ($table) { $table->increments('id'); diff --git a/tests/Integration/Database/MySql/FulltextTest.php b/tests/Integration/Database/MySql/FulltextTest.php index a98d0c74b48a..540840953218 100644 --- a/tests/Integration/Database/MySql/FulltextTest.php +++ b/tests/Integration/Database/MySql/FulltextTest.php @@ -12,7 +12,7 @@ */ class FulltextTest extends MySqlTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('articles', function (Blueprint $table) { $table->id('id'); diff --git a/tests/Integration/Database/Postgres/DatabaseEloquentPostgresIntegrationTest.php b/tests/Integration/Database/Postgres/DatabaseEloquentPostgresIntegrationTest.php index d95dca61a0cb..87e0488ea233 100644 --- a/tests/Integration/Database/Postgres/DatabaseEloquentPostgresIntegrationTest.php +++ b/tests/Integration/Database/Postgres/DatabaseEloquentPostgresIntegrationTest.php @@ -9,7 +9,7 @@ class DatabaseEloquentPostgresIntegrationTest extends PostgresTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable('database_eloquent_postgres_integration_users')) { Schema::create('database_eloquent_postgres_integration_users', function (Blueprint $table) { diff --git a/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php b/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php index 41c238787712..408b79dfe09a 100644 --- a/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php +++ b/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php @@ -12,7 +12,7 @@ */ class DatabasePostgresConnectionTest extends PostgresTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable('json_table')) { Schema::create('json_table', function (Blueprint $table) { diff --git a/tests/Integration/Database/Postgres/FulltextTest.php b/tests/Integration/Database/Postgres/FulltextTest.php index 39ddb6837022..1315e101de65 100644 --- a/tests/Integration/Database/Postgres/FulltextTest.php +++ b/tests/Integration/Database/Postgres/FulltextTest.php @@ -12,7 +12,7 @@ */ class FulltextTest extends PostgresTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('articles', function (Blueprint $table) { $table->id('id'); diff --git a/tests/Integration/Database/QueryBuilderTest.php b/tests/Integration/Database/QueryBuilderTest.php index a7f32f42d1e6..98ae0792b212 100644 --- a/tests/Integration/Database/QueryBuilderTest.php +++ b/tests/Integration/Database/QueryBuilderTest.php @@ -12,7 +12,7 @@ class QueryBuilderTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/QueryingWithEnumsTest.php b/tests/Integration/Database/QueryingWithEnumsTest.php index bd528584e5df..923eba19eb65 100644 --- a/tests/Integration/Database/QueryingWithEnumsTest.php +++ b/tests/Integration/Database/QueryingWithEnumsTest.php @@ -10,7 +10,7 @@ class QueryingWithEnumsTest extends DatabaseTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('enum_casts', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Integration/Database/SqlServer/DatabaseEloquentSqlServerIntegrationTest.php b/tests/Integration/Database/SqlServer/DatabaseEloquentSqlServerIntegrationTest.php index 9098e6608c6c..1a0ccace294a 100644 --- a/tests/Integration/Database/SqlServer/DatabaseEloquentSqlServerIntegrationTest.php +++ b/tests/Integration/Database/SqlServer/DatabaseEloquentSqlServerIntegrationTest.php @@ -9,7 +9,7 @@ class DatabaseEloquentSqlServerIntegrationTest extends SqlServerTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable('database_eloquent_sql_server_integration_users')) { Schema::create('database_eloquent_sql_server_integration_users', function (Blueprint $table) { diff --git a/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php b/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php index 28b27f89f80f..5c3b1725552e 100644 --- a/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php +++ b/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php @@ -12,7 +12,7 @@ */ class DatabaseSqlServerConnectionTest extends SqlServerTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable('json_table')) { Schema::create('json_table', function (Blueprint $table) { diff --git a/tests/Integration/Database/SqlServer/DatabaseSqlServerSchemaBuilderTest.php b/tests/Integration/Database/SqlServer/DatabaseSqlServerSchemaBuilderTest.php index ac7a5373241c..3332983a3342 100644 --- a/tests/Integration/Database/SqlServer/DatabaseSqlServerSchemaBuilderTest.php +++ b/tests/Integration/Database/SqlServer/DatabaseSqlServerSchemaBuilderTest.php @@ -9,7 +9,7 @@ class DatabaseSqlServerSchemaBuilderTest extends SqlServerTestCase { - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->integer('id'); diff --git a/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php b/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php index 212ee52be7d8..3068dd920280 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php @@ -24,7 +24,7 @@ protected function getEnvironmentSetUp($app) ]); } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { if (! Schema::hasTable('json_table')) { Schema::create('json_table', function (Blueprint $table) { diff --git a/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php b/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php index 1fd52a7fc3cd..b11e852f0aa7 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php @@ -25,7 +25,7 @@ protected function getEnvironmentSetUp($app) ]); } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { $table->integer('id'); From b6cb872da49d6e113fb900e00eea5ef97f58ca65 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 28 Nov 2023 12:14:04 -0500 Subject: [PATCH 118/207] [10.x] Use HtmlString in Vite fake (#49163) * use htmlstring * test --- .../Foundation/Testing/Concerns/InteractsWithContainer.php | 2 +- .../Foundation/Testing/Concerns/InteractsWithContainerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index 7deafc636fde..514d8e37e105 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -117,7 +117,7 @@ protected function withoutVite() { public function __invoke($entrypoints, $buildDirectory = null) { - return ''; + return new HtmlString(''); } public function __call($method, $parameters) diff --git a/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php b/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php index 5b6680685291..7b1c279c8659 100644 --- a/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php +++ b/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php @@ -13,7 +13,7 @@ public function testWithoutViteBindsEmptyHandlerAndReturnsInstance() { $instance = $this->withoutVite(); - $this->assertSame('', app(Vite::class)(['resources/js/app.js'])); + $this->assertSame('', app(Vite::class)(['resources/js/app.js'])->toHtml()); $this->assertSame($this, $instance); } From 401285329ef3ad01cb578d14a2f5d11beaaefc74 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 28 Nov 2023 11:16:43 -0600 Subject: [PATCH 119/207] patch --- 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 5de3243c9bc6..be08fd085d5b 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.34.0'; + const VERSION = '10.34.1'; /** * The base path for the Laravel installation. From 364cc050a297d198619b0b57b7dbf3cf00bb1a91 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 28 Nov 2023 14:05:48 -0500 Subject: [PATCH 120/207] Add missing methods to fake Vite instance (#49165) --- .../Testing/Concerns/InteractsWithContainer.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index 514d8e37e105..6c517d336ebf 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -160,6 +160,11 @@ public function useStyleTagAttributes($attributes) return $this; } + public function usePreloadTagAttributes($attributes) + { + return $this; + } + public function preloadedAssets() { return []; @@ -170,6 +175,11 @@ public function reactRefresh() return ''; } + public function content($asset, $buildDirectory = null) + { + return ''; + } + public function asset($asset, $buildDirectory = null) { return ''; From c581caa233e380610b34cc491490bfa147a3b62b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 28 Nov 2023 13:06:27 -0600 Subject: [PATCH 121/207] patch --- 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 be08fd085d5b..f0fd0e8e8b3c 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.34.1'; + const VERSION = '10.34.2'; /** * The base path for the Laravel installation. From 1d878ccac4ed1e2a0cc337bf32fac4e52f893dfb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 28 Nov 2023 14:26:24 -0600 Subject: [PATCH 122/207] [11.x] Slim skeleton support (#47309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Configuration All configuration files now have framework counterparts and application level configuration is merged with the framework defaults. The default configuration files have also received many more environment variables so that more options can be changed from the application’s `.env` file. The `LoadConfiguration` bootstrap class has been added to support framework configuration cascading. A new `config:publish` command has been introduced to publish framework configuration files. ### Middleware The `Authenticate` middleware and `AuthenticationException` exception now have `redirectUsing` methods that accept a closure. This closure will be invoked to determine where to redirect unauthenticated users. Helper methods for setting this closure are available on the `Middleware` application builder class. The `Authenticate` middleware also now does not return a redirect location if the incoming request expected JSON. The `RedirectIfAuthenticated` middleware has been added to the framework. This middleware also includes a `redirectUsing` method to customize the “guest” redirection behavior. The default behavior is to redirect to `/dashboard`. Helper methods for setting this closure are available on the `Middleware` application builder class. The `AuthenticateSession` middleware has received a `redirectUsing` helper to control the redirection behavior. Helper methods for setting this closure are available on the `Middleware` application builder class. The `TrimStrings` middleware has received an `except` method that may be used to specify which strings should not be trimmed. The `ValidateCsrfToken` middleware has received an `except` method that may be used to specify which paths should not receive CSRF token validation. The `ValidateSignature` middleware has receive an `except` method that may be used to specify which parameters should not be included in signature validation. `ValidateCsrfToken` has been added as an alias of `VerifyCsrfToken`. The `TrustHosts` middleware has been updated to allow all subdomains of the application’s configured URL by default. The `TrustProxies` middleware has been updated to trust all proxies by default. The `EncryptCookies` middleware has received a static `except` method which may be used in a service provider to specify the cookies that should not be encrypted. ### Events The foundation `EventServiceProvider` has been updated to discover events by default. In addition, the email verification listener to send email verification notifications is now configured automatically if no `Registered` event listeners exist or the `SendEmailVerificatioNotification` listener is not in the list of listeners for the `Registered` event. ### Notificiations A `slack` configuration array has been added to the framework's copy of the `services.php` configuration file. ### Artisan Commands The `cache:table` command has received a `make:cache-table` alias to move all generation commands under the `make` namespace. The `notifications:table` command has receive a `make:notifications-table` alias to move all generation commands under the `make` namespace. The `queue:batches-table` command has received a `make:queue-batches-table` alias for the same reason as above. The `queue:failed-table` command has received a `make:queue-failed-table` alias for the same reason as above. In addition, `queue:table` has received a `make:queue-table` alias. Also, `session:table` has received a `make:session-table` alias. A `schedule` command has been added to closure commands, allowing the fluent scheduling of closure commands in the console routes file. The console scheduler is now available via a `Schedule` facade. The `optimize` command now also caches views and events. ### Service Providers The `RegisterProviders` bootstrap class has been updated to support the loading of additional providers from the `bootstrap/providers.php` array file. The `make:provider` command has been updated to add the new service provider to the `bootstrap/providers.php` file if it exists. The `ServiceProvider` class has received a new static `addProviderToBootstrapFile` method that will add a service provider class to the `bootstrap/providers.php` file if it exists. ### Application Configuration The `Application` class has received several new methods and helpers. A new `registered` listener method has been added to allow code to react to the registration service providers. A new `getBootstrapProvidersPath` method has been added that returns the location to the bootstrap providers file. New `handleRequest` and `handleCommands` method have been added in order to clean up and simplify the application level bootstrap / index files. A new `configure` method has been added to the `Application` class in order to allow the fluent configuration of multiple framework features, including routing and container bindings. A new `ApplicationBuilder` class has been introduced to allow the easy configuration of a variety of core framework functionality, including routing, commands, middleware, exception handling, booting / booted callbacks, and more. A new `Middleware` application configuration class has been introduced that allows the easy definition of new middleware groups, prepending and appending of middleware to existing groups, replacing middleware in existing groups, and fluent methods for enabling middleware like `TrustHosts` and `EnsureFrontendRequestsAreStateful`. Helper methods have been added to the exception handler for `dontReport`, `dontReportDuplicates`, `dontFlash`, `buildContextUsing`. ### Installers A new `install:api` command has been added. This command installs `laravel/sanctum` and uncomments the “API” routes definition in the bootstrap file. A new `install:broadcasting` has been added which uncomments the “channels” routes definition in the bootstrap file. In addition, a Laravel Echo file is written to the `resources/js` directory which contains the Echo configuration. A directive to include this file is injected into the main `bootstrap.js` file. --- .github/workflows/databases.yml | 3 +- composer.json | 2 +- config/app.php | 184 ++++++ config/auth.php | 115 ++++ config/broadcasting.php | 71 +++ config/cache.php | 111 ++++ config/cors.php | 34 + config/database.php | 166 +++++ config/filesystems.php | 76 +++ config/hashing.php | 54 ++ config/logging.php | 131 ++++ config/mail.php | 126 ++++ config/queue.php | 111 ++++ config/services.php | 41 ++ config/session.php | 202 ++++++ config/view.php | 36 ++ .../Auth/AuthenticationException.php | 30 +- .../Auth/Middleware/Authenticate.php | 26 +- .../Middleware/RedirectIfAuthenticated.php | 57 ++ .../Cache/Console/CacheTableCommand.php | 11 +- .../Cookie/Middleware/EncryptCookies.php | 23 +- src/Illuminate/Foundation/Application.php | 86 +++ .../Bootstrap/LoadConfiguration.php | 56 +- .../Bootstrap/RegisterProviders.php | 63 ++ .../Configuration/ApplicationBuilder.php | 337 ++++++++++ .../Foundation/Configuration/Exceptions.php | 137 ++++ .../Foundation/Configuration/Middleware.php | 601 ++++++++++++++++++ .../Foundation/Console/ApiInstallCommand.php | 106 +++ .../Console/BroadcastingInstallCommand.php | 94 +++ .../Foundation/Console/ClosureCommand.php | 32 + .../Console/ConfigPublishCommand.php | 88 +++ .../Console/InteractsWithComposerPackages.php | 44 ++ src/Illuminate/Foundation/Console/Kernel.php | 144 ++++- .../Console/OptimizeClearCommand.php | 8 +- .../Foundation/Console/OptimizeCommand.php | 6 +- .../Console/ProviderMakeCommand.php | 24 + .../Foundation/Console/stubs/api-routes.stub | 20 + .../Console/stubs/broadcasting-routes.stub | 18 + .../Console/stubs/echo-bootstrap-js.stub | 7 + .../Foundation/Console/stubs/echo-js.stub | 15 + .../Foundation/Exceptions/Handler.php | 96 ++- src/Illuminate/Foundation/Http/Kernel.php | 55 ++ .../Http/Middleware/TrimStrings.php | 41 +- .../Http/Middleware/ValidateCsrfToken.php | 11 + .../Http/Middleware/ValidatePostSize.php | 47 +- .../Http/Middleware/VerifyCsrfToken.php | 23 +- .../Providers/ArtisanServiceProvider.php | 18 + .../Providers/FoundationServiceProvider.php | 15 + .../Providers/EventServiceProvider.php | 22 +- .../Providers/RouteServiceProvider.php | 22 +- src/Illuminate/Http/Middleware/TrustHosts.php | 9 +- .../Http/Middleware/TrustProxies.php | 8 +- .../Http/Middleware/ValidatePostSize.php | 52 ++ .../Console/NotificationTableCommand.php | 11 +- .../Queue/Console/BatchesTableCommand.php | 11 +- .../Queue/Console/FailedTableCommand.php | 11 +- src/Illuminate/Queue/Console/TableCommand.php | 11 +- .../Routing/Middleware/ValidateSignature.php | 22 +- .../Session/Console/SessionTableCommand.php | 11 +- .../Middleware/AuthenticateSession.php | 22 +- src/Illuminate/Support/Facades/App.php | 5 + src/Illuminate/Support/Facades/Artisan.php | 4 + src/Illuminate/Support/Facades/Facade.php | 1 + src/Illuminate/Support/Facades/Schedule.php | 35 + src/Illuminate/Support/ServiceProvider.php | 34 + .../View/Engines/CompilerEngine.php | 5 +- tests/Auth/AuthenticateMiddlewareTest.php | 2 + .../Cookie/Middleware/EncryptCookiesTest.php | 17 +- tests/Http/Middleware/TrimStringsTest.php | 17 + .../Scheduling/ScheduleListCommandTest.php | 25 + .../Database/SchemaBuilderTest.php | 7 +- .../Generators/CacheTableCommandTest.php | 4 +- .../NotificationTableCommandTest.php | 4 +- .../QueueBatchesTableCommandTest.php | 4 +- .../QueueFailedTableCommandTest.php | 4 +- .../Generators/QueueTableCommandTest.php | 4 +- .../Generators/SessionTableCommandTest.php | 4 +- .../Middleware/VerifyCsrfTokenExceptTest.php | 7 + tests/Integration/Routing/UrlSigningTest.php | 22 +- 79 files changed, 4085 insertions(+), 134 deletions(-) create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/broadcasting.php create mode 100644 config/cache.php create mode 100644 config/cors.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/hashing.php create mode 100644 config/logging.php create mode 100644 config/mail.php create mode 100644 config/queue.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 config/view.php create mode 100644 src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php create mode 100644 src/Illuminate/Foundation/Configuration/ApplicationBuilder.php create mode 100644 src/Illuminate/Foundation/Configuration/Exceptions.php create mode 100644 src/Illuminate/Foundation/Configuration/Middleware.php create mode 100644 src/Illuminate/Foundation/Console/ApiInstallCommand.php create mode 100644 src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php create mode 100644 src/Illuminate/Foundation/Console/ConfigPublishCommand.php create mode 100644 src/Illuminate/Foundation/Console/InteractsWithComposerPackages.php create mode 100644 src/Illuminate/Foundation/Console/stubs/api-routes.stub create mode 100644 src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub create mode 100644 src/Illuminate/Foundation/Console/stubs/echo-bootstrap-js.stub create mode 100644 src/Illuminate/Foundation/Console/stubs/echo-js.stub create mode 100644 src/Illuminate/Foundation/Http/Middleware/ValidateCsrfToken.php create mode 100644 src/Illuminate/Http/Middleware/ValidatePostSize.php create mode 100644 src/Illuminate/Support/Facades/Schedule.php diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 37ffbe3c2cae..06040a997ab2 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -52,6 +52,7 @@ jobs: env: DB_CONNECTION: mysql DB_USERNAME: root + MYSQL_COLLATION: utf8mb4_unicode_ci mysql_8: runs-on: ubuntu-22.04 @@ -140,7 +141,7 @@ jobs: - name: Execute tests run: vendor/bin/phpunit tests/Integration/Database env: - DB_CONNECTION: mysql + DB_CONNECTION: mariadb DB_USERNAME: root pgsql: diff --git a/composer.json b/composer.json index 84c53c70428d..ee285ac68d1a 100644 --- a/composer.json +++ b/composer.json @@ -106,7 +106,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.0", + "orchestra/testbench-core": "dev-next/slim-skeleton", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.1", diff --git a/config/app.php b/config/app.php new file mode 100644 index 000000000000..cd591805bccd --- /dev/null +++ b/config/app.php @@ -0,0 +1,184 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'), + + 'asset_url' => env('ASSET_URL'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => env('APP_TIMEZONE', 'UTC'), + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the default one + | is not available. You may change the value to correspond to any of + | the languages which are currently supported by your application. + | + */ + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string or all of the encrypted strings are + | not secure. You should do this prior to deploying the application. + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'redis'), + ], + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on any + | requests to your application. You may add your own services to the + | arrays below to provide additional features to this application. + | + */ + + 'providers' => ServiceProvider::defaultProviders()->merge([ + // Package Service Providers... + ])->merge([ + // Application Service Providers... + // App\Providers\AppServiceProvider::class, + ])->merge([ + // Added Service Providers (Do not remove this line)... + ])->toArray(), + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. You may add any additional class aliases which should + | be loaded to the array. For speed, all aliases are lazy loaded. + | + */ + + 'aliases' => Facade::defaultAliases()->merge([ + // 'Example' => App\Facades\Example::class, + ])->toArray(), + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 000000000000..7bf212ad24a4 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_reset_tokens', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 000000000000..de5dd4e5ce8c --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,71 @@ + env('BROADCAST_CONNECTION', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_BROADCASTING_CONNECTION', 'default'), + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 000000000000..4d67e5de8a49 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,111 @@ + env('CACHE_STORE', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "apc", "array", "database", "file", + | "memcached", "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'connection' => env('DB_CACHE_CONNECTION', null), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION', null), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, or DynamoDB cache + | stores there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 000000000000..8a39e6daa63d --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 000000000000..ff5880bd2e8e --- /dev/null +++ b/config/database.php @@ -0,0 +1,166 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to assist your development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_0900_ai_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_uca1400_ai_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 000000000000..21ad5c8bdb40 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,76 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been set up for each driver as an example of the required values. + | + | Supported Drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + 'throw' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 000000000000..2925f725b487 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,54 @@ + env('HASH_DRIVER', 'bcrypt'), + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 12), + 'verify' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => env('ARGON_MEMORY', 65536), + 'threads' => env('ARGON_THREADS', 1), + 'time' => env('ARGON_TIME', 4), + 'verify' => true, + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 000000000000..06a666ac78c0 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,131 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 000000000000..07e449a5fa23 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,126 @@ + env('MAIL_MAILER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | delivering an email. You may specify which one you're using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "log", "array", "failover" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN'), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => null, + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => env('MAIL_MARKDOWN_THEME', 'default'), + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 000000000000..70c9fcf83a4d --- /dev/null +++ b/config/queue.php @@ -0,0 +1,111 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 000000000000..a12fbb266b0b --- /dev/null +++ b/config/services.php @@ -0,0 +1,41 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 000000000000..b29b8b783815 --- /dev/null +++ b/config/session.php @@ -0,0 +1,202 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, we need a spot where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | While using one of the framework's cache driven session backends you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 000000000000..22b8a18d3258 --- /dev/null +++ b/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/src/Illuminate/Auth/AuthenticationException.php b/src/Illuminate/Auth/AuthenticationException.php index 66808c3b8151..c4f835c5e6c0 100644 --- a/src/Illuminate/Auth/AuthenticationException.php +++ b/src/Illuminate/Auth/AuthenticationException.php @@ -3,6 +3,7 @@ namespace Illuminate\Auth; use Exception; +use Illuminate\Http\Request; class AuthenticationException extends Exception { @@ -20,6 +21,13 @@ class AuthenticationException extends Exception */ protected $redirectTo; + /** + * The callback that should be used to generate the authentication redirect path. + * + * @var callable + */ + protected static $redirectToCallback; + /** * Create a new authentication exception. * @@ -49,10 +57,28 @@ public function guards() /** * Get the path the user should be redirected to. * + * @param \Illuminate\Http\Request $request * @return string|null */ - public function redirectTo() + public function redirectTo(Request $request) + { + if ($this->redirectTo) { + return $this->redirectTo; + } + + if (static::$redirectToCallback) { + return call_user_func(static::$redirectToCallback, $request); + } + } + + /** + * Specify the callback that should be used to generate the redirect path. + * + * @param callable $redirectToCallback + * @return void + */ + public static function redirectUsing(callable $redirectToCallback) { - return $this->redirectTo; + static::$redirectToCallback = $redirectToCallback; } } diff --git a/src/Illuminate/Auth/Middleware/Authenticate.php b/src/Illuminate/Auth/Middleware/Authenticate.php index b1b4cf90abc2..ef1a3f2ea030 100644 --- a/src/Illuminate/Auth/Middleware/Authenticate.php +++ b/src/Illuminate/Auth/Middleware/Authenticate.php @@ -17,6 +17,13 @@ class Authenticate implements AuthenticatesRequests */ protected $auth; + /** + * The callback that should be used to generate the authentication redirect path. + * + * @var callable + */ + protected static $redirectToCallback; + /** * Create a new middleware instance. * @@ -93,7 +100,9 @@ protected function authenticate($request, array $guards) protected function unauthenticated($request, array $guards) { throw new AuthenticationException( - 'Unauthenticated.', $guards, $this->redirectTo($request) + 'Unauthenticated.', + $guards, + $request->expectsJson() ? null : $this->redirectTo($request), ); } @@ -105,6 +114,19 @@ protected function unauthenticated($request, array $guards) */ protected function redirectTo(Request $request) { - // + if (static::$redirectToCallback) { + return call_user_func(static::$redirectToCallback, $request); + } + } + + /** + * Specify the callback that should be used to generate the redirect path. + * + * @param callable $redirectToCallback + * @return void + */ + public static function redirectUsing(callable $redirectToCallback) + { + static::$redirectToCallback = $redirectToCallback; } } diff --git a/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php b/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 000000000000..c436ef16ee0a --- /dev/null +++ b/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,57 @@ +check()) { + return redirect($this->redirectTo($request)); + } + } + + return $next($request); + } + + /** + * Get the path the user should be redirected to when they are authenticated. + */ + protected function redirectTo(Request $request): ?string + { + return static::$redirectToCallback + ? call_user_func(static::$redirectToCallback, $request) + : '/dashboard'; + } + + /** + * Specify the callback that should be used to generate the redirect path. + * + * @param callable $redirectToCallback + * @return void + */ + public static function redirectUsing(callable $redirectToCallback) + { + static::$redirectToCallback = $redirectToCallback; + } +} diff --git a/src/Illuminate/Cache/Console/CacheTableCommand.php b/src/Illuminate/Cache/Console/CacheTableCommand.php index b44794f95856..ab7b2d613b06 100644 --- a/src/Illuminate/Cache/Console/CacheTableCommand.php +++ b/src/Illuminate/Cache/Console/CacheTableCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\MigrationGeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'cache:table')] +#[AsCommand(name: 'make:cache-table')] class CacheTableCommand extends MigrationGeneratorCommand { /** @@ -13,7 +13,14 @@ class CacheTableCommand extends MigrationGeneratorCommand * * @var string */ - protected $name = 'cache:table'; + protected $name = 'make:cache-table'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['cache:table']; /** * The console command description. diff --git a/src/Illuminate/Cookie/Middleware/EncryptCookies.php b/src/Illuminate/Cookie/Middleware/EncryptCookies.php index 53a914e70a0a..784bd13b82e7 100644 --- a/src/Illuminate/Cookie/Middleware/EncryptCookies.php +++ b/src/Illuminate/Cookie/Middleware/EncryptCookies.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract; use Illuminate\Cookie\CookieValuePrefix; +use Illuminate\Support\Arr; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,6 +27,13 @@ class EncryptCookies */ protected $except = []; + /** + * The globally ignored cookies that should not be encrypted. + * + * @var array + */ + protected static $neverEncrypt = []; + /** * Indicates if cookies should be serialized. * @@ -210,7 +218,20 @@ protected function duplicate(Cookie $cookie, $value) */ public function isDisabled($name) { - return in_array($name, $this->except); + return in_array($name, array_merge($this->except, static::$neverEncrypt)); + } + + /** + * Indicate that the given cookies should never be encrypted. + * + * @param array|string $cookies + * @return void + */ + public static function except($cookies) + { + static::$neverEncrypt = array_values(array_unique( + array_merge(static::$neverEncrypt, Arr::wrap($cookies)) + )); } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index b95ccc31e476..7679ed757c94 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Container\Container; +use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; use Illuminate\Contracts\Foundation\Application as ApplicationContract; use Illuminate\Contracts\Foundation\CachesConfiguration; use Illuminate\Contracts\Foundation\CachesRoutes; @@ -23,6 +24,8 @@ use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; use RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -47,6 +50,13 @@ class Application extends Container implements ApplicationContract, CachesConfig */ protected $basePath; + /** + * The array of registered callbacks. + * + * @var callable[] + */ + protected $registeredCallbacks = []; + /** * Indicates if the application has been bootstrapped before. * @@ -204,6 +214,24 @@ public function __construct($basePath = null) $this->registerCoreContainerAliases(); } + /** + * Begin configuring a new Laravel application instance. + * + * @param string|null $baseDirectory + * @return \Illuminate\Foundation\ApplicationBuilder + */ + public static function configure(string $baseDirectory = null) + { + $baseDirectory = $ENV['APP_BASE_PATH'] ?? ($baseDirectory ?: dirname(dirname( + debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'] + ))); + + return (new Configuration\ApplicationBuilder(new static($baseDirectory))) + ->withKernels() + ->withEvents() + ->withCommands(); + } + /** * Get the version number of the application. * @@ -402,6 +430,16 @@ public function bootstrapPath($path = '') return $this->joinPaths($this->bootstrapPath, $path); } + /** + * Get the path to the service provider list in the bootstrap directory. + * + * @return string + */ + public function getBootstrapProvidersPath() + { + return $this->bootstrapPath('providers.php'); + } + /** * Set the bootstrap file directory. * @@ -747,6 +785,17 @@ public function hasDebugModeEnabled() return (bool) $this['config']->get('app.debug'); } + /** + * Register a new registered listener. + * + * @param callable $callback + * @return void + */ + public function registered($callback) + { + $this->registeredCallbacks[] = $callback; + } + /** * Register all of the configured providers. * @@ -761,6 +810,8 @@ public function registerConfiguredProviders() (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) ->load($providers->collapse()->toArray()); + + $this->fireAppCallbacks($this->registeredCallbacks); } /** @@ -1084,6 +1135,41 @@ public function handle(SymfonyRequest $request, int $type = self::MAIN_REQUEST, return $this[HttpKernelContract::class]->handle(Request::createFromBase($request)); } + /** + * Handle the incoming HTTP request and send the response to the browser. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + public function handleRequest(Request $request) + { + $kernel = $this->make(HttpKernelContract::class); + + $response = $kernel->handle($request)->send(); + + $kernel->terminate($request, $response); + } + + /** + * Handle the incoming Artisan command. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return int + */ + public function handleCommand(InputInterface $input) + { + $kernel = $this->make(ConsoleKernelContract::class); + + $status = $kernel->handle( + $input, + new ConsoleOutput + ); + + $kernel->terminate($input, $status); + + return $status; + } + /** * Determine if middleware has been disabled for the application. * diff --git a/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php b/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php index ae3f73881e5d..8d096031988c 100644 --- a/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php +++ b/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php @@ -27,7 +27,7 @@ public function bootstrap(Application $app) if (file_exists($cached = $app->getCachedConfigPath())) { $items = require $cached; - $loadedFromCache = true; + $app->instance('config_loaded_from_cache', $loadedFromCache = true); } // Next we will spin through all of the configuration files in the configuration @@ -62,15 +62,45 @@ protected function loadConfigurationFiles(Application $app, RepositoryContract $ { $files = $this->getConfigurationFiles($app); - if (! isset($files['app'])) { - throw new Exception('Unable to load the "app" configuration file.'); + // if (! isset($files['app'])) { + // throw new Exception('Unable to load the "app" configuration file.'); + // } + + $base = $this->getBaseConfiguration(); + + foreach ($files as $name => $path) { + $base = $this->loadConfigurationFile($repository, $name, $path, $base); } - foreach ($files as $key => $path) { - $repository->set($key, require $path); + foreach ($base as $name => $config) { + $repository->set($name, $config); } } + /** + * Load the given configuration file. + * + * @param \Illuminate\Contracts\Config\Repository $repository + * @param string $name + * @param string $path + * @param array $base + * @return array + */ + protected function loadConfigurationFile(RepositoryContract $repository, $name, $path, array $base) + { + $config = require $path; + + if (isset($base[$name])) { + $config = array_merge($base[$name], $config); + + unset($base[$name]); + } + + $repository->set($name, $config); + + return $base; + } + /** * Get all of the configuration files for the application. * @@ -111,4 +141,20 @@ protected function getNestedDirectory(SplFileInfo $file, $configPath) return $nested; } + + /** + * Get the base configuration files. + * + * @return array + */ + protected function getBaseConfiguration() + { + $config = []; + + foreach (Finder::create()->files()->name('*.php')->in(__DIR__.'/../../../../config') as $file) { + $config[basename($file->getRealPath(), '.php')] = require $file->getRealPath(); + } + + return $config; + } } diff --git a/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php b/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php index f18375cf3c7d..ddad0ffabcc5 100644 --- a/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php +++ b/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php @@ -6,6 +6,20 @@ class RegisterProviders { + /** + * The service providers that should be merged before registration. + * + * @var array + */ + protected static $merge = []; + + /** + * The path to the bootstrap provider configuration file. + * + * @var string|null + */ + protected static $bootstrapProviderPath; + /** * Bootstrap the given application. * @@ -14,6 +28,55 @@ class RegisterProviders */ public function bootstrap(Application $app) { + if (! $app->bound('config_loaded_from_cache') || + $app->make('config_loaded_from_cache') === false) { + $this->mergeAdditionalProviders($app); + } + $app->registerConfiguredProviders(); } + + /** + * Merge the additional configured providers into the configuration. + * + * @param \Illuminate\Foundation\Application $app + */ + protected function mergeAdditionalProviders(Application $app) + { + if (static::$bootstrapProviderPath && + file_exists(static::$bootstrapProviderPath)) { + $packageProviders = require static::$bootstrapProviderPath; + + foreach ($packageProviders as $index => $provider) { + if (! class_exists($provider)) { + unset($packageProviders[$index]); + } + } + } + + $app->make('config')->set( + 'app.providers', + array_merge( + $app->make('config')->get('app.providers'), + static::$merge, + array_values($packageProviders ?? []), + ), + ); + } + + /** + * Merge the given providers into the provider configuration before registration. + * + * @param array $providers + * @param string|null $bootstrapProviderPath + * @return void + */ + public static function merge(array $providers, ?string $bootstrapProviderPath = null) + { + static::$bootstrapProviderPath = $bootstrapProviderPath; + + static::$merge = array_values(array_filter(array_unique( + array_merge(static::$merge, $providers) + ))); + } } diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php new file mode 100644 index 000000000000..dd213b977743 --- /dev/null +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -0,0 +1,337 @@ +app->singleton( + \Illuminate\Contracts\Http\Kernel::class, + \Illuminate\Foundation\Http\Kernel::class, + ); + + $this->app->singleton( + \Illuminate\Contracts\Console\Kernel::class, + \Illuminate\Foundation\Console\Kernel::class, + ); + + return $this; + } + + /** + * Register additional service providers. + * + * @param array $providers + * @param bool $withBootstrapProviders + * @return $this + */ + public function withProviders(array $providers = [], bool $withBootstrapProviders = true) + { + RegisterProviders::merge( + $providers, + $withBootstrapProviders + ? $this->app->getBootstrapProvidersPath() + : null + ); + + return $this; + } + + /** + * Register the core event service provider for the application. + * + * @return $this + */ + public function withEvents() + { + $this->app->booting(function () { + $this->app->register(AppEventServiceProvider::class); + }); + + return $this; + } + + /** + * Register the braodcasting services for the application. + * + * @param string $channels + * @return $this + */ + public function withBroadcasting(string $channels) + { + $this->app->booted(function () use ($channels) { + Broadcast::routes(); + + if (file_exists($channels)) { + require $channels; + } + }); + + return $this; + } + + /** + * Register the routing services for the application. + * + * @param \Closure|null $using + * @param string|null $web + * @param string|null $api + * @param string|null $commands + * @param string|null $channels + * @param string|null $pages + * @param string|null $apiPrefix + * @param callable|null $then + * @return $this + */ + public function withRouting(?Closure $using = null, + ?string $web = null, + ?string $api = null, + ?string $commands = null, + ?string $channels = null, + ?string $pages = null, + string $apiPrefix = 'api', + ?callable $then = null) + { + if (is_null($using) && (is_string($web) || is_string($api))) { + $using = $this->buildRoutingCallback($web, $api, $pages, $apiPrefix, $then); + } + + AppRouteServiceProvider::loadRoutesUsing($using); + + $this->app->booting(function () { + $this->app->register(AppRouteServiceProvider::class); + }); + + if (is_string($commands) && realpath($commands) !== false) { + $this->withCommands([$commands]); + } + + if (is_string($channels) && realpath($channels) !== false) { + $this->withBroadcasting($channels); + } + + return $this; + } + + /** + * Create the routing callback for the application. + * + * @param string|null $web + * @param string|null $api + * @param string|null $pages + * @param string $apiPrefix + * @param callable|null $then + * @return \Closure + */ + protected function buildRoutingCallback(?string $web, + ?string $api, + ?string $pages, + string $apiPrefix, + ?callable $then) + { + return function () use ($web, $api, $pages, $apiPrefix, $then) { + if (is_string($api) && realpath($api) !== false) { + Route::middleware('api')->prefix($apiPrefix)->group($api); + } + + if (is_string($web) && realpath($web) !== false) { + Route::middleware('web')->group($web); + } + + if (is_string($pages) && + realpath($pages) !== false && + class_exists(Folio::class)) { + Folio::route($pages, middleware: $this->pageMiddleware); + } + + if (is_callable($then)) { + $then(); + } + }; + } + + /** + * Register the global middleware, middleware groups, and middleware aliases for the application. + * + * @param callable $callback + * @return $this + */ + public function withMiddleware(callable $callback) + { + $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { + $middleware = (new Middleware) + ->auth(redirectTo: fn () => route('login')) + ->guest(redirectTo: fn () => route('dashboard')); + + $callback($middleware); + + $this->pageMiddleware = $middleware->getPageMiddleware(); + $kernel->setGlobalMiddleware($middleware->getGlobalMiddleware()); + $kernel->setMiddlewareGroups($middleware->getMiddlewareGroups()); + $kernel->setMiddlewareAliases($middleware->getMiddlewareAliases()); + }); + + return $this; + } + + /** + * Register additional Artisan commands with the application. + * + * @param array $commands + * @return $this + */ + public function withCommands(array $commands = []) + { + if (empty($commands)) { + $commands = [$this->app->path('Console/Commands')]; + } + + $this->app->afterResolving(ConsoleKernel::class, function ($kernel) use ($commands) { + [$commands, $paths] = collect($commands)->partition(fn ($command) => class_exists($command)); + [$routes, $paths] = $paths->partition(fn ($path) => is_file($path)); + + $kernel->addCommands($commands->all()); + $kernel->addCommandPaths($paths->all()); + $kernel->addCommandRoutePaths($routes->all()); + }); + + return $this; + } + + /** + * Register additional Artisan route paths. + * + * @param array $paths + * @return $this + */ + protected function withCommandRouting(array $paths) + { + $this->app->afterResolving(ConsoleKernel::class, function ($kernel) use ($paths) { + $kernel->setCommandRoutePaths($paths); + }); + } + + /** + * Register and configure the application's exception handler. + * + * @param callable|null $using + * @return $this + */ + public function withExceptions(?callable $using = null) + { + $this->app->singleton( + \Illuminate\Contracts\Debug\ExceptionHandler::class, + \Illuminate\Foundation\Exceptions\Handler::class + ); + + $using ??= fn () => true; + + $this->app->afterResolving( + \Illuminate\Foundation\Exceptions\Handler::class, + fn ($handler) => $using(new Exceptions($handler)), + ); + + return $this; + } + + /** + * Register an array of container bindings to be bound when the application is booting. + * + * @param array $bindings + * @return $this + */ + public function withBindings(array $bindings) + { + return $this->registered(function ($app) use ($bindings) { + foreach ($bindings as $abstract => $concrete) { + $app->bind($abstract, $concrete); + } + }); + } + + /** + * Register an array of singleton container bindings to be bound when the application is booting. + * + * @param array $singletons + * @return $this + */ + public function withSingletons(array $singletons) + { + return $this->registered(function ($app) use ($singletons) { + foreach ($singletons as $abstract => $concrete) { + if (is_string($abstract)) { + $app->singleton($abstract, $concrete); + } else { + $app->singleton($concrete); + } + } + }); + } + + /** + * Register a callback to be invoked when the application is "booting". + * + * @param callable $callback + * @return $this + */ + public function booting(callable $callback) + { + $this->app->booting($callback); + + return $this; + } + + /** + * Register a callback to be invoked when the application is "booted". + * + * @param callable $callback + * @return $this + */ + public function booted(callable $callback) + { + $this->app->booted($callback); + + return $this; + } + + /** + * Get the application instance. + * + * @return \Illuminate\Foundation\Application + */ + public function create() + { + return $this->app; + } +} diff --git a/src/Illuminate/Foundation/Configuration/Exceptions.php b/src/Illuminate/Foundation/Configuration/Exceptions.php new file mode 100644 index 000000000000..b38bd2a20278 --- /dev/null +++ b/src/Illuminate/Foundation/Configuration/Exceptions.php @@ -0,0 +1,137 @@ +handler->reportable($reportUsing); + } + + /** + * Register a renderable callback. + * + * @param callable $renderUsing + * @return $this + */ + public function renderable(callable $renderUsing) + { + $this->handler->renderable($renderUsing); + + return $this; + } + + /** + * Specify the callback that should be used to throttle reportable exceptions. + * + * @param callable $throttleUsing + * @return $this + */ + public function throttle(callable $throttleUsing) + { + $this->handler->throttleUsing($throttleUsing); + + return $this; + } + + /** + * Register a new exception mapping. + * + * @param \Closure|string $from + * @param \Closure|string|null $to + * @return $this + * + * @throws \InvalidArgumentException + */ + public function map($from, $to = null) + { + $this->handler->map($from, $to); + + return $this; + } + + /** + * Set the log level for the given exception type. + * + * @param class-string<\Throwable> $type + * @param \Psr\Log\LogLevel::* $level + * @return $this + */ + public function level($type, $level) + { + $this->handler->level($type, $level); + + return $this; + } + + /** + * Register a closure that should be used to build exception context data. + * + * @param \Closure $contextCallback + * @return $this + */ + public function context(Closure $contextCallback) + { + $this->handler->buildContextUsing($contextCallback); + + return $this; + } + + /** + * Indicate that the given exception type should not be reported. + * + * @param string $class + * @return $this + */ + public function dontReport(string $class) + { + $this->handler->dontReport($class); + + return $this; + } + + /** + * Do not report duplicate exceptions. + * + * @return $this + */ + public function dontReportDuplicates() + { + $this->handler->dontReportDuplicates(); + + return $this; + } + + /** + * Indicate that the given attributes should never be flashed to the session on validation errors. + * + * @param array|string $attributes + * @return $this + */ + public function dontFlash($attributes) + { + $this->handler->dontFlash($attributes); + + return $this; + } +} diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php new file mode 100644 index 000000000000..949dfe4b5806 --- /dev/null +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -0,0 +1,601 @@ + \Illuminate\Auth\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \Illuminate\Auth\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'subscribed' => \Spark\Http\Middleware\VerifyBillableIsSubscribed::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; + + /** + * The custom middleware aliases. + * + * @var array + */ + protected $customAliases = []; + + /** + * Prepend middleware to the application's global middleware stack. + * + * @param array|string $middleware + * @return $this + */ + public function prepend(array|string $middleware) + { + $this->prepends = array_merge( + Arr::wrap($middleware), + $this->prepends + ); + + return $this; + } + + /** + * Append middleware to the application's global middleware stack. + * + * @param array|string $middleware + * @return $this + */ + public function append(array|string $middleware) + { + $this->appends = array_merge( + $this->appends, + Arr::wrap($middleware) + ); + + return $this; + } + + /** + * Remove middleware from the application's global middleware stack. + * + * @param array|string $middleware + * @return $this + */ + public function remove(array|string $middleware) + { + $this->removals = array_merge( + $this->removals, + Arr::wrap($middleware) + ); + + return $this; + } + + /** + * Specify a middleware that should be replaced with another middleware. + * + * @param string $search + * @param string $replace + * @return $this + */ + public function replace(string $search, string $replace) + { + $this->replacements[$search] = $replace; + + return $this; + } + + /** + * Define the global middleware for the application. + * + * @param array $middleware + * @return $this + */ + public function use(array $middleware) + { + $this->global = $middleware; + + return $this; + } + + /** + * Define a middleware group. + * + * @param string $group + * @param array $middleware + * @return $this + */ + public function group(string $group, array $middleware) + { + $this->groups[$group] = $middleware; + + return $this; + } + + /** + * Prepend the given middleware to the specified group. + * + * @param string $group + * @param array|string $middleware + * @return $this + */ + public function prependToGroup(string $group, array|string $middleware) + { + $this->groupPrepends[$group] = array_merge( + Arr::wrap($middleware), + $this->groupPrepends[$group] ?? [] + ); + + return $this; + } + + /** + * Append the given middleware to the specified group. + * + * @param string $group + * @param array|string $middleware + * @return $this + */ + public function appendToGroup(string $group, array|string $middleware) + { + $this->groupAppends[$group] = array_merge( + Arr::wrap($middleware), + $this->groupAppends[$group] ?? [] + ); + + return $this; + } + + /** + * Remove the given middleware from the specified group. + * + * @param string $group + * @param array|string $middleware + * @return $this + */ + public function removeFromGroup(string $group, array|string $middleware) + { + $this->groupRemovals[$group] = array_merge( + Arr::wrap($middleware), + $this->groupRemovals[$group] ?? [] + ); + + return $this; + } + + /** + * Replace the given middleware in the specified group with another middleware. + * + * @param string $group + * @param string $search + * @param string $replace + * @return $this + */ + public function replaceInGroup(string $group, string $search, string $replace) + { + $this->groupReplacements[$group][$search] = $replace; + + return $this; + } + + /** + * Modify the middleware in the "web" group. + * + * @param string $group + * @param array|string $append + * @param array|string $prepend + * @param array|string $remove + * @param array $replace + * @return $this + */ + public function web(array|string $append = [], array|string $prepend = [], array|string $remove = [], array $replace = []) + { + return $this->modifyGroup('web', $append, $prepend, $remove, $replace); + } + + /** + * Modify the middleware in the "api" group. + * + * @param string $group + * @param array|string $append + * @param array|string $prepend + * @param array|string $remove + * @param array $replace + * @return $this + */ + public function api(array|string $append = [], array|string $prepend = [], array|string $remove = [], array $replace = []) + { + return $this->modifyGroup('api', $append, $prepend, $remove, $replace); + } + + /** + * Modify the middleware in the given group. + * + * @param string $group + * @param array|string $append + * @param array|string $prepend + * @param array|string $remove + * @param array $replace + * @return $this + */ + protected function modifyGroup(string $group, array|string $append, array|string $prepend, array|string $remove, array $replace) + { + if (! empty($append)) { + $this->appendToGroup($group, $append); + } + + if (! empty($prepend)) { + $this->prependToGroup($group, $prepend); + } + + if (! empty($remove)) { + $this->removeFromGroup($group, $remove); + } + + if (! empty($replace)) { + foreach ($replace as $search => $replace) { + $this->replaceInGroup($group, $search, $replace); + } + } + + return $this; + } + + /** + * Register the Folio / page middleware for the application. + * + * @param array $middleware + * @return $this + */ + public function pages(array $middleware) + { + $this->pageMiddleware = $middleware; + + return $this; + } + + /** + * Register additional middleware aliases. + * + * @param array $aliases + * @return $this + */ + public function alias(array $aliases) + { + $this->customAliases = $aliases; + + return $this; + } + + /** + * Get the global middleware. + * + * @return array + */ + public function getGlobalMiddleware() + { + $middleware = $this->global ?: array_values(array_filter([ + $this->trustHosts ? \Illuminate\Http\Middleware\TrustHosts::class : null, + \Illuminate\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, + \Illuminate\Http\Middleware\ValidatePostSize::class, + \Illuminate\Foundation\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ])); + + $middleware = array_map(function ($middleware) { + return isset($this->replacements[$middleware]) + ? $this->replacements[$middleware] + : $middleware; + }, $middleware); + + return array_values(array_filter( + array_diff( + array_unique(array_merge($this->prepends, $middleware, $this->appends)), + $this->removals + ) + )); + } + + /** + * Get the middleware groups. + * + * @return array + */ + public function getMiddlewareGroups() + { + $middleware = [ + 'web' => [ + \Illuminate\Cookie\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => array_values(array_filter([ + $this->statefulApi ? \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class : null, + $this->apiLimiter ? 'throttle:'.$this->apiLimiter : null, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ])), + ]; + + $middleware = array_merge($middleware, $this->groups); + + foreach ($middleware as $group => $groupedMiddleware) { + foreach ($groupedMiddleware as $index => $groupMiddleware) { + if (isset($this->groupReplacements[$group][$groupMiddleware])) { + $middleware[$group][$index] = $this->groupReplacements[$group][$groupMiddleware]; + } + } + } + + foreach ($this->groupRemovals as $group => $removals) { + $middleware[$group] = array_values(array_filter( + array_diff($middleware[$group] ?? [], $removals) + )); + } + + foreach ($this->groupPrepends as $group => $prepends) { + $middleware[$group] = array_values(array_filter( + array_unique(array_merge($prepends, $middleware[$group] ?? [])) + )); + } + + foreach ($this->groupAppends as $group => $appends) { + $middleware[$group] = array_values(array_filter( + array_unique(array_merge($middleware[$group] ?? [], $appends)) + )); + } + + return $middleware; + } + + /** + * Configure the behavior of the authentication middleware. + * + * @param callable $redirectTo + * @return $this + */ + public function auth(callable $redirectTo) + { + Authenticate::redirectUsing($redirectTo); + AuthenticateSession::redirectUsing($redirectTo); + AuthenticationException::redirectUsing($redirectTo); + + return $this; + } + + /** + * Configure the behavior of the "guest" middleware. + * + * @param callable $redirectTo + * @return $this + */ + public function guest(callable $redirectTo) + { + RedirectIfAuthenticated::redirectUsing($redirectTo); + + return $this; + } + + /** + * Indicate that the trusted host middleware should be enabled. + * + * @return $this + */ + public function withTrustedHosts() + { + $this->trustHosts = true; + + return $this; + } + + /** + * Indicate that Sanctum's frontend state middleware should be enabled. + * + * @return $this + */ + public function withStatefulApi() + { + $this->statefulApi = true; + + return $this; + } + + /** + * Indicate that the API middleware group's throttling middleware should be enabled. + * + * @param string $limiter + * @param bool $redis + * @return $this + */ + public function withThrottledApi($limiter = 'api', $redis = false) + { + $this->apiLimiter = $limiter; + + if ($redis) { + $this->throttleWithRedis(); + } + + return $this; + } + + /** + * Indicate that Laravel's throttling middleware should use Redis. + * + * @return $this + */ + public function throttleWithRedis() + { + $this->throttleWithRedis = true; + + return $this; + } + + /** + * Get the Folio / page middleware for the application. + * + * @return array + */ + public function getPageMiddleware() + { + return $this->pageMiddleware; + } + + /** + * Get the middleware aliases. + * + * @return array + */ + public function getMiddlewareAliases() + { + return array_merge($this->defaultAliases(), $this->customAliases); + } + + /** + * Get the default middleware aliases. + * + * @return array + */ + protected function defaultAliases() + { + return [ + 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \Illuminate\Auth\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'subscribed' => \Spark\Http\Middleware\VerifyBillableIsSubscribed::class, + 'throttle' => $this->throttleWithRedis + ? \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class + : \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; + } +} diff --git a/src/Illuminate/Foundation/Console/ApiInstallCommand.php b/src/Illuminate/Foundation/Console/ApiInstallCommand.php new file mode 100644 index 000000000000..2a7a072a0461 --- /dev/null +++ b/src/Illuminate/Foundation/Console/ApiInstallCommand.php @@ -0,0 +1,106 @@ +installSanctum(); + + if (file_exists($apiRoutesPath = $this->laravel->basePath('routes/api.php')) && + ! $this->option('force')) { + $this->components->error('API routes file already exists.'); + } else { + $this->components->info('Published API routes file.'); + + copy(__DIR__.'/stubs/api-routes.stub', $apiRoutesPath); + + $this->uncommentApiRoutesFile(); + } + + $this->components->info('API scaffolding installed. Please add the "Laravel\Sanctum\HasApiTokens" trait to your User model.'); + } + + /** + * Uncomment the API routes file in the application bootstrap file. + * + * @return void + */ + protected function uncommentApiRoutesFile() + { + $appBootstrapPath = $this->laravel->bootstrapPath('app.php'); + + $content = file_get_contents($appBootstrapPath); + + if (str_contains($content, '// api: ')) { + (new Filesystem)->replaceInFile( + '// api: ', + 'api: ', + $appBootstrapPath, + ); + } elseif (str_contains($content, 'web: __DIR__.\'/../routes/web.php\',')) { + (new Filesystem)->replaceInFile( + 'web: __DIR__.\'/../routes/web.php\',', + 'web: __DIR__.\'/../routes/web.php\','.PHP_EOL.' api: __DIR__.\'/../routes/api.php\',', + $appBootstrapPath, + ); + } else { + $this->components->warn('Unable to automatically add API route definition to bootstrap file. API route file should be registered manually.'); + + return; + } + } + + /** + * Install Laravel Sanctum into the application. + * + * @return void + */ + protected function installSanctum() + { + $this->requireComposerPackages($this->option('composer'), [ + 'laravel/sanctum:dev-master', + ]); + + $php = (new PhpExecutableFinder())->find(false) ?: 'php'; + + $result = Process::run([ + $php, + defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', + 'vendor:publish', + '--provider', + 'Laravel\\Sanctum\\SanctumServiceProvider', + ]); + } +} diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php new file mode 100644 index 000000000000..d0b606e4e74e --- /dev/null +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -0,0 +1,94 @@ +laravel->basePath('routes/channels.php')) && + ! $this->option('force')) { + $this->components->error('Broadcasting routes file already exists.'); + } else { + $this->components->info('Published broadcasting routes file.'); + + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); + + $this->uncommentChannelsRoutesFile(); + } + + // Install bootstrapping... + if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { + copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); + } + + if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { + $bootstrapScript = file_get_contents( + $bootstrapScriptPath + ); + + if (! str_contains($bootstrapScript, 'echo.js')) { + file_put_contents( + $bootstrapScriptPath, + $bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub') + ); + } + } + } + + /** + * Uncomment the "channels" routes file in the application bootstrap file. + * + * @return void + */ + protected function uncommentChannelsRoutesFile() + { + $appBootstrapPath = $this->laravel->bootstrapPath('app.php'); + + $content = file_get_contents($appBootstrapPath); + + if (str_contains($content, '// channels: ')) { + (new Filesystem)->replaceInFile( + '// channels: ', + 'channels: ', + $appBootstrapPath, + ); + } elseif (str_contains($content, 'commands: __DIR__.\'/../routes/console.php\',')) { + (new Filesystem)->replaceInFile( + 'commands: __DIR__.\'/../routes/console.php\',', + 'commands: __DIR__.\'/../routes/console.php\','.PHP_EOL.' channels: __DIR__.\'/../routes/channels.php\',', + $appBootstrapPath, + ); + } else { + $this->components->warn('Unable to automatically add channel route definition to bootstrap file. Channel route file should be registered manually.'); + + return; + } + } +} diff --git a/src/Illuminate/Foundation/Console/ClosureCommand.php b/src/Illuminate/Foundation/Console/ClosureCommand.php index c5be75ecc103..ae51801fe0a0 100644 --- a/src/Illuminate/Foundation/Console/ClosureCommand.php +++ b/src/Illuminate/Foundation/Console/ClosureCommand.php @@ -4,12 +4,19 @@ use Closure; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Schedule; +use Illuminate\Support\Traits\ForwardsCalls; use ReflectionFunction; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * @mixin \Illuminate\Console\Scheduling\Event + */ class ClosureCommand extends Command { + use ForwardsCalls; + /** * The command callback. * @@ -79,4 +86,29 @@ public function describe($description) return $this; } + + /** + * Create a new scheduled event for the command. + * + * @param array $parameters + * @return \Illuminate\Console\Scheduling\Event + */ + public function schedule($parameters = []) + { + return Schedule::command($this->name, $parameters); + } + + /** + * Dynamically proxy calls to a new scheduled event. + * + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + return $this->forwardCallTo($this->schedule(), $method, $parameters); + } } diff --git a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php new file mode 100644 index 000000000000..65eb8c592968 --- /dev/null +++ b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php @@ -0,0 +1,88 @@ +getBaseConfigurationFiles(); + + $name = $this->argument('name'); + + if (! is_null($name) && ! isset($config[$name])) { + $this->components->error('Unrecognized configuration file.'); + + return 1; + } + + foreach ($config as $key => $file) { + if ($key === $name || is_null($name)) { + $this->publish($key, $file, $this->laravel->configPath().'/'.$key.'.php'); + } + } + } + + /** + * Publish the given file to the given destination. + * + * @param string $name + * @param string $file + * @param string $destination + * @return void + */ + protected function publish(string $name, string $file, string $destination) + { + if (file_exists($destination) && ! $this->option('force')) { + $this->components->error("The '{$name}' configuration file already exists."); + + return; + } + + copy($file, $destination); + + $this->components->info("Published '{$name}' configuration file."); + } + + /** + * Get an array containing the base configuration files. + * + * @return array + */ + protected function getBaseConfigurationFiles() + { + $config = []; + + foreach (Finder::create()->files()->name('*.php')->in(__DIR__.'/../../../../config') as $file) { + $config[basename($file->getRealPath(), '.php')] = $file->getRealPath(); + } + + return collect($config)->sortKeys()->all(); + } +} diff --git a/src/Illuminate/Foundation/Console/InteractsWithComposerPackages.php b/src/Illuminate/Foundation/Console/InteractsWithComposerPackages.php new file mode 100644 index 000000000000..ebd8b7e7b3d1 --- /dev/null +++ b/src/Illuminate/Foundation/Console/InteractsWithComposerPackages.php @@ -0,0 +1,44 @@ +phpBinary(), $composer, 'require']; + } + + $command = array_merge( + $command ?? ['composer', 'require'], + $packages, + ); + + return ! (new Process($command, $this->laravel->basePath(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) + ->setTimeout(null) + ->run(function ($type, $output) { + $this->output->write($output); + }); + } + + /** + * Get the path to the appropriate PHP binary. + * + * @return string + */ + protected function phpBinary() + { + return (new PhpExecutableFinder())->find(false) ?: 'php'; + } +} diff --git a/src/Illuminate/Foundation/Console/Kernel.php b/src/Illuminate/Foundation/Console/Kernel.php index 227f7caea513..21a82f7c927b 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -67,6 +67,20 @@ class Kernel implements KernelContract */ protected $commands = []; + /** + * The paths where Artisan commands should be automatically discovered. + * + * @var array + */ + protected $commandPaths = []; + + /** + * The paths where Artisan "routes" should be automatically discovered. + * + * @var array + */ + protected $commandRoutePaths = []; + /** * Indicates if the Closure commands have been loaded. * @@ -74,6 +88,13 @@ class Kernel implements KernelContract */ protected $commandsLoaded = false; + /** + * The commands paths that have been "loaded". + * + * @var array + */ + protected $loadedPaths = []; + /** * All of the registered command duration handlers. * @@ -123,8 +144,6 @@ public function __construct(Application $app, Dispatcher $events) if (! $this->app->runningUnitTests()) { $this->rerouteSymfonyCommandEvents(); } - - $this->defineConsoleSchedule(); }); } @@ -156,30 +175,6 @@ public function rerouteSymfonyCommandEvents() return $this; } - /** - * Define the application's command schedule. - * - * @return void - */ - protected function defineConsoleSchedule() - { - $this->app->singleton(Schedule::class, function ($app) { - return tap(new Schedule($this->scheduleTimezone()), function ($schedule) { - $this->schedule($schedule->useCache($this->scheduleCache())); - }); - }); - } - - /** - * Get the name of the cache store that should manage scheduling mutexes. - * - * @return string - */ - protected function scheduleCache() - { - return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER')); - } - /** * Run the console application. * @@ -280,6 +275,18 @@ protected function schedule(Schedule $schedule) // } + /** + * Resolve a console schedule instance. + * + * @return \Illuminate\Console\Scheduling\Schedule + */ + public function resolveConsoleSchedule() + { + return tap(new Schedule($this->scheduleTimezone()), function ($schedule) { + $this->schedule($schedule->useCache($this->scheduleCache())); + }); + } + /** * Get the timezone that should be used by default for scheduled events. * @@ -292,6 +299,16 @@ protected function scheduleTimezone() return $config->get('app.schedule_timezone', $config->get('app.timezone')); } + /** + * Get the name of the cache store that should manage scheduling mutexes. + * + * @return string + */ + protected function scheduleCache() + { + return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER')); + } + /** * Register the commands for the application. * @@ -338,6 +355,10 @@ protected function load($paths) return; } + $this->loadedPaths = array_values( + array_unique(array_merge($this->loadedPaths, $paths)) + ); + $namespace = $this->app->getNamespace(); foreach ((new Finder)->in($paths)->files() as $file) { @@ -452,10 +473,32 @@ public function bootstrap() if (! $this->commandsLoaded) { $this->commands(); + if ($this->shouldDiscoverCommands()) { + $this->discoverCommands(); + } + $this->commandsLoaded = true; } } + /** + * Discover the commands that should be automatically loaded. + * + * @return void + */ + protected function discoverCommands() + { + foreach ($this->commandPaths as $path) { + $this->load($path); + } + + foreach ($this->commandRoutePaths as $path) { + if (file_exists($path)) { + require $path; + } + } + } + /** * Bootstrap the application without booting service providers. * @@ -470,6 +513,16 @@ public function bootstrapWithoutBootingProviders() ); } + /** + * Determine if the kernel should discover commands. + * + * @return bool + */ + protected function shouldDiscoverCommands() + { + return get_class($this) === __CLASS__; + } + /** * Get the Artisan application instance. * @@ -502,6 +555,45 @@ public function setArtisan($artisan) $this->artisan = $artisan; } + /** + * Set the Artisan commands provided by the application. + * + * @param array $commands + * @return $this + */ + public function addCommands(array $commands) + { + $this->commands = array_values(array_unique(array_merge($this->commands, $commands))); + + return $this; + } + + /** + * Set the paths that should have their Artisan commands automatically discovered. + * + * @param array $paths + * @return $this + */ + public function addCommandPaths(array $paths) + { + $this->commandPaths = array_values(array_unique(array_merge($this->commandPaths, $paths))); + + return $this; + } + + /** + * Set the paths that should have their Artisan "routes" automatically discovered. + * + * @param array $paths + * @return $this + */ + public function addCommandRoutePaths(array $paths) + { + $this->commandRoutePaths = array_values(array_unique(array_merge($this->commandRoutePaths, $paths))); + + return $this; + } + /** * Get the bootstrap classes for the application. * diff --git a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php index 7e932d7924d9..c75ab3a95902 100644 --- a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php @@ -32,12 +32,12 @@ public function handle() $this->components->info('Clearing cached bootstrap files.'); collect([ - 'events' => fn () => $this->callSilent('event:clear') == 0, - 'views' => fn () => $this->callSilent('view:clear') == 0, 'cache' => fn () => $this->callSilent('cache:clear') == 0, - 'route' => fn () => $this->callSilent('route:clear') == 0, - 'config' => fn () => $this->callSilent('config:clear') == 0, 'compiled' => fn () => $this->callSilent('clear-compiled') == 0, + 'config' => fn () => $this->callSilent('config:clear') == 0, + 'events' => fn () => $this->callSilent('event:clear') == 0, + 'route' => fn () => $this->callSilent('route:clear') == 0, + 'views' => fn () => $this->callSilent('view:clear') == 0, ])->each(fn ($task, $description) => $this->components->task($description, $task)); $this->newLine(); diff --git a/src/Illuminate/Foundation/Console/OptimizeCommand.php b/src/Illuminate/Foundation/Console/OptimizeCommand.php index 10f264628542..0bcf3e97a3a6 100644 --- a/src/Illuminate/Foundation/Console/OptimizeCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeCommand.php @@ -20,7 +20,7 @@ class OptimizeCommand extends Command * * @var string */ - protected $description = 'Cache the framework bootstrap files'; + protected $description = 'Cache framework bootstrap, configuration, and metadata to increase performance'; /** * Execute the console command. @@ -29,11 +29,13 @@ class OptimizeCommand extends Command */ public function handle() { - $this->components->info('Caching the framework bootstrap files'); + $this->components->info('Caching framework bootstrap, configuration, and metadata.'); collect([ 'config' => fn () => $this->callSilent('config:cache') == 0, + 'events' => fn () => $this->callSilent('event:cache') == 0, 'routes' => fn () => $this->callSilent('route:cache') == 0, + 'views' => fn () => $this->callSilent('view:cache') == 0, ])->each(fn ($task, $description) => $this->components->task($description, $task)); $this->newLine(); diff --git a/src/Illuminate/Foundation/Console/ProviderMakeCommand.php b/src/Illuminate/Foundation/Console/ProviderMakeCommand.php index 54d7e4c06486..d955c8254d7b 100644 --- a/src/Illuminate/Foundation/Console/ProviderMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ProviderMakeCommand.php @@ -3,6 +3,7 @@ namespace Illuminate\Foundation\Console; use Illuminate\Console\GeneratorCommand; +use Illuminate\Support\ServiceProvider; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; @@ -30,6 +31,29 @@ class ProviderMakeCommand extends GeneratorCommand */ protected $type = 'Provider'; + /** + * Execute the console command. + * + * @return bool|null + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function handle() + { + $result = parent::handle(); + + if ($result === false) { + return $result; + } + + ServiceProvider::addProviderToBootstrapFile( + $this->qualifyClass($this->getNameInput()), + $this->laravel->getBootstrapProvidersPath(), + ); + + return $result; + } + /** * Get the stub file for the generator. * diff --git a/src/Illuminate/Foundation/Console/stubs/api-routes.stub b/src/Illuminate/Foundation/Console/stubs/api-routes.stub new file mode 100644 index 000000000000..aee5f3636395 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/api-routes.stub @@ -0,0 +1,20 @@ +user(); +})->middleware(Authenticate::using('sanctum')); diff --git a/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub b/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub new file mode 100644 index 000000000000..5d451e1fae88 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub @@ -0,0 +1,18 @@ +id === (int) $id; +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-bootstrap-js.stub b/src/Illuminate/Foundation/Console/stubs/echo-bootstrap-js.stub new file mode 100644 index 000000000000..e932c573c3a3 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-bootstrap-js.stub @@ -0,0 +1,7 @@ +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allow your team to quickly build robust real-time web applications. + */ + +import './echo'; diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js.stub b/src/Illuminate/Foundation/Console/stubs/echo-js.stub new file mode 100644 index 000000000000..520887d57d4a --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js.stub @@ -0,0 +1,15 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: 'pusher', + key: import.meta.env.VITE_PUSHER_APP_KEY, + cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', + wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, + wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, + wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, + forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', + enabledTransports: ['ws', 'wss'], +}); diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index c877e772df3c..facb54fe54c1 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -80,6 +80,20 @@ class Handler implements ExceptionHandlerContract */ protected $levels = []; + /** + * The callbacks that should be used to throttle reportable exceptions. + * + * @var array + */ + protected $throttleCallbacks = []; + + /** + * The callbacks that should be used to build exception context data. + * + * @var array + */ + protected $contextCallbacks = []; + /** * The callbacks that should be used during rendering. * @@ -232,6 +246,19 @@ public function map($from, $to = null) return $this; } + /** + * Indicate that the given exception type should not be reported. + * + * Alias of "ignore". + * + * @param string $class + * @return $this + */ + public function dontReport(string $class) + { + return $this->ignore($class); + } + /** * Indicate that the given exception type should not be reported. * @@ -245,6 +272,21 @@ public function ignore(string $class) return $this; } + /** + * Indicate that the given attributes should never be flashed to the session on validation errors. + * + * @param array|string $attributes + * @return $this + */ + public function dontFlash($attributes) + { + $this->dontFlash = array_values(array_unique( + array_merge($this->dontFlash, Arr::wrap($attributes)) + )); + + return $this; + } + /** * Set the log level for the given exception type. * @@ -373,9 +415,38 @@ protected function shouldntReport(Throwable $e) */ protected function throttle(Throwable $e) { + foreach ($this->throttleCallbacks as $throttleCallback) { + foreach ($this->firstClosureParameterTypes($throttleCallback) as $type) { + if (is_a($e, $type)) { + $response = $throttleCallback($e); + + if (! is_null($response)) { + return $response; + } + } + } + } + return Limit::none(); } + /** + * Specify the callback that should be used to throttle reportable exceptions. + * + * @param callable $throttleUsing + * @return $this + */ + public function throttleUsing(callable $throttleUsing) + { + if (! $throttleUsing instanceof Closure) { + $throttleUsing = Closure::fromCallable($throttleUsing); + } + + $this->throttleCallbacks[] = $throttleUsing; + + return $this; + } + /** * Remove the given exception class from the list of exceptions that should be ignored. * @@ -416,11 +487,17 @@ protected function buildExceptionContext(Throwable $e) */ protected function exceptionContext(Throwable $e) { + $context = []; + if (method_exists($e, 'context')) { - return $e->context(); + $context = $e->context(); } - return []; + foreach ($this->contextCallbacks as $callback) { + $context = array_merge($context, $callback($e, $context)); + } + + return $context; } /** @@ -439,6 +516,19 @@ protected function context() } } + /** + * Register a closure that should be used to build exception context data. + * + * @param \Closure $contextCallback + * @return $this + */ + public function buildContextUsing(Closure $contextCallback) + { + $this->contextCallbacks[] = $contextCallback; + + return $this; + } + /** * Render an exception into an HTTP response. * @@ -567,7 +657,7 @@ protected function unauthenticated($request, AuthenticationException $exception) { return $this->shouldReturnJson($request, $exception) ? response()->json(['message' => $exception->getMessage()], 401) - : redirect()->guest($exception->redirectTo() ?? route('login')); + : redirect()->guest($exception->redirectTo($request) ?? route('login')); } /** diff --git a/src/Illuminate/Foundation/Http/Kernel.php b/src/Illuminate/Foundation/Http/Kernel.php index 51ad7929aa55..2ddb1c58c8bf 100644 --- a/src/Illuminate/Foundation/Http/Kernel.php +++ b/src/Illuminate/Foundation/Http/Kernel.php @@ -509,6 +509,31 @@ protected function renderException($request, Throwable $e) return $this->app[ExceptionHandler::class]->render($request, $e); } + /** + * Get the application's global middleware. + * + * @return array + */ + public function getGlobalMiddleware() + { + return $this->middleware; + } + + /** + * Set the application's global middleware. + * + * @param array $middleware + * @return $this + */ + public function setGlobalMiddleware(array $middleware) + { + $this->middleware = $middleware; + + $this->syncMiddlewareToRouter(); + + return $this; + } + /** * Get the application's route middleware groups. * @@ -519,6 +544,21 @@ public function getMiddlewareGroups() return $this->middlewareGroups; } + /** + * Set the application's middleware groups. + * + * @param array $groups + * @return $this + */ + public function setMiddlewareGroups(array $groups) + { + $this->middlewareGroups = $groups; + + $this->syncMiddlewareToRouter(); + + return $this; + } + /** * Get the application's route middleware aliases. * @@ -541,6 +581,21 @@ public function getMiddlewareAliases() return array_merge($this->routeMiddleware, $this->middlewareAliases); } + /** + * Set the application's route middleware aliases. + * + * @param array $aliases + * @return $this + */ + public function setMiddlewareAliases(array $aliases) + { + $this->middlewareAliases = $aliases; + + $this->syncMiddlewareToRouter(); + + return $this; + } + /** * Get the Laravel application instance. * diff --git a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php index 7ffcd5eaa1f7..a6e654659a9e 100644 --- a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php +++ b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php @@ -3,24 +3,34 @@ namespace Illuminate\Foundation\Http\Middleware; use Closure; +use Illuminate\Support\Arr; class TrimStrings extends TransformsRequest { /** - * All of the registered skip callbacks. + * The attributes that should not be trimmed. + * + * @var array + */ + protected $except = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * The globally ignored attributes that should not be trimmed. * * @var array */ - protected static $skipCallbacks = []; + protected static $neverTrim = []; /** - * The attributes that should not be trimmed. + * All of the registered skip callbacks. * - * @var array + * @var array */ - protected $except = [ - // - ]; + protected static $skipCallbacks = []; /** * Handle an incoming request. @@ -49,13 +59,28 @@ public function handle($request, Closure $next) */ protected function transform($key, $value) { - if (in_array($key, $this->except, true) || ! is_string($value)) { + $except = array_merge($this->except, static::$neverTrim); + + if (in_array($key, $except, true) || ! is_string($value)) { return $value; } return preg_replace('~^[\s\x{FEFF}\x{200B}]+|[\s\x{FEFF}\x{200B}]+$~u', '', $value) ?? trim($value); } + /** + * Indicate that the given attributes should never be trimmed. + * + * @param array|string $attributes + * @return void + */ + public static function except($attributes) + { + static::$neverTrim = array_values(array_unique( + array_merge(static::$neverTrim, Arr::wrap($attributes)) + )); + } + /** * Register a callback that instructs the middleware to be skipped. * diff --git a/src/Illuminate/Foundation/Http/Middleware/ValidateCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/ValidateCsrfToken.php new file mode 100644 index 000000000000..f49d6141f7d7 --- /dev/null +++ b/src/Illuminate/Foundation/Http/Middleware/ValidateCsrfToken.php @@ -0,0 +1,11 @@ +getPostMaxSize(); - - if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) { - throw new PostTooLargeException; - } - - return $next($request); - } - - /** - * Determine the server 'post_max_size' as bytes. - * - * @return int - */ - protected function getPostMaxSize() - { - if (is_numeric($postMaxSize = ini_get('post_max_size'))) { - return (int) $postMaxSize; - } - - $metric = strtoupper(substr($postMaxSize, -1)); - $postMaxSize = (int) $postMaxSize; - - return match ($metric) { - 'K' => $postMaxSize * 1024, - 'M' => $postMaxSize * 1048576, - 'G' => $postMaxSize * 1073741824, - default => $postMaxSize, - }; - } + // } diff --git a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php index 69faa52726d2..3111be5a9ba3 100644 --- a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php +++ b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php @@ -10,6 +10,7 @@ use Illuminate\Cookie\CookieValuePrefix; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Session\TokenMismatchException; +use Illuminate\Support\Arr; use Illuminate\Support\InteractsWithTime; use Symfony\Component\HttpFoundation\Cookie; @@ -38,6 +39,13 @@ class VerifyCsrfToken */ protected $except = []; + /** + * The globally ignored URIs that should be excluded from CSRF verification. + * + * @var array + */ + protected static $neverVerify = []; + /** * Indicates whether the XSRF-TOKEN cookie should be set on the response. * @@ -114,7 +122,7 @@ protected function runningUnitTests() */ protected function inExceptArray($request) { - foreach ($this->except as $except) { + foreach (array_merge($this->except, static::$neverVerify) as $except) { if ($except !== '/') { $except = trim($except, '/'); } @@ -215,6 +223,19 @@ protected function newCookie($request, $config) ); } + /** + * Indicate that the given URIs should be excluded from CSRF verification. + * + * @param array|string $paths + * @return void + */ + public static function except($paths) + { + static::$neverVerify = array_values(array_unique( + array_merge(static::$neverVerify, Arr::wrap($paths)) + )); + } + /** * Determine if the cookie contents should be serialized. * diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index 7269e90ede95..eab3b0d97adb 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -28,6 +28,8 @@ use Illuminate\Database\Console\TableCommand as DatabaseTableCommand; use Illuminate\Database\Console\WipeCommand; use Illuminate\Foundation\Console\AboutCommand; +use Illuminate\Foundation\Console\ApiInstallCommand; +use Illuminate\Foundation\Console\BroadcastingInstallCommand; use Illuminate\Foundation\Console\CastMakeCommand; use Illuminate\Foundation\Console\ChannelListCommand; use Illuminate\Foundation\Console\ChannelMakeCommand; @@ -35,6 +37,7 @@ use Illuminate\Foundation\Console\ComponentMakeCommand; use Illuminate\Foundation\Console\ConfigCacheCommand; use Illuminate\Foundation\Console\ConfigClearCommand; +use Illuminate\Foundation\Console\ConfigPublishCommand; use Illuminate\Foundation\Console\ConfigShowCommand; use Illuminate\Foundation\Console\ConsoleMakeCommand; use Illuminate\Foundation\Console\DocsCommand; @@ -169,11 +172,14 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid * @var array */ protected $devCommands = [ + 'ApiInstall' => ApiInstallCommand::class, + 'BroadcastingInstall' => BroadcastingInstallCommand::class, 'CacheTable' => CacheTableCommand::class, 'CastMake' => CastMakeCommand::class, 'ChannelList' => ChannelListCommand::class, 'ChannelMake' => ChannelMakeCommand::class, 'ComponentMake' => ComponentMakeCommand::class, + 'ConfigPublish' => ConfigPublishCommand::class, 'ConsoleMake' => ConsoleMakeCommand::class, 'ControllerMake' => ControllerMakeCommand::class, 'Docs' => DocsCommand::class, @@ -356,6 +362,18 @@ protected function registerConfigClearCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerConfigPublishCommand() + { + $this->app->singleton(ConfigPublishCommand::class, function ($app) { + return new ConfigPublishCommand; + }); + } + /** * Register the command. * diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 1d5ed5ff03c8..2134171e0ee7 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -2,6 +2,8 @@ namespace Illuminate\Foundation\Providers; +use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract; @@ -69,6 +71,7 @@ public function register() { parent::register(); + $this->registerConsoleSchedule(); $this->registerDumper(); $this->registerRequestValidation(); $this->registerRequestSignatureValidation(); @@ -76,6 +79,18 @@ public function register() $this->registerMaintenanceModeManager(); } + /** + * Register the console schedule implementation. + * + * @return void + */ + public function registerConsoleSchedule() + { + $this->app->singleton(Schedule::class, function ($app) { + return $app->make(ConsoleKernel::class)->resolveConsoleSchedule(); + }); + } + /** * Register a var dumper (with source) to debug variables. * diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index d966c5fd6d23..6dd42b16ff9c 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -2,7 +2,10 @@ namespace Illuminate\Foundation\Support\Providers; +use Illuminate\Auth\Events\Registered; +use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Events\DiscoverEvents; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; @@ -53,6 +56,10 @@ public function register() $model::observe($observers); } }); + + $this->booted(function () { + $this->configureEmailVerification(); + }); } /** @@ -113,7 +120,7 @@ protected function discoveredEvents() */ public function shouldDiscoverEvents() { - return false; + return get_class($this) === __CLASS__; } /** @@ -156,4 +163,17 @@ protected function eventDiscoveryBasePath() { return base_path(); } + + /** + * Configure the proper event listeners for email verification. + * + * @return void + */ + protected function configureEmailVerification() + { + if (! isset($this->listen[Registered::class]) || + ! in_array(SendEmailVerificationNotification::class, Arr::wrap($this->listen[Registered::class]))) { + Event::listen(Registered::class, SendEmailVerificationNotification::class); + } + } } diff --git a/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php index c8679e51ede6..9c9ba4f90be6 100644 --- a/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php @@ -29,6 +29,13 @@ class RouteServiceProvider extends ServiceProvider */ protected $loadRoutesUsing; + /** + * The global callback that should be used to load the application's routes. + * + * @var \Closure|null + */ + protected static $alwaysLoadRoutesUsing; + /** * Register any application services. * @@ -75,6 +82,17 @@ protected function routes(Closure $routesCallback) return $this; } + /** + * Register the callback that will be used to load the application's routes. + * + * @param \Closure $routesCallback + * @return void + */ + public static function loadRoutesUsing(Closure $routesCallback) + { + static::$alwaysLoadRoutesUsing = $routesCallback; + } + /** * Set the root controller namespace for the application. * @@ -116,7 +134,9 @@ protected function loadCachedRoutes() */ protected function loadRoutes() { - if (! is_null($this->loadRoutesUsing)) { + if (! is_null(static::$alwaysLoadRoutesUsing)) { + $this->app->call(static::$alwaysLoadRoutesUsing); + } elseif (! is_null($this->loadRoutesUsing)) { $this->app->call($this->loadRoutesUsing); } elseif (method_exists($this, 'map')) { $this->app->call([$this, 'map']); diff --git a/src/Illuminate/Http/Middleware/TrustHosts.php b/src/Illuminate/Http/Middleware/TrustHosts.php index 00a5a44c2e92..c2657f02756e 100644 --- a/src/Illuminate/Http/Middleware/TrustHosts.php +++ b/src/Illuminate/Http/Middleware/TrustHosts.php @@ -5,7 +5,7 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Http\Request; -abstract class TrustHosts +class TrustHosts { /** * The application instance. @@ -30,7 +30,12 @@ public function __construct(Application $app) * * @return array */ - abstract public function hosts(); + public function hosts() + { + return [ + $this->allSubdomainsOfApplicationUrl(), + ]; + } /** * Handle the incoming request. diff --git a/src/Illuminate/Http/Middleware/TrustProxies.php b/src/Illuminate/Http/Middleware/TrustProxies.php index 81906c1f1951..12a2f9b3692e 100644 --- a/src/Illuminate/Http/Middleware/TrustProxies.php +++ b/src/Illuminate/Http/Middleware/TrustProxies.php @@ -12,14 +12,18 @@ class TrustProxies * * @var array|string|null */ - protected $proxies; + protected $proxies = '*'; /** * The proxy header mappings. * * @var int */ - protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PREFIX | Request::HEADER_X_FORWARDED_AWS_ELB; + protected $headers = Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB; /** * Handle an incoming request. diff --git a/src/Illuminate/Http/Middleware/ValidatePostSize.php b/src/Illuminate/Http/Middleware/ValidatePostSize.php new file mode 100644 index 000000000000..bfa620a85f46 --- /dev/null +++ b/src/Illuminate/Http/Middleware/ValidatePostSize.php @@ -0,0 +1,52 @@ +getPostMaxSize(); + + if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) { + throw new PostTooLargeException; + } + + return $next($request); + } + + /** + * Determine the server 'post_max_size' as bytes. + * + * @return int + */ + protected function getPostMaxSize() + { + if (is_numeric($postMaxSize = ini_get('post_max_size'))) { + return (int) $postMaxSize; + } + + $metric = strtoupper(substr($postMaxSize, -1)); + + $postMaxSize = (int) $postMaxSize; + + return match ($metric) { + 'K' => $postMaxSize * 1024, + 'M' => $postMaxSize * 1048576, + 'G' => $postMaxSize * 1073741824, + default => $postMaxSize, + }; + } +} diff --git a/src/Illuminate/Notifications/Console/NotificationTableCommand.php b/src/Illuminate/Notifications/Console/NotificationTableCommand.php index 4df79e5371d8..d2329f914620 100644 --- a/src/Illuminate/Notifications/Console/NotificationTableCommand.php +++ b/src/Illuminate/Notifications/Console/NotificationTableCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\MigrationGeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'notifications:table')] +#[AsCommand(name: 'make:notifications-table')] class NotificationTableCommand extends MigrationGeneratorCommand { /** @@ -13,7 +13,14 @@ class NotificationTableCommand extends MigrationGeneratorCommand * * @var string */ - protected $name = 'notifications:table'; + protected $name = 'make:notifications-table'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['notifications:table']; /** * The console command description. diff --git a/src/Illuminate/Queue/Console/BatchesTableCommand.php b/src/Illuminate/Queue/Console/BatchesTableCommand.php index 56c1158ab903..92a83ef5ea40 100644 --- a/src/Illuminate/Queue/Console/BatchesTableCommand.php +++ b/src/Illuminate/Queue/Console/BatchesTableCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\MigrationGeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'queue:batches-table')] +#[AsCommand(name: 'make:queue-batches-table')] class BatchesTableCommand extends MigrationGeneratorCommand { /** @@ -13,7 +13,14 @@ class BatchesTableCommand extends MigrationGeneratorCommand * * @var string */ - protected $name = 'queue:batches-table'; + protected $name = 'make:queue-batches-table'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['queue:batches-table']; /** * The console command description. diff --git a/src/Illuminate/Queue/Console/FailedTableCommand.php b/src/Illuminate/Queue/Console/FailedTableCommand.php index 041196536443..3e6f00751c6f 100644 --- a/src/Illuminate/Queue/Console/FailedTableCommand.php +++ b/src/Illuminate/Queue/Console/FailedTableCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\MigrationGeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'queue:failed-table')] +#[AsCommand(name: 'make:queue-failed-table')] class FailedTableCommand extends MigrationGeneratorCommand { /** @@ -13,7 +13,14 @@ class FailedTableCommand extends MigrationGeneratorCommand * * @var string */ - protected $name = 'queue:failed-table'; + protected $name = 'make:queue-failed-table'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['queue:failed-table']; /** * The console command description. diff --git a/src/Illuminate/Queue/Console/TableCommand.php b/src/Illuminate/Queue/Console/TableCommand.php index bb1fe00a9421..24e5be54d962 100644 --- a/src/Illuminate/Queue/Console/TableCommand.php +++ b/src/Illuminate/Queue/Console/TableCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\MigrationGeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'queue:table')] +#[AsCommand(name: 'make:queue-table')] class TableCommand extends MigrationGeneratorCommand { /** @@ -13,7 +13,14 @@ class TableCommand extends MigrationGeneratorCommand * * @var string */ - protected $name = 'queue:table'; + protected $name = 'make:queue-table'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['queue:table']; /** * The console command description. diff --git a/src/Illuminate/Routing/Middleware/ValidateSignature.php b/src/Illuminate/Routing/Middleware/ValidateSignature.php index b7bf23b31fc7..2ce5ae154ccb 100644 --- a/src/Illuminate/Routing/Middleware/ValidateSignature.php +++ b/src/Illuminate/Routing/Middleware/ValidateSignature.php @@ -17,6 +17,13 @@ class ValidateSignature // ]; + /** + * The globally ignored parameters. + * + * @var array + */ + protected static $neverValidate = []; + /** * Specify that the URL signature is for a relative URL. * @@ -85,6 +92,19 @@ protected function parseArguments(array $args) $args ); - return [$relative, $ignore]; + return [$relative, array_merge($ignore, static::$neverValidate)]; + } + + /** + * Indicate that the given parameters should be ignored during signature validation. + * + * @param array|string $parameters + * @return void + */ + public static function except($parameters) + { + static::$neverValidate = array_values(array_unique( + array_merge(static::$neverValidate, Arr::wrap($parameters)) + )); } } diff --git a/src/Illuminate/Session/Console/SessionTableCommand.php b/src/Illuminate/Session/Console/SessionTableCommand.php index c020f2ac64b7..d162f3e7ce37 100644 --- a/src/Illuminate/Session/Console/SessionTableCommand.php +++ b/src/Illuminate/Session/Console/SessionTableCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\MigrationGeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand(name: 'session:table')] +#[AsCommand(name: 'make:session-table')] class SessionTableCommand extends MigrationGeneratorCommand { /** @@ -13,7 +13,14 @@ class SessionTableCommand extends MigrationGeneratorCommand * * @var string */ - protected $name = 'session:table'; + protected $name = 'make:session-table'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['session:table']; /** * The console command description. diff --git a/src/Illuminate/Session/Middleware/AuthenticateSession.php b/src/Illuminate/Session/Middleware/AuthenticateSession.php index 43982374b456..d525e8f535e1 100644 --- a/src/Illuminate/Session/Middleware/AuthenticateSession.php +++ b/src/Illuminate/Session/Middleware/AuthenticateSession.php @@ -17,6 +17,13 @@ class AuthenticateSession implements AuthenticatesSessions */ protected $auth; + /** + * The callback that should be used to generate the authentication redirect path. + * + * @var callable + */ + protected static $redirectToCallback; + /** * Create a new middleware instance. * @@ -118,6 +125,19 @@ protected function guard() */ protected function redirectTo(Request $request) { - // + if (static::$redirectToCallback) { + return call_user_func(static::$redirectToCallback, $request); + } + } + + /** + * Specify the callback that should be used to generate the redirect path. + * + * @param callable $redirectToCallback + * @return void + */ + public static function redirectUsing(callable $redirectToCallback) + { + static::$redirectToCallback = $redirectToCallback; } } diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index a2b23eb49453..bd53f28ee085 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -3,6 +3,7 @@ namespace Illuminate\Support\Facades; /** + * @method static void configure(string|null $baseDirectory = null) * @method static string version() * @method static void bootstrapWith(string[] $bootstrappers) * @method static void afterLoadingEnvironment(\Closure $callback) @@ -14,6 +15,7 @@ * @method static \Illuminate\Foundation\Application useAppPath(string $path) * @method static string basePath(string $path = '') * @method static string bootstrapPath(string $path = '') + * @method static string getBootstrapProvidersPath() * @method static \Illuminate\Foundation\Application useBootstrapPath(string $path) * @method static string configPath(string $path = '') * @method static \Illuminate\Foundation\Application useConfigPath(string $path) @@ -41,6 +43,7 @@ * @method static bool runningConsoleCommand(string|array ...$commands) * @method static bool runningUnitTests() * @method static bool hasDebugModeEnabled() + * @method static void registered(callable $callback) * @method static void registerConfiguredProviders() * @method static \Illuminate\Support\ServiceProvider register(\Illuminate\Support\ServiceProvider|string $provider, bool $force = false) * @method static \Illuminate\Support\ServiceProvider|null getProvider(\Illuminate\Support\ServiceProvider|string $provider) @@ -56,6 +59,8 @@ * @method static void booting(callable $callback) * @method static void booted(callable $callback) * @method static \Symfony\Component\HttpFoundation\Response handle(\Symfony\Component\HttpFoundation\Request $request, int $type = 1, bool $catch = true) + * @method static void handleRequest(\Illuminate\Http\Request $request) + * @method static int handleCommand(\Symfony\Component\Console\Input\InputInterface $input) * @method static bool shouldSkipMiddleware() * @method static string getCachedServicesPath() * @method static string getCachedPackagesPath() diff --git a/src/Illuminate/Support/Facades/Artisan.php b/src/Illuminate/Support/Facades/Artisan.php index 4a0bab379582..d3a0d9d4ea81 100755 --- a/src/Illuminate/Support/Facades/Artisan.php +++ b/src/Illuminate/Support/Facades/Artisan.php @@ -9,6 +9,7 @@ * @method static void terminate(\Symfony\Component\Console\Input\InputInterface $input, int $status) * @method static void whenCommandLifecycleIsLongerThan(\DateTimeInterface|\Carbon\CarbonInterval|float|int $threshold, callable $handler) * @method static \Illuminate\Support\Carbon|null commandStartedAt() + * @method static \Illuminate\Console\Scheduling\Schedule resolveConsoleSchedule() * @method static \Illuminate\Foundation\Console\ClosureCommand command(string $signature, \Closure $callback) * @method static void registerCommand(\Symfony\Component\Console\Command\Command $command) * @method static int call(string $command, array $parameters = [], \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer = null) @@ -18,6 +19,9 @@ * @method static void bootstrap() * @method static void bootstrapWithoutBootingProviders() * @method static void setArtisan(\Illuminate\Console\Application|null $artisan) + * @method static \Illuminate\Foundation\Console\Kernel addCommands(array $commands) + * @method static \Illuminate\Foundation\Console\Kernel addCommandPaths(array $paths) + * @method static \Illuminate\Foundation\Console\Kernel addCommandRoutePaths(array $paths) * * @see \Illuminate\Foundation\Console\Kernel */ diff --git a/src/Illuminate/Support/Facades/Facade.php b/src/Illuminate/Support/Facades/Facade.php index 1dbdc321543b..caba479ee65e 100755 --- a/src/Illuminate/Support/Facades/Facade.php +++ b/src/Illuminate/Support/Facades/Facade.php @@ -303,6 +303,7 @@ public static function defaultAliases() 'Request' => Request::class, 'Response' => Response::class, 'Route' => Route::class, + 'Schedule' => Schedule::class, 'Schema' => Schema::class, 'Session' => Session::class, 'Storage' => Storage::class, diff --git a/src/Illuminate/Support/Facades/Schedule.php b/src/Illuminate/Support/Facades/Schedule.php new file mode 100644 index 000000000000..04c7e0ed516d --- /dev/null +++ b/src/Illuminate/Support/Facades/Schedule.php @@ -0,0 +1,35 @@ +getBootstrapProvidersPath(); + + if (! file_exists($path)) { + return false; + } + + $providers = collect(require $path) + ->merge([$provider]) + ->unique() + ->sort() + ->values() + ->map(fn ($p) => ' '.$p.'::class,') + ->implode(PHP_EOL); + + $content = 'shouldReceive('expectsJson')->andReturn(false); + $nextParam = null; $next = function ($param) use (&$nextParam) { diff --git a/tests/Cookie/Middleware/EncryptCookiesTest.php b/tests/Cookie/Middleware/EncryptCookiesTest.php index 06f3c6c3b05d..352f117db901 100644 --- a/tests/Cookie/Middleware/EncryptCookiesTest.php +++ b/tests/Cookie/Middleware/EncryptCookiesTest.php @@ -42,6 +42,8 @@ protected function setUp(): void }); $this->router = new Router(new Dispatcher, $this->container); + + EncryptCookiesTestMiddleware::except(['globally_unencrypted_cookie']); } public function testSetCookieEncryption() @@ -54,7 +56,7 @@ public function testSetCookieEncryption() $response = $this->router->dispatch(Request::create($this->setCookiePath, 'GET')); $cookies = $response->headers->getCookies(); - $this->assertCount(4, $cookies); + $this->assertCount(5, $cookies); $this->assertSame('encrypted_cookie', $cookies[0]->getName()); $this->assertNotSame('value', $cookies[0]->getValue()); $this->assertSame('encrypted[array_cookie]', $cookies[1]->getName()); @@ -62,6 +64,8 @@ public function testSetCookieEncryption() $this->assertSame('encrypted[nested][array_cookie]', $cookies[2]->getName()); $this->assertSame('unencrypted_cookie', $cookies[3]->getName()); $this->assertSame('value', $cookies[3]->getValue()); + $this->assertSame('globally_unencrypted_cookie', $cookies[4]->getName()); + $this->assertSame('value', $cookies[4]->getValue()); } public function testQueuedCookieEncryption() @@ -74,7 +78,7 @@ public function testQueuedCookieEncryption() $response = $this->router->dispatch(Request::create($this->queueCookiePath, 'GET')); $cookies = $response->headers->getCookies(); - $this->assertCount(4, $cookies); + $this->assertCount(5, $cookies); $this->assertSame('encrypted_cookie', $cookies[0]->getName()); $this->assertNotSame('value', $cookies[0]->getValue()); $this->assertSame('encrypted[array_cookie]', $cookies[1]->getName()); @@ -83,6 +87,8 @@ public function testQueuedCookieEncryption() $this->assertNotSame('value', $cookies[2]->getValue()); $this->assertSame('unencrypted_cookie', $cookies[3]->getName()); $this->assertSame('value', $cookies[3]->getValue()); + $this->assertSame('globally_unencrypted_cookie', $cookies[4]->getName()); + $this->assertSame('value', $cookies[4]->getValue()); } protected function getEncryptedCookieValue($key, $value) @@ -106,13 +112,14 @@ public function testCookieDecryption() ], ], 'unencrypted_cookie' => 'value', + 'globally_unencrypted_cookie' => 'value', ]; $this->container->make(EncryptCookiesTestMiddleware::class)->handle( Request::create('/cookie/read', 'GET', [], $cookies), function ($request) { $cookies = $request->cookies->all(); - $this->assertCount(3, $cookies); + $this->assertCount(4, $cookies); $this->assertArrayHasKey('encrypted_cookie', $cookies); $this->assertSame('value', $cookies['encrypted_cookie']); $this->assertArrayHasKey('encrypted', $cookies); @@ -123,6 +130,8 @@ function ($request) { $this->assertSame('value', $cookies['encrypted']['nested']['array_cookie']); $this->assertArrayHasKey('unencrypted_cookie', $cookies); $this->assertSame('value', $cookies['unencrypted_cookie']); + $this->assertArrayHasKey('globally_unencrypted_cookie', $cookies); + $this->assertSame('value', $cookies['globally_unencrypted_cookie']); return new Response; } @@ -139,6 +148,7 @@ public function setCookies() $response->headers->setCookie(new Cookie('encrypted[array_cookie]', 'value')); $response->headers->setCookie(new Cookie('encrypted[nested][array_cookie]', 'value')); $response->headers->setCookie(new Cookie('unencrypted_cookie', 'value')); + $response->headers->setCookie(new Cookie('globally_unencrypted_cookie', 'value')); return $response; } @@ -165,6 +175,7 @@ public function __construct() $cookie->queue(new Cookie('encrypted[array_cookie]', 'value')); $cookie->queue(new Cookie('encrypted[nested][array_cookie]', 'value')); $cookie->queue(new Cookie('unencrypted_cookie', 'value')); + $cookie->queue(new Cookie('globally_unencrypted_cookie', 'value')); $this->cookies = $cookie; } diff --git a/tests/Http/Middleware/TrimStringsTest.php b/tests/Http/Middleware/TrimStringsTest.php index cbc4109c5d02..98302534b6a5 100644 --- a/tests/Http/Middleware/TrimStringsTest.php +++ b/tests/Http/Middleware/TrimStringsTest.php @@ -44,6 +44,23 @@ public function test_leading_zero_width_space_character_is_trimmed() }); } + public function test_trim_strings_can_globally_ignore_certain_inputs() + { + $request = new Request; + + $request->merge([ + 'globally_ignored_title' => ' test title ', + ]); + + TrimStrings::except(['globally_ignored_title']); + + $middleware = new TrimStrings; + + $middleware->handle($request, function ($req) { + $this->assertEquals(' test title ', $req->globally_ignored_title); + }); + } + /** * Test trailing zero-width space character is trimmed [ZWSP]. */ diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 83c98f525d34..75ba79f9ce0a 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -6,6 +6,7 @@ use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\ScheduleListCommand; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\ProcessUtils; use Orchestra\Testbench\TestCase; @@ -129,6 +130,30 @@ public function testDisplayScheduleSubMinute() ->expectsOutput(' * * * * * 30s php artisan inspire ........... Next Due: 30 seconds from now'); } + public function testClosureCommandsMayBeScheduled() + { + $closure = function () { + }; + + Artisan::command('one', $closure)->weekly()->everySecond(); + Artisan::command('two', $closure)->everyTwoSeconds(); + Artisan::command('three', $closure)->everyFiveSeconds(); + Artisan::command('four', $closure)->everyTenSeconds(); + Artisan::command('five', $closure)->everyFifteenSeconds(); + Artisan::command('six', $closure)->everyTwentySeconds()->hourly(); + Artisan::command('six', $closure)->everyThreeHours()->everySecond(); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutput(' * 0 * * 0 1s php artisan one ............... Next Due: 1 second from now') + ->expectsOutput(' * * * * * 2s php artisan two .............. Next Due: 2 seconds from now') + ->expectsOutput(' * * * * * 5s php artisan three ............ Next Due: 5 seconds from now') + ->expectsOutput(' * * * * * 10s php artisan four ............ Next Due: 10 seconds from now') + ->expectsOutput(' * * * * * 15s php artisan five ............ Next Due: 15 seconds from now') + ->expectsOutput(' 0 * * * * 20s php artisan six ............. Next Due: 20 seconds from now') + ->expectsOutput(' * */3 * * * 1s php artisan six ............... Next Due: 1 second from now'); + } + protected function tearDown(): void { parent::tearDown(); diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 38f2c34d6cc8..96763dfa732f 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -132,9 +132,10 @@ public function testChangeTextColumnToTextColumn() $uppercase = strtoupper($type); - $expected = ["ALTER TABLE test CHANGE test_column test_column $uppercase NOT NULL"]; - - $this->assertEquals($expected, $queries); + $this->assertContains($queries, [ + ["ALTER TABLE test CHANGE test_column test_column $uppercase NOT NULL"], // MySQL + ["ALTER TABLE test CHANGE test_column test_column $uppercase NOT NULL COLLATE `utf8mb4_uca1400_ai_ci`"], // MariaDB + ]); } } diff --git a/tests/Integration/Generators/CacheTableCommandTest.php b/tests/Integration/Generators/CacheTableCommandTest.php index a02c35dc845b..403061605974 100644 --- a/tests/Integration/Generators/CacheTableCommandTest.php +++ b/tests/Integration/Generators/CacheTableCommandTest.php @@ -2,11 +2,13 @@ namespace Illuminate\Tests\Integration\Generators; +use Illuminate\Cache\Console\CacheTableCommand; + class CacheTableCommandTest extends TestCase { public function testCreateMakesMigration() { - $this->artisan('cache:table')->assertExitCode(0); + $this->artisan(CacheTableCommand::class)->assertExitCode(0); $this->assertMigrationFileContains([ 'use Illuminate\Database\Migrations\Migration;', diff --git a/tests/Integration/Generators/NotificationTableCommandTest.php b/tests/Integration/Generators/NotificationTableCommandTest.php index cf956e495c78..334a6612c760 100644 --- a/tests/Integration/Generators/NotificationTableCommandTest.php +++ b/tests/Integration/Generators/NotificationTableCommandTest.php @@ -2,11 +2,13 @@ namespace Illuminate\Tests\Integration\Generators; +use Illuminate\Notifications\Console\NotificationTableCommand; + class NotificationTableCommandTest extends TestCase { public function testCreateMakesMigration() { - $this->artisan('notifications:table')->assertExitCode(0); + $this->artisan(NotificationTableCommand::class)->assertExitCode(0); $this->assertMigrationFileContains([ 'use Illuminate\Database\Migrations\Migration;', diff --git a/tests/Integration/Generators/QueueBatchesTableCommandTest.php b/tests/Integration/Generators/QueueBatchesTableCommandTest.php index d9356fc9304c..4afb4612c03e 100644 --- a/tests/Integration/Generators/QueueBatchesTableCommandTest.php +++ b/tests/Integration/Generators/QueueBatchesTableCommandTest.php @@ -2,11 +2,13 @@ namespace Illuminate\Tests\Integration\Generators; +use Illuminate\Queue\Console\BatchesTableCommand; + class QueueBatchesTableCommandTest extends TestCase { public function testCreateMakesMigration() { - $this->artisan('queue:batches-table')->assertExitCode(0); + $this->artisan(BatchesTableCommand::class)->assertExitCode(0); $this->assertMigrationFileContains([ 'use Illuminate\Database\Migrations\Migration;', diff --git a/tests/Integration/Generators/QueueFailedTableCommandTest.php b/tests/Integration/Generators/QueueFailedTableCommandTest.php index dd1a1d40b7c7..c996da1d834a 100644 --- a/tests/Integration/Generators/QueueFailedTableCommandTest.php +++ b/tests/Integration/Generators/QueueFailedTableCommandTest.php @@ -2,11 +2,13 @@ namespace Illuminate\Tests\Integration\Generators; +use Illuminate\Queue\Console\FailedTableCommand; + class QueueFailedTableCommandTest extends TestCase { public function testCreateMakesMigration() { - $this->artisan('queue:failed-table')->assertExitCode(0); + $this->artisan(FailedTableCommand::class)->assertExitCode(0); $this->assertMigrationFileContains([ 'use Illuminate\Database\Migrations\Migration;', diff --git a/tests/Integration/Generators/QueueTableCommandTest.php b/tests/Integration/Generators/QueueTableCommandTest.php index d70b81ea48ea..ec1b953f16f4 100644 --- a/tests/Integration/Generators/QueueTableCommandTest.php +++ b/tests/Integration/Generators/QueueTableCommandTest.php @@ -2,11 +2,13 @@ namespace Illuminate\Tests\Integration\Generators; +use Illuminate\Queue\Console\TableCommand; + class QueueTableCommandTest extends TestCase { public function testCreateMakesMigration() { - $this->artisan('queue:table')->assertExitCode(0); + $this->artisan(TableCommand::class)->assertExitCode(0); $this->assertMigrationFileContains([ 'use Illuminate\Database\Migrations\Migration;', diff --git a/tests/Integration/Generators/SessionTableCommandTest.php b/tests/Integration/Generators/SessionTableCommandTest.php index cb491893e376..0fc60230cbcf 100644 --- a/tests/Integration/Generators/SessionTableCommandTest.php +++ b/tests/Integration/Generators/SessionTableCommandTest.php @@ -2,11 +2,13 @@ namespace Illuminate\Tests\Integration\Generators; +use Illuminate\Session\Console\SessionTableCommand; + class SessionTableCommandTest extends TestCase { public function testCreateMakesMigration() { - $this->artisan('session:table')->assertExitCode(0); + $this->artisan(SessionTableCommand::class)->assertExitCode(0); $this->assertMigrationFileContains([ 'use Illuminate\Database\Migrations\Migration;', diff --git a/tests/Integration/Http/Middleware/VerifyCsrfTokenExceptTest.php b/tests/Integration/Http/Middleware/VerifyCsrfTokenExceptTest.php index 7bd169a450f9..dfb0055892cb 100644 --- a/tests/Integration/Http/Middleware/VerifyCsrfTokenExceptTest.php +++ b/tests/Integration/Http/Middleware/VerifyCsrfTokenExceptTest.php @@ -15,6 +15,7 @@ protected function setUp(): void { parent::setUp(); + VerifyCsrfTokenExceptStub::except(['/globally/ignored']); $this->stub = new VerifyCsrfTokenExceptStub(app(), new Encrypter(Encrypter::generateKey('AES-128-CBC'))); $this->request = Request::create('http://example.com/foo/bar', 'POST'); } @@ -26,6 +27,12 @@ public function testItCanExceptPaths() $this->assertNonMatchingExcept(['/bar/foo']); } + public function testPathsCanBeGloballyIgnored() + { + $this->request = Request::create('http://example.com/globally/ignored', 'POST'); + $this->assertMatchingExcept(['globally/ignored']); + } + public function testItCanExceptWildcardPaths() { $this->assertMatchingExcept(['/foo/*']); diff --git a/tests/Integration/Routing/UrlSigningTest.php b/tests/Integration/Routing/UrlSigningTest.php index ffa3e433274e..66dd9a87cda5 100644 --- a/tests/Integration/Routing/UrlSigningTest.php +++ b/tests/Integration/Routing/UrlSigningTest.php @@ -270,7 +270,7 @@ public function testSignedMiddlewareWithRelativePath() public function testSignedMiddlewareIgnoringParameter() { - Route::get('/foo/{id}}', function (Request $request, $id) { + Route::get('/foo/{id}', function (Request $request, $id) { })->name('foo')->middleware('signed:relative'); $this->assertIsString($url = URL::signedRoute('foo', ['id' => 1]).'&ignore=me'); @@ -297,6 +297,26 @@ public function testSignedMiddlewareIgnoringParameterViaArgumentsWithRelative() $response->assertStatus(403); } + public function testSignedMiddlewareCanGloballyIgnoreParameters() + { + ValidateSignature::except(['globally_ignore']); + + Route::get('/foo/{id}', function (Request $request, $id) { + })->name('foo')->middleware('signed:relative'); + + $this->assertIsString($url = URL::signedRoute('foo', ['id' => 1]).'&globally_ignore=me'); + $request = Request::create($url); + $middleware = $this->createValidateSignatureMiddleware(['ignore']); + + try { + $middleware->handle($request, function ($request) { + $this->assertTrue($request->hasValidSignatureWhileIgnoring(['globally_ignore'])); + }); + } catch (InvalidSignatureException $exception) { + $this->fail($exception->getMessage()); + } + } + public function testSignedMiddlewareIgnoringParameterViaArgumentsWithoutRelative() { Route::get('/foo/{id}', function (Request $request, $id) { From 8725dff284b708e8988abb8d960a2348a868498a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 29 Nov 2023 11:15:33 +0800 Subject: [PATCH 123/207] [11.x] Update Testbench to `^9.0` (#49167) * Update Testbench to `^9.0` * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki --- composer.json | 2 +- tests/Integration/Foundation/Console/AboutCommandTest.php | 2 +- tests/Integration/Queue/JobEncryptionTest.php | 1 + tests/Integration/Queue/UniqueJobTest.php | 1 + tests/Integration/Queue/WorkCommandTest.php | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ee285ac68d1a..84c53c70428d 100644 --- a/composer.json +++ b/composer.json @@ -106,7 +106,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "dev-next/slim-skeleton", + "orchestra/testbench-core": "^9.0", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.1", diff --git a/tests/Integration/Foundation/Console/AboutCommandTest.php b/tests/Integration/Foundation/Console/AboutCommandTest.php index 6795d3bafeb2..b1311d1b4d91 100644 --- a/tests/Integration/Foundation/Console/AboutCommandTest.php +++ b/tests/Integration/Foundation/Console/AboutCommandTest.php @@ -35,7 +35,7 @@ public function testItCanDisplayAboutCommandAsJson() 'database' => 'testing', 'logs' => ['single'], 'mail' => 'smtp', - 'queue' => 'sync', + 'queue' => 'database', 'session' => 'file', ], $output['drivers']); }); diff --git a/tests/Integration/Queue/JobEncryptionTest.php b/tests/Integration/Queue/JobEncryptionTest.php index 0042e24bf20c..704c08001074 100644 --- a/tests/Integration/Queue/JobEncryptionTest.php +++ b/tests/Integration/Queue/JobEncryptionTest.php @@ -15,6 +15,7 @@ use Illuminate\Tests\Integration\Database\DatabaseTestCase; use Orchestra\Testbench\Attributes\WithMigration; +#[WithMigration] #[WithMigration('queue')] class JobEncryptionTest extends DatabaseTestCase { diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index e0fa5ff9da8e..e4393986cd21 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -15,6 +15,7 @@ use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; +#[WithMigration] #[WithMigration('queue')] class UniqueJobTest extends TestCase { diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 968b1c5d3548..95a2a18fa179 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -11,6 +11,7 @@ use Orchestra\Testbench\TestCase; use Queue; +#[WithMigration] #[WithMigration('queue')] class WorkCommandTest extends TestCase { From d80af24f364ec69fc4cd520afb5263694790892a Mon Sep 17 00:00:00 2001 From: Khalil Laleh Date: Wed, 29 Nov 2023 17:35:12 +0100 Subject: [PATCH 124/207] Add trait to (#49172) --- src/Illuminate/Testing/Fluent/AssertableJson.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Testing/Fluent/AssertableJson.php b/src/Illuminate/Testing/Fluent/AssertableJson.php index fde468e33a87..5fd76d9a7de3 100644 --- a/src/Illuminate/Testing/Fluent/AssertableJson.php +++ b/src/Illuminate/Testing/Fluent/AssertableJson.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; +use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\AssertableJsonString; @@ -16,6 +17,7 @@ class AssertableJson implements Arrayable Concerns\Matching, Concerns\Debugging, Concerns\Interaction, + Conditionable, Macroable, Tappable; From 059759a963e213e3422f0c61e7cf16b0441f8e9f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Nov 2023 00:36:16 +0800 Subject: [PATCH 125/207] [11.x] Add `Illuminate\Bus\BatchRepository::rollBack()` contract. (#49176) Introduced #48961 (minor release for Laravel 10). Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Bus/BatchRepository.php | 10 +++++++--- src/Illuminate/Queue/Jobs/Job.php | 10 ++++------ .../Support/Testing/Fakes/BatchRepositoryFake.php | 10 ++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Bus/BatchRepository.php b/src/Illuminate/Bus/BatchRepository.php index 0e580ca4dcd9..f4b756008e95 100644 --- a/src/Illuminate/Bus/BatchRepository.php +++ b/src/Illuminate/Bus/BatchRepository.php @@ -4,9 +4,6 @@ use Closure; -/** - * @method void rollBack() - */ interface BatchRepository { /** @@ -92,4 +89,11 @@ public function delete(string $batchId); * @return mixed */ public function transaction(Closure $callback); + + /** + * Rollback the last database transaction for the connection. + * + * @return void + */ + public function rollBack(); } diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index 67a80e50c03e..a41b401dddb7 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -197,12 +197,10 @@ public function fail($e = null) in_array(Batchable::class, class_uses_recursive($commandName))) { $batchRepository = $this->resolve(BatchRepository::class); - if (method_exists($batchRepository, 'rollBack')) { - try { - $batchRepository->rollBack(); - } catch (Throwable $e) { - // ... - } + try { + $batchRepository->rollBack(); + } catch (Throwable $e) { + // ... } } diff --git a/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php b/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php index 021c73f27163..2afa4ab94e2c 100644 --- a/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php @@ -150,4 +150,14 @@ public function transaction(Closure $callback) { return $callback(); } + + /** + * Rollback the last database transaction for the connection. + * + * @return void + */ + public function rollBack() + { + // + } } From 3d2c03decf744c36b05496a9b1448296873ec2fa Mon Sep 17 00:00:00 2001 From: Jacob Daniel Prunkl <48543796+jj15asmr@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:47:42 -0500 Subject: [PATCH 126/207] [10.x] Add `--generate-secret` option to Artisan `down` command. (#49171) * Add option to Artisan `down` command to generate a secret phrase. * Revise sentence. * Use `Str::random()` helper instead. * Rename `getSecretPhrase()` to `getSecret()`. * Fix spelling. * Move import statement. * formatting --------- Co-authored-by: Taylor Otwell --- .../Foundation/Console/DownCommand.php | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index 25cbce8624a5..d1af66d19ecf 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -7,6 +7,7 @@ use Illuminate\Console\Command; use Illuminate\Foundation\Events\MaintenanceModeEnabled; use Illuminate\Foundation\Exceptions\RegisterErrorViewPaths; +use Illuminate\Support\Str; use Symfony\Component\Console\Attribute\AsCommand; use Throwable; @@ -23,6 +24,7 @@ class DownCommand extends Command {--retry= : The number of seconds after which the request may be retried} {--refresh= : The number of seconds after which the browser may refresh} {--secret= : The secret phrase that may be used to bypass maintenance mode} + {--with-secret : Generate a random secret phrase that may be used to bypass maintenance mode} {--status=503 : The status code that should be used when returning the maintenance mode response}'; /** @@ -46,7 +48,9 @@ public function handle() return 0; } - $this->laravel->maintenanceMode()->activate($this->getDownFilePayload()); + $downFilePayload = $this->getDownFilePayload(); + + $this->laravel->maintenanceMode()->activate($downFilePayload); file_put_contents( storage_path('framework/maintenance.php'), @@ -56,6 +60,10 @@ public function handle() $this->laravel->get('events')->dispatch(new MaintenanceModeEnabled()); $this->components->info('Application is now in maintenance mode.'); + + if ($downFilePayload['secret'] !== null) { + $this->components->info("You may bypass maintenance mode via [".config('app.url')."/{$downFilePayload['secret']}]."); + } } catch (Exception $e) { $this->components->error(sprintf( 'Failed to enter maintenance mode: %s.', @@ -78,7 +86,7 @@ protected function getDownFilePayload() 'redirect' => $this->redirectPath(), 'retry' => $this->getRetryTime(), 'refresh' => $this->option('refresh'), - 'secret' => $this->option('secret'), + 'secret' => $this->getSecret(), 'status' => (int) $this->option('status', 503), 'template' => $this->option('render') ? $this->prerenderView() : null, ]; @@ -137,4 +145,18 @@ protected function getRetryTime() return is_numeric($retry) && $retry > 0 ? (int) $retry : null; } + + /** + * Get the secret phrase that may be used to bypass maintenance mode. + * + * @return string|null + */ + protected function getSecret() + { + return match (true) { + ! is_null($this->option('secret')) => $this->option('secret'), + $this->option('with-secret') => Str::random(), + default => null, + }; + } } From 0c574d42e9c7d82a0bd304a5cb2ce031a914bfa6 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 29 Nov 2023 16:48:05 +0000 Subject: [PATCH 127/207] Apply fixes from StyleCI --- src/Illuminate/Foundation/Console/DownCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index d1af66d19ecf..10d7dbfd755a 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -62,7 +62,7 @@ public function handle() $this->components->info('Application is now in maintenance mode.'); if ($downFilePayload['secret'] !== null) { - $this->components->info("You may bypass maintenance mode via [".config('app.url')."/{$downFilePayload['secret']}]."); + $this->components->info('You may bypass maintenance mode via ['.config('app.url')."/{$downFilePayload['secret']}]."); } } catch (Exception $e) { $this->components->error(sprintf( From e447ce2fe789687c25098951d002174807b42879 Mon Sep 17 00:00:00 2001 From: Sergey Danilchenko Date: Wed, 29 Nov 2023 22:09:33 +0200 Subject: [PATCH 128/207] [11.x] Add first/last directives to data_get helper (#49175) * [11.x] Add first/last directives to data_get helper * More tests * use first and last --------- Co-authored-by: Sergey Danilchenko Co-authored-by: Taylor Otwell --- src/Illuminate/Collections/helpers.php | 9 ++++ tests/Support/SupportHelpersTest.php | 74 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 235edceb1509..7c8a7069655a 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -74,6 +74,15 @@ function data_get($target, $key, $default = null) return in_array('*', $key) ? Arr::collapse($result) : $result; } + $segment = match ($segment) { + '\*' => '*', + '\{first}' => '{first}', + '{first}' => array_key_first($target), + '\{last}' => '{last}', + '{last}' => array_key_last($target), + default => $segment, + }; + if (Arr::accessible($target) && Arr::exists($target, $segment)) { $target = $target[$segment]; } elseif (is_object($target) && isset($target->{$segment})) { diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index c0fddec3ec76..51a1929a219c 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -252,6 +252,80 @@ public function testDataGetWithDoubleNestedArraysCollapsesResult() $this->assertEquals([], data_get($array, 'posts.*.users.*.name')); } + public function testDataGetFirstLastDirectives() + { + $array = [ + 'flights' => [ + [ + 'segments' => [ + ['from' => 'LHR', 'departure' => '9:00', 'to' => 'IST', 'arrival' => '15:00'], + ['from' => 'IST', 'departure' => '16:00', 'to' => 'PKX', 'arrival' => '20:00'], + ], + ], + [ + 'segments' => [ + ['from' => 'LGW', 'departure' => '8:00', 'to' => 'SAW', 'arrival' => '14:00'], + ['from' => 'SAW', 'departure' => '15:00', 'to' => 'PEK', 'arrival' => '19:00'], + ], + ], + ], + ]; + + $this->assertEquals('LHR', data_get($array, 'flights.0.segments.{first}.from')); + $this->assertEquals('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->assertEquals(['LHR', 'IST'], data_get($array, 'flights.{first}.segments.*.from')); + $this->assertEquals(['SAW', 'PEK'], data_get($array, 'flights.{last}.segments.*.to')); + + $this->assertEquals(['LHR', 'LGW'], data_get($array, 'flights.*.segments.{first}.from')); + $this->assertEquals(['PKX', 'PEK'], data_get($array, 'flights.*.segments.{last}.to')); + } + + public function testDataGetFirstLastDirectivesOnKeyedArrays() + { + $array = [ + 'numericKeys' => [ + 2 => 'first', + 0 => 'second', + 1 => 'last', + ], + 'stringKeys' => [ + 'one' => 'first', + 'two' => 'second', + 'three' => 'last', + ], + ]; + + $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}')); + } + + public function testDataGetEscapedSegmentKeys() + { + $array = [ + 'symbols' => [ + '{last}' => ['description' => 'dollar'], + '*' => ['description' => 'asterisk'], + '{first}' => ['description' => 'caret'], + ], + ]; + + $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->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')); + } + public function testDataFill() { $data = ['foo' => 'bar']; From 5fedaf4db5b54f16920ef742e4c5655575efd34a Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Wed, 29 Nov 2023 17:39:22 -0500 Subject: [PATCH 129/207] [11.x] `Fluent::scope/collect` methods (#49180) * Add toFluent method * Add scope and toCollection methods * Add tests * Remove toFluent on collections * Remove unused import * Remove unused import * Cast attributes as array in scope instead * formatting * Use `value` for `offsetGet` and `__get` methods --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Support/Fluent.php | 42 ++++++++++++++++++++++++++--- tests/Support/SupportFluentTest.php | 20 ++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index 109ce17336a7..a4f482f31743 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -37,7 +37,7 @@ public function __construct($attributes = []) } /** - * Get an attribute from the fluent instance. + * Get an attribute from the fluent instance using "dot" notation. * * @template TGetDefault * @@ -46,6 +46,18 @@ public function __construct($attributes = []) * @return TValue|TGetDefault */ public function get($key, $default = null) + { + return data_get($this->attributes, $key, $default); + } + + /** + * Get an attribute from the fluent instance. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function value($key, $default = null) { if (array_key_exists($key, $this->attributes)) { return $this->attributes[$key]; @@ -54,6 +66,20 @@ public function get($key, $default = null) return value($default); } + /** + * Get the value of the given key as a new Fluent instance. + * + * @param string $key + * @param mixed $default + * @return static + */ + public function scope($key, $default = null) + { + return new static( + (array) $this->get($key, $default) + ); + } + /** * Get the attributes from the fluent instance. * @@ -74,6 +100,16 @@ public function toArray() return $this->attributes; } + /** + * Convert the fluent instance to a Collection. + * + * @return \Illuminate\Support\Collection + */ + public function collect() + { + return new Collection($this->attributes); + } + /** * Convert the object into something JSON serializable. * @@ -114,7 +150,7 @@ public function offsetExists($offset): bool */ public function offsetGet($offset): mixed { - return $this->get($offset); + return $this->value($offset); } /** @@ -162,7 +198,7 @@ public function __call($method, $parameters) */ public function __get($key) { - return $this->get($key); + return $this->value($key); } /** diff --git a/tests/Support/SupportFluentTest.php b/tests/Support/SupportFluentTest.php index 515b4817da10..659472df335b 100755 --- a/tests/Support/SupportFluentTest.php +++ b/tests/Support/SupportFluentTest.php @@ -110,6 +110,26 @@ public function testToJsonEncodesTheToArrayResult() $this->assertJsonStringEqualsJsonString(json_encode(['foo']), $results); } + + public function testScope() + { + $fluent = new Fluent(['user' => ['name' => 'taylor']]); + $this->assertEquals(['taylor'], $fluent->scope('user.name')->toArray()); + $this->assertEquals(['dayle'], $fluent->scope('user.age', 'dayle')->toArray()); + + $fluent = new Fluent(['products' => ['forge', 'vapour', 'spark']]); + $this->assertEquals(['forge', 'vapour', 'spark'], $fluent->scope('products')->toArray()); + $this->assertEquals(['foo', 'bar'], $fluent->scope('missing', ['foo', 'bar'])->toArray()); + + $fluent = new Fluent(['authors' => ['taylor' => ['products' => ['forge', 'vapour', 'spark']]]]); + $this->assertEquals(['forge', 'vapour', 'spark'], $fluent->scope('authors.taylor.products')->toArray()); + } + + public function testToCollection() + { + $fluent = new Fluent(['forge', 'vapour', 'spark']); + $this->assertEquals(['forge', 'vapour', 'spark'], $fluent->collect()->all()); + } } class FluentArrayIteratorStub implements IteratorAggregate From 0141b7521edff8e880b04105c80e31edb440b3ac Mon Sep 17 00:00:00 2001 From: Sergey Danilchenko Date: Thu, 30 Nov 2023 16:10:06 +0200 Subject: [PATCH 130/207] [11.x] data_get helper first/last directives support for ArrayAccess&Iterable (#49193) * [11.x] data_get helper first/last directives support for ArrayAccess&Iterable * optimization --------- Co-authored-by: Sergey Danilchenko --- src/Illuminate/Collections/helpers.php | 4 +- tests/Support/SupportHelpersTest.php | 81 +++++++++++++++++++++----- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 7c8a7069655a..1854137ee6bf 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -77,9 +77,9 @@ function data_get($target, $key, $default = null) $segment = match ($segment) { '\*' => '*', '\{first}' => '{first}', - '{first}' => array_key_first($target), + '{first}' => array_key_first(is_array($target) ? $target : collect($target)->all()), '\{last}' => '{last}', - '{last}' => array_key_last($target), + '{last}' => array_key_last(is_array($target) ? $target : collect($target)->all()), default => $segment, }; diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 51a1929a219c..3570323c252a 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -269,6 +269,7 @@ public function testDataGetFirstLastDirectives() ], ], ], + 'empty' => [], ]; $this->assertEquals('LHR', data_get($array, 'flights.0.segments.{first}.from')); @@ -284,6 +285,47 @@ 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')); + } + + public function testDataGetFirstLastDirectivesOnArrayAccessIterable() + { + $arrayAccessIterable = [ + 'flights' => new SupportTestArrayAccessIterable([ + [ + 'segments' => new SupportTestArrayAccessIterable([ + ['from' => 'LHR', 'departure' => '9:00', 'to' => 'IST', 'arrival' => '15:00'], + ['from' => 'IST', 'departure' => '16:00', 'to' => 'PKX', 'arrival' => '20:00'], + ]), + ], + [ + 'segments' => new SupportTestArrayAccessIterable([ + ['from' => 'LGW', 'departure' => '8:00', 'to' => 'SAW', 'arrival' => '14:00'], + ['from' => 'SAW', 'departure' => '15:00', 'to' => 'PEK', 'arrival' => '19:00'], + ]), + ], + ]), + '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->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->assertEquals(['LHR', 'IST'], data_get($arrayAccessIterable, 'flights.{first}.segments.*.from')); + $this->assertEquals(['SAW', 'PEK'], data_get($arrayAccessIterable, 'flights.{last}.segments.*.to')); + + $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')); } public function testDataGetFirstLastDirectivesOnKeyedArrays() @@ -1160,43 +1202,37 @@ class SupportTestClassThree extends SupportTestClassTwo use SupportTestTraitThree; } -class SupportTestArrayAccess implements ArrayAccess +trait SupportTestTraitArrayAccess { - protected $attributes = []; - - public function __construct($attributes = []) + public function __construct(protected array $items = []) { - $this->attributes = $attributes; } public function offsetExists($offset): bool { - return array_key_exists($offset, $this->attributes); + return array_key_exists($offset, $this->items); } public function offsetGet($offset): mixed { - return $this->attributes[$offset]; + return $this->items[$offset]; } public function offsetSet($offset, $value): void { - $this->attributes[$offset] = $value; + $this->items[$offset] = $value; } public function offsetUnset($offset): void { - unset($this->attributes[$offset]); + unset($this->items[$offset]); } } -class SupportTestArrayIterable implements IteratorAggregate +trait SupportTestTraitArrayIterable { - protected $items = []; - - public function __construct($items = []) + public function __construct(protected array $items = []) { - $this->items = $items; } public function getIterator(): Traversable @@ -1205,6 +1241,23 @@ public function getIterator(): Traversable } } +class SupportTestArrayAccess implements ArrayAccess +{ + use SupportTestTraitArrayAccess; +} + +class SupportTestArrayIterable implements IteratorAggregate +{ + use SupportTestTraitArrayIterable; +} + +class SupportTestArrayAccessIterable implements ArrayAccess, IteratorAggregate +{ + use SupportTestTraitArrayAccess, SupportTestTraitArrayIterable { + SupportTestTraitArrayAccess::__construct insteadof SupportTestTraitArrayIterable; + } +} + class SupportTestCountable implements Countable { public function count(): int From 866582b96f1c3969e3d639756e47690acdee833d Mon Sep 17 00:00:00 2001 From: Choraimy Kroonstuiver <3661474+axlon@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:40:28 +0100 Subject: [PATCH 131/207] Improve stateful guard return types (#49196) --- src/Illuminate/Contracts/Auth/StatefulGuard.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Contracts/Auth/StatefulGuard.php b/src/Illuminate/Contracts/Auth/StatefulGuard.php index faf1497d5f84..2448cf9b08b8 100644 --- a/src/Illuminate/Contracts/Auth/StatefulGuard.php +++ b/src/Illuminate/Contracts/Auth/StatefulGuard.php @@ -35,7 +35,7 @@ public function login(Authenticatable $user, $remember = false); * * @param mixed $id * @param bool $remember - * @return \Illuminate\Contracts\Auth\Authenticatable|bool + * @return \Illuminate\Contracts\Auth\Authenticatable|false */ public function loginUsingId($id, $remember = false); @@ -43,7 +43,7 @@ public function loginUsingId($id, $remember = false); * Log the given user ID into the application without sessions or cookies. * * @param mixed $id - * @return \Illuminate\Contracts\Auth\Authenticatable|bool + * @return \Illuminate\Contracts\Auth\Authenticatable|false */ public function onceUsingId($id); From e0fc281359e081a37bc25509c8d66e0f5e906e49 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Thu, 30 Nov 2023 14:40:59 +0000 Subject: [PATCH 132/207] Update facade docblocks --- src/Illuminate/Support/Facades/Auth.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Facades/Auth.php b/src/Illuminate/Support/Facades/Auth.php index d2d44fac857d..a93e3244ae44 100755 --- a/src/Illuminate/Support/Facades/Auth.php +++ b/src/Illuminate/Support/Facades/Auth.php @@ -32,8 +32,8 @@ * @method static bool attempt(array $credentials = [], bool $remember = false) * @method static bool once(array $credentials = []) * @method static void login(\Illuminate\Contracts\Auth\Authenticatable $user, bool $remember = false) - * @method static \Illuminate\Contracts\Auth\Authenticatable|bool loginUsingId(mixed $id, bool $remember = false) - * @method static \Illuminate\Contracts\Auth\Authenticatable|bool onceUsingId(mixed $id) + * @method static \Illuminate\Contracts\Auth\Authenticatable|false loginUsingId(mixed $id, bool $remember = false) + * @method static \Illuminate\Contracts\Auth\Authenticatable|false onceUsingId(mixed $id) * @method static bool viaRemember() * @method static void logout() * @method static \Symfony\Component\HttpFoundation\Response|null basic(string $field = 'email', array $extraConditions = []) From afcf28f15333ef05f087f5f903ebcfa8c3906bbc Mon Sep 17 00:00:00 2001 From: Saya <379924+chu121su12@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:41:17 +0800 Subject: [PATCH 133/207] [11.x] Allow to specify nested key when calling Fluent::collect() (#49191) * [11.x] Allow to specify nested key when calling Fluent::collect() * type key as string * Update src/Illuminate/Support/Fluent.php Co-authored-by: Mior Muhammad Zaki --------- Co-authored-by: Mior Muhammad Zaki --- src/Illuminate/Support/Fluent.php | 5 +++-- tests/Support/SupportFluentTest.php | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Fluent.php b/src/Illuminate/Support/Fluent.php index a4f482f31743..a4948f2848b2 100755 --- a/src/Illuminate/Support/Fluent.php +++ b/src/Illuminate/Support/Fluent.php @@ -103,11 +103,12 @@ public function toArray() /** * Convert the fluent instance to a Collection. * + * @param string|null $key * @return \Illuminate\Support\Collection */ - public function collect() + public function collect($key = null) { - return new Collection($this->attributes); + return new Collection($this->get($key)); } /** diff --git a/tests/Support/SupportFluentTest.php b/tests/Support/SupportFluentTest.php index 659472df335b..07c883c55a2a 100755 --- a/tests/Support/SupportFluentTest.php +++ b/tests/Support/SupportFluentTest.php @@ -129,6 +129,9 @@ public function testToCollection() { $fluent = new Fluent(['forge', 'vapour', 'spark']); $this->assertEquals(['forge', 'vapour', 'spark'], $fluent->collect()->all()); + + $fluent = new Fluent(['authors' => ['taylor' => ['products' => ['forge', 'vapour', 'spark']]]]); + $this->assertEquals(['forge', 'vapour', 'spark'], $fluent->collect('authors.taylor.products')->all()); } } From 51726652426a1078332258e8546bc1cd8b66ae02 Mon Sep 17 00:00:00 2001 From: jcsoriano Date: Fri, 1 Dec 2023 00:28:02 +0800 Subject: [PATCH 134/207] [10.x] Add support for `Number::summarize` (#49197) * Add Number::summarize * Update comment * Make units configurable * Make end unit dynamic * Remove named arguments to be consistent with other method calls * CS fix * Fix tests * Fix tests * Fix tests * formatting * format --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Support/Number.php | 63 ++++++++++++++++++++++++----- tests/Support/SupportNumberTest.php | 53 ++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 47d218ffcd47..5c62564a08a4 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -141,30 +141,71 @@ public static function fileSize(int|float $bytes, int $precision = 0, ?int $maxP * @param int|null $maxPrecision * @return string */ - public static function forHumans(int|float $number, int $precision = 0, ?int $maxPrecision = null) + public static function abbreviate(int|float $number, int $precision = 0, ?int $maxPrecision = null) { - $units = [ - 3 => 'thousand', - 6 => 'million', - 9 => 'billion', - 12 => 'trillion', - 15 => 'quadrillion', - ]; + return static::forHumans($number, $precision, $maxPrecision, abbreviate: true); + } + + /** + * Convert the number to its human readable equivalent. + * + * @param int $number + * @param int $precision + * @param int|null $maxPrecision + * @return string + */ + public static function forHumans(int|float $number, int $precision = 0, ?int $maxPrecision = null, bool $abbreviate = false) + { + return static::summarize($number, $precision, $maxPrecision, $abbreviate ? [ + 3 => 'K', + 6 => 'M', + 9 => 'B', + 12 => 'T', + 15 => 'Q', + ] : [ + 3 => ' thousand', + 6 => ' million', + 9 => ' billion', + 12 => ' trillion', + 15 => ' quadrillion', + ]); + } + + /** + * Convert the number to its human readable equivalent. + * + * @param int $number + * @param int $precision + * @param int|null $maxPrecision + * @param array $units + * @return string + */ + protected static function summarize(int|float $number, int $precision = 0, ?int $maxPrecision = null, array $units = []) + { + if (empty($units)) { + $units = [ + 3 => 'K', + 6 => 'M', + 9 => 'B', + 12 => 'T', + 15 => 'Q', + ]; + } switch (true) { case $number === 0: return '0'; case $number < 0: - return sprintf('-%s', static::forHumans(abs($number), $precision, $maxPrecision)); + return sprintf('-%s', static::summarize(abs($number), $precision, $maxPrecision, $units)); case $number >= 1e15: - return sprintf('%s quadrillion', static::forHumans($number / 1e15, $precision, $maxPrecision)); + return sprintf('%s'.end($units), static::summarize($number / 1e15, $precision, $maxPrecision, $units)); } $numberExponent = floor(log10($number)); $displayExponent = $numberExponent - ($numberExponent % 3); $number /= pow(10, $displayExponent); - return trim(sprintf('%s %s', static::format($number, $precision, $maxPrecision), $units[$displayExponent] ?? '')); + return trim(sprintf('%s%s', static::format($number, $precision, $maxPrecision), $units[$displayExponent] ?? '')); } /** diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index e758ecabdede..ce8463bce324 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -208,6 +208,59 @@ public function testToHuman() $this->assertSame('-1 thousand quadrillion', Number::forHumans(-1000000000000000000)); } + public function testSummarize() + { + $this->assertSame('1', Number::abbreviate(1)); + $this->assertSame('1.00', Number::abbreviate(1, precision: 2)); + $this->assertSame('10', Number::abbreviate(10)); + $this->assertSame('100', Number::abbreviate(100)); + $this->assertSame('1K', Number::abbreviate(1000)); + $this->assertSame('1.00K', Number::abbreviate(1000, precision: 2)); + $this->assertSame('1K', Number::abbreviate(1000, maxPrecision: 2)); + $this->assertSame('1K', Number::abbreviate(1230)); + $this->assertSame('1.2K', Number::abbreviate(1230, maxPrecision: 1)); + $this->assertSame('1M', Number::abbreviate(1000000)); + $this->assertSame('1B', Number::abbreviate(1000000000)); + $this->assertSame('1T', Number::abbreviate(1000000000000)); + $this->assertSame('1Q', Number::abbreviate(1000000000000000)); + $this->assertSame('1KQ', Number::abbreviate(1000000000000000000)); + + $this->assertSame('123', Number::abbreviate(123)); + $this->assertSame('1K', Number::abbreviate(1234)); + $this->assertSame('1.23K', Number::abbreviate(1234, precision: 2)); + $this->assertSame('12K', Number::abbreviate(12345)); + $this->assertSame('1M', Number::abbreviate(1234567)); + $this->assertSame('1B', Number::abbreviate(1234567890)); + $this->assertSame('1T', Number::abbreviate(1234567890123)); + $this->assertSame('1.23T', Number::abbreviate(1234567890123, precision: 2)); + $this->assertSame('1Q', Number::abbreviate(1234567890123456)); + $this->assertSame('1.23KQ', Number::abbreviate(1234567890123456789, precision: 2)); + $this->assertSame('490K', Number::abbreviate(489939)); + $this->assertSame('489.9390K', Number::abbreviate(489939, precision: 4)); + $this->assertSame('500.00000M', Number::abbreviate(500000000, precision: 5)); + + $this->assertSame('1MQ', Number::abbreviate(1000000000000000000000)); + $this->assertSame('1BQ', Number::abbreviate(1000000000000000000000000)); + $this->assertSame('1TQ', Number::abbreviate(1000000000000000000000000000)); + $this->assertSame('1QQ', Number::abbreviate(1000000000000000000000000000000)); + $this->assertSame('1KQQ', Number::abbreviate(1000000000000000000000000000000000)); + + $this->assertSame('0', Number::abbreviate(0)); + $this->assertSame('-1', Number::abbreviate(-1)); + $this->assertSame('-1.00', Number::abbreviate(-1, precision: 2)); + $this->assertSame('-10', Number::abbreviate(-10)); + $this->assertSame('-100', Number::abbreviate(-100)); + $this->assertSame('-1K', Number::abbreviate(-1000)); + $this->assertSame('-1.23K', Number::abbreviate(-1234, precision: 2)); + $this->assertSame('-1.2K', Number::abbreviate(-1234, maxPrecision: 1)); + $this->assertSame('-1M', Number::abbreviate(-1000000)); + $this->assertSame('-1B', Number::abbreviate(-1000000000)); + $this->assertSame('-1T', Number::abbreviate(-1000000000000)); + $this->assertSame('-1.1T', Number::abbreviate(-1100000000000, maxPrecision: 1)); + $this->assertSame('-1Q', Number::abbreviate(-1000000000000000)); + $this->assertSame('-1KQ', Number::abbreviate(-1000000000000000000)); + } + protected function needsIntlExtension() { if (! extension_loaded('intl')) { From e4ffa951d608eac2cd5011aaadb63ed0a26f58ee Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Thu, 30 Nov 2023 16:57:53 +0000 Subject: [PATCH 135/207] [10.x]: Add Blade @use directive (#49179) * Add Blade @use directive * Allow for `use` without `as` * Update CompilesUseStatements.php --------- Co-authored-by: Taylor Otwell --- .../View/Compilers/BladeCompiler.php | 1 + .../Concerns/CompilesUseStatements.php | 22 +++++++++++++++++++ tests/View/Blade/BladeUseTest.php | 20 +++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php create mode 100644 tests/View/Blade/BladeUseTest.php diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 51479e81edc5..18aae67e14ba 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -33,6 +33,7 @@ class BladeCompiler extends Compiler implements CompilerInterface Concerns\CompilesStacks, Concerns\CompilesStyles, Concerns\CompilesTranslations, + Concerns\CompilesUseStatements, ReflectsClosures; /** diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php b/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php new file mode 100644 index 000000000000..8d74a77046d6 --- /dev/null +++ b/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php @@ -0,0 +1,22 @@ +"; + } +} diff --git a/tests/View/Blade/BladeUseTest.php b/tests/View/Blade/BladeUseTest.php new file mode 100644 index 000000000000..bc1c8a708c2d --- /dev/null +++ b/tests/View/Blade/BladeUseTest.php @@ -0,0 +1,20 @@ + bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testUseStatementsWithoutAsAreCompiled() + { + $string = "Foo @use('SomeNamespace\SomeClass') bar"; + $expected = "Foo bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } +} From 2b9d596bdd9340e87f0e18d5319bf0adc774451d Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 1 Dec 2023 03:21:51 +0800 Subject: [PATCH 136/207] [10.x] Fixes retrying failed jobs causes PHP memory exhaustion errors when dealing with thousands of failed jobs (#49186) * [10.x] Fixes retrying failed jobs causes PHP memory exhaustion errors when dealing with thousands of failed jobs fixes laravel/framework#49185 Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * formatting --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- src/Illuminate/Queue/Console/RetryCommand.php | 18 +++++++++++++----- .../Queue/Failed/DatabaseFailedJobProvider.php | 15 +++++++++++++++ .../Failed/DatabaseUuidFailedJobProvider.php | 15 +++++++++++++++ .../Queue/Failed/DynamoDbFailedJobProvider.php | 14 ++++++++++++++ .../Failed/FailedJobProviderInterface.php | 3 +++ .../Queue/Failed/FileFailedJobProvider.php | 14 ++++++++++++++ .../Queue/Failed/NullFailedJobProvider.php | 11 +++++++++++ 7 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Queue/Console/RetryCommand.php b/src/Illuminate/Queue/Console/RetryCommand.php index 37827dd01683..13640feb4efb 100644 --- a/src/Illuminate/Queue/Console/RetryCommand.php +++ b/src/Illuminate/Queue/Console/RetryCommand.php @@ -70,7 +70,11 @@ protected function getJobIds() $ids = (array) $this->argument('id'); if (count($ids) === 1 && $ids[0] === 'all') { - return Arr::pluck($this->laravel['queue.failer']->all(), 'id'); + $failer = $this->laravel['queue.failer']; + + return method_exists($failer, 'ids') + ? $failer->ids() + : Arr::pluck($failer->all(), 'id'); } if ($queue = $this->option('queue')) { @@ -92,10 +96,14 @@ protected function getJobIds() */ protected function getJobIdsByQueue($queue) { - $ids = collect($this->laravel['queue.failer']->all()) - ->where('queue', $queue) - ->pluck('id') - ->toArray(); + $failer = $this->laravel['queue.failer']; + + $ids = method_exists($failer, 'ids') + ? $failer->ids($queue) + : collect($failer->all()) + ->where('queue', $queue) + ->pluck('id') + ->toArray(); if (count($ids) === 0) { $this->components->error("Unable to find failed jobs for queue [{$queue}]."); diff --git a/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php b/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php index 4da8c0337c2f..49cb3b98ae9a 100644 --- a/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php +++ b/src/Illuminate/Queue/Failed/DatabaseFailedJobProvider.php @@ -64,6 +64,21 @@ public function log($connection, $queue, $payload, $exception) )); } + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return $this->getTable() + ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) + ->orderBy('id', 'desc') + ->pluck('id') + ->all(); + } + /** * Get a list of all of the failed jobs. * diff --git a/src/Illuminate/Queue/Failed/DatabaseUuidFailedJobProvider.php b/src/Illuminate/Queue/Failed/DatabaseUuidFailedJobProvider.php index f51c46f571b0..b3192f246beb 100644 --- a/src/Illuminate/Queue/Failed/DatabaseUuidFailedJobProvider.php +++ b/src/Illuminate/Queue/Failed/DatabaseUuidFailedJobProvider.php @@ -67,6 +67,21 @@ public function log($connection, $queue, $payload, $exception) return $uuid; } + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return $this->getTable() + ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) + ->orderBy('id', 'desc') + ->pluck('uuid') + ->all(); + } + /** * Get a list of all of the failed jobs. * diff --git a/src/Illuminate/Queue/Failed/DynamoDbFailedJobProvider.php b/src/Illuminate/Queue/Failed/DynamoDbFailedJobProvider.php index 2feeaa98a29f..226c752e94ef 100644 --- a/src/Illuminate/Queue/Failed/DynamoDbFailedJobProvider.php +++ b/src/Illuminate/Queue/Failed/DynamoDbFailedJobProvider.php @@ -78,6 +78,20 @@ public function log($connection, $queue, $payload, $exception) return $id; } + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return collect($this->all()) + ->when(! is_null($queue), fn ($collect) => $collect->where('queue', $queue)) + ->pluck('id') + ->all(); + } + /** * Get a list of all of the failed jobs. * diff --git a/src/Illuminate/Queue/Failed/FailedJobProviderInterface.php b/src/Illuminate/Queue/Failed/FailedJobProviderInterface.php index 26dd583b9496..bb52d1749ada 100644 --- a/src/Illuminate/Queue/Failed/FailedJobProviderInterface.php +++ b/src/Illuminate/Queue/Failed/FailedJobProviderInterface.php @@ -2,6 +2,9 @@ namespace Illuminate\Queue\Failed; +/** + * @method array ids(string $queue = null) + */ interface FailedJobProviderInterface { /** diff --git a/src/Illuminate/Queue/Failed/FileFailedJobProvider.php b/src/Illuminate/Queue/Failed/FileFailedJobProvider.php index f79efc05f39f..43f43248deb1 100644 --- a/src/Illuminate/Queue/Failed/FileFailedJobProvider.php +++ b/src/Illuminate/Queue/Failed/FileFailedJobProvider.php @@ -78,6 +78,20 @@ public function log($connection, $queue, $payload, $exception) }); } + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return collect($this->all()) + ->when(! is_null($queue), fn ($collect) => $collect->where('queue', $queue)) + ->pluck('id') + ->all(); + } + /** * Get a list of all of the failed jobs. * diff --git a/src/Illuminate/Queue/Failed/NullFailedJobProvider.php b/src/Illuminate/Queue/Failed/NullFailedJobProvider.php index a086134c98f3..f92c59eba658 100644 --- a/src/Illuminate/Queue/Failed/NullFailedJobProvider.php +++ b/src/Illuminate/Queue/Failed/NullFailedJobProvider.php @@ -18,6 +18,17 @@ public function log($connection, $queue, $payload, $exception) // } + /** + * Get the IDs of all of the failed jobs. + * + * @param string|null $queue + * @return array + */ + public function ids($queue = null) + { + return []; + } + /** * Get a list of all of the failed jobs. * From ccf8beb7b0c8207f00038fe90464cb973a3ea1ae Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Thu, 30 Nov 2023 17:15:02 -0500 Subject: [PATCH 137/207] [10.x] Add "substituteImplicitBindingsUsing" method to router (#49200) * Add "substituteImplicitBindingsUsing" method to router * Remove un-used test method * formatting * simplify code --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Routing/Router.php | 27 ++++++++++++++++++++++++++- tests/Routing/RoutingRouteTest.php | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index ee416226b1cc..f70a2183e282 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -119,6 +119,13 @@ class Router implements BindingRegistrar, RegistrarContract */ protected $groupStack = []; + /** + * The registered custom implicit binding callback. + * + * @var array + */ + protected $implicitBindingCallback; + /** * All of the verbs supported by the router. * @@ -949,7 +956,25 @@ public function substituteBindings($route) */ public function substituteImplicitBindings($route) { - ImplicitRouteBinding::resolveForRoute($this->container, $route); + $default = fn () => ImplicitRouteBinding::resolveForRoute($this->container, $route); + + return call_user_func( + $this->implicitBindingCallback ?? $default, $this->container, $route, $default + ); + + } + + /** + * Register a callback to to run after implicit bindings are substituted. + * + * @param callable $callback + * @return $this + */ + public function substituteImplicitBindingsUsing($callback) + { + $this->implicitBindingCallback = $callback; + + return $this; } /** diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index da8318a04aee..d75f92be6dd9 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -1756,6 +1756,27 @@ public function testImplicitBindings() $this->assertSame('taylor', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); } + public function testImplicitBindingsWithClosure() + { + $router = $this->getRouter(); + + $router->substituteImplicitBindingsUsing(function ($container, $route, $default) { + $default = $default(); + + $model = $route->parameter('bar'); + $model->value = 'otwell'; + }); + + $router->get('foo/{bar}', [ + 'middleware' => SubstituteBindings::class, + 'uses' => function (RoutingTestUserModel $bar) { + return $bar->value; + }, + ]); + + $this->assertSame('otwell', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); + } + public function testImplicitBindingsWhereScopedBindingsArePrevented() { $router = $this->getRouter(); From 378e4b32e072d1aee6ba2d8761201d7f3e86905e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Nov 2023 22:15:27 +0000 Subject: [PATCH 138/207] Apply fixes from StyleCI --- src/Illuminate/Routing/Router.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index f70a2183e282..4ddc0cc76c35 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -961,7 +961,6 @@ public function substituteImplicitBindings($route) return call_user_func( $this->implicitBindingCallback ?? $default, $this->container, $route, $default ); - } /** From bd30b02a93994b8ef995d1a3dbecc125557c6b58 Mon Sep 17 00:00:00 2001 From: Fabrice Locher Date: Thu, 30 Nov 2023 23:32:43 +0100 Subject: [PATCH 139/207] [10.x] Cookies Having Independent Partitioned State (CHIPS) (#48745) * [10.x] Cookies Having Independent Partitioned State (CHIPS) * Clean up cookie duplication * Bump components symfony/http-foundation version --- composer.json | 2 +- src/Illuminate/Cookie/Middleware/EncryptCookies.php | 6 +----- src/Illuminate/Cookie/composer.json | 2 +- .../Foundation/Http/Middleware/VerifyCsrfToken.php | 3 ++- src/Illuminate/Http/composer.json | 2 +- src/Illuminate/Routing/composer.json | 2 +- src/Illuminate/Session/Middleware/StartSession.php | 13 ++++++++++--- src/Illuminate/Session/composer.json | 2 +- src/Illuminate/Testing/TestResponse.php | 3 ++- src/Illuminate/Validation/composer.json | 2 +- 10 files changed, 21 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index d9d4a64a3783..9e7705b73db9 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "symfony/console": "^6.2", "symfony/error-handler": "^6.2", "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.3", + "symfony/http-foundation": "^6.4", "symfony/http-kernel": "^6.2", "symfony/mailer": "^6.2", "symfony/mime": "^6.2", diff --git a/src/Illuminate/Cookie/Middleware/EncryptCookies.php b/src/Illuminate/Cookie/Middleware/EncryptCookies.php index 53a914e70a0a..c80e4c340e08 100644 --- a/src/Illuminate/Cookie/Middleware/EncryptCookies.php +++ b/src/Illuminate/Cookie/Middleware/EncryptCookies.php @@ -195,11 +195,7 @@ protected function encrypt(Response $response) */ protected function duplicate(Cookie $cookie, $value) { - return new Cookie( - $cookie->getName(), $value, $cookie->getExpiresTime(), - $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), - $cookie->isHttpOnly(), $cookie->isRaw(), $cookie->getSameSite() - ); + return $cookie->withValue($value); } /** diff --git a/src/Illuminate/Cookie/composer.json b/src/Illuminate/Cookie/composer.json index 0e278831b6e0..10277e08b715 100755 --- a/src/Illuminate/Cookie/composer.json +++ b/src/Illuminate/Cookie/composer.json @@ -20,7 +20,7 @@ "illuminate/contracts": "^10.0", "illuminate/macroable": "^10.0", "illuminate/support": "^10.0", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.4", "symfony/http-kernel": "^6.2" }, "autoload": { diff --git a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php index 69faa52726d2..254139403546 100644 --- a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php +++ b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php @@ -211,7 +211,8 @@ protected function newCookie($request, $config) $config['secure'], false, false, - $config['same_site'] ?? null + $config['same_site'] ?? null, + $config['partitioned'] ?? false ); } diff --git a/src/Illuminate/Http/composer.json b/src/Illuminate/Http/composer.json index bb21d1853450..94d03fc4e851 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -22,7 +22,7 @@ "illuminate/macroable": "^10.0", "illuminate/session": "^10.0", "illuminate/support": "^10.0", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.4", "symfony/http-kernel": "^6.2", "symfony/mime": "^6.2" }, diff --git a/src/Illuminate/Routing/composer.json b/src/Illuminate/Routing/composer.json index 11ecf786d243..1375ef059f9a 100644 --- a/src/Illuminate/Routing/composer.json +++ b/src/Illuminate/Routing/composer.json @@ -25,7 +25,7 @@ "illuminate/pipeline": "^10.0", "illuminate/session": "^10.0", "illuminate/support": "^10.0", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.4", "symfony/http-kernel": "^6.2", "symfony/routing": "^6.2" }, diff --git a/src/Illuminate/Session/Middleware/StartSession.php b/src/Illuminate/Session/Middleware/StartSession.php index 53f3a16c6ba2..5077cf6f5b90 100644 --- a/src/Illuminate/Session/Middleware/StartSession.php +++ b/src/Illuminate/Session/Middleware/StartSession.php @@ -219,9 +219,16 @@ protected function addCookieToResponse(Response $response, Session $session) { if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) { $response->headers->setCookie(new Cookie( - $session->getName(), $session->getId(), $this->getCookieExpirationDate(), - $config['path'], $config['domain'], $config['secure'] ?? false, - $config['http_only'] ?? true, false, $config['same_site'] ?? null + $session->getName(), + $session->getId(), + $this->getCookieExpirationDate(), + $config['path'], + $config['domain'], + $config['secure'] ?? false, + $config['http_only'] ?? true, + false, + $config['same_site'] ?? null, + $config['partitioned'] ?? false )); } } diff --git a/src/Illuminate/Session/composer.json b/src/Illuminate/Session/composer.json index 392bc21328b5..083ff581a229 100755 --- a/src/Illuminate/Session/composer.json +++ b/src/Illuminate/Session/composer.json @@ -22,7 +22,7 @@ "illuminate/filesystem": "^10.0", "illuminate/support": "^10.0", "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.2" + "symfony/http-foundation": "^6.4" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 2a7945a72585..47e4cab56e47 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -498,7 +498,8 @@ public function getCookie($cookieName, $decrypt = true, $unserialize = false) $cookie->isSecure(), $cookie->isHttpOnly(), $cookie->isRaw(), - $cookie->getSameSite() + $cookie->getSameSite(), + $cookie->isPartitioned() ); } } diff --git a/src/Illuminate/Validation/composer.json b/src/Illuminate/Validation/composer.json index 43a2965ecea3..611f05092f38 100755 --- a/src/Illuminate/Validation/composer.json +++ b/src/Illuminate/Validation/composer.json @@ -25,7 +25,7 @@ "illuminate/macroable": "^10.0", "illuminate/support": "^10.0", "illuminate/translation": "^10.0", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.4", "symfony/mime": "^6.2" }, "autoload": { From e574ffa03436a85d0aefccca9d8127746b3032aa Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Thu, 30 Nov 2023 22:33:19 +0000 Subject: [PATCH 140/207] Update facade docblocks --- src/Illuminate/Support/Facades/Route.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/Route.php b/src/Illuminate/Support/Facades/Route.php index db5c9a562cf2..89bd67cf403b 100755 --- a/src/Illuminate/Support/Facades/Route.php +++ b/src/Illuminate/Support/Facades/Route.php @@ -37,6 +37,7 @@ * @method static \Symfony\Component\HttpFoundation\Response toResponse(\Symfony\Component\HttpFoundation\Request $request, mixed $response) * @method static \Illuminate\Routing\Route substituteBindings(\Illuminate\Routing\Route $route) * @method static void substituteImplicitBindings(\Illuminate\Routing\Route $route) + * @method static \Illuminate\Routing\Router substituteImplicitBindingsUsing(callable $callback) * @method static void matched(string|callable $callback) * @method static array getMiddleware() * @method static \Illuminate\Routing\Router aliasMiddleware(string $name, string $class) From e82b7e8ca849248e8986d7fb9bfe0258b232c63c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Nov 2023 16:36:36 -0600 Subject: [PATCH 141/207] add partitioned entry --- config/session.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/config/session.php b/config/session.php index b29b8b783815..51c77f90af42 100644 --- a/config/session.php +++ b/config/session.php @@ -199,4 +199,17 @@ 'same_site' => env('SESSION_SAME_SITE', 'lax'), + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => false, + ]; From 8b94c3e205a82e8e4d13b51a77053b6b661e3117 Mon Sep 17 00:00:00 2001 From: Grldk <33746490+Grldk@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:11:12 +0100 Subject: [PATCH 142/207] Update InteractsWithDictionary.php to use base InvalidArgumentException (#49209) use base InvalidArgumentException instead of \Doctrine\Instantiator\Exception\InvalidArgumentException --- .../Eloquent/Relations/Concerns/InteractsWithDictionary.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php index d9775cc5a493..21f4fce22eac 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php @@ -3,7 +3,7 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; use BackedEnum; -use Doctrine\Instantiator\Exception\InvalidArgumentException; +use InvalidArgumentException; use UnitEnum; trait InteractsWithDictionary @@ -14,7 +14,7 @@ trait InteractsWithDictionary * @param mixed $attribute * @return mixed * - * @throws \Doctrine\Instantiator\Exception\InvalidArgumentException + * @throws \InvalidArgumentException */ protected function getDictionaryKey($attribute) { From a91c3f49f9c6df2c93586ada1a3e3eab8de8e55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 1 Dec 2023 16:16:16 +0100 Subject: [PATCH 143/207] fix docblock for wasRecentlyCreated (#49208) --- src/Illuminate/Database/Eloquent/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 9648d6b99df4..ee4c10aae4c4 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -106,7 +106,7 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt public $exists = false; /** - * Indicates if the model was inserted during the current request lifecycle. + * Indicates if the model was inserted during the object's lifecycle. * * @var bool */ From 37cb3450150cf75af787dc9bcd8de08370024008 Mon Sep 17 00:00:00 2001 From: Roj Vroemen Date: Fri, 1 Dec 2023 23:04:08 +0100 Subject: [PATCH 144/207] [10.x] Fix loss of attributes after calling child component (#49216) * Store and restore original attributes when rendering component * Move attribute assertion to mock This is needed because the original attributes are now restored after rendering a component. * Assert withAttribute params on child component prop * Fix assertion * Add assertion to ensure original attributes are restored --- .../Compilers/Concerns/CompilesComponents.php | 5 ++ .../Blade/BladeComponentTagCompilerTest.php | 62 ++++++++++++++++++- tests/View/Blade/BladeComponentsTest.php | 5 ++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php index 8e986553abb1..88617186fb20 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php @@ -67,6 +67,7 @@ public static function compileClassComponentOpening(string $component, string $a { return implode("\n", [ '', + '', 'getIterator() : [])); ?>', 'withName('.$alias.'); ?>', 'shouldRender()): ?>', @@ -94,6 +95,10 @@ public function compileEndComponentClass() $hash = array_pop(static::$componentHashStack); return $this->compileEndComponent()."\n".implode("\n", [ + '', + '', + '', + '', '', '', '', diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index ec100b50ee58..5143ae503c72 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -715,7 +715,7 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() $component->shouldReceive('shouldRender')->once()->andReturn(true); $component->shouldReceive('resolveView')->once()->andReturn(''); $component->shouldReceive('data')->once()->andReturn([]); - $component->shouldReceive('withAttributes')->once(); + $component->shouldReceive('withAttributes')->with(['attributes' => new ComponentAttributeBag(['other' => 'ok'])])->once(); Component::resolveComponentsUsing(fn () => $component); @@ -730,7 +730,57 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() eval(" ?> $template assertNull($attributes->get('userId')); + $this->assertSame($attributes->get('userId'), 'bar'); + $this->assertSame($attributes->get('other'), 'ok'); + } + + public function testOriginalAttributesAreRestoredAfterRenderingChildComponentWithProps() + { + $container = new Container; + $container->instance(Application::class, $app = m::mock(Application::class)); + $container->instance(Factory::class, $factory = m::mock(Factory::class)); + $container->alias(Factory::class, 'view'); + $app->shouldReceive('getNamespace')->never()->andReturn('App\\'); + $factory->shouldReceive('exists')->never(); + + Container::setInstance($container); + + $attributes = new ComponentAttributeBag(['userId' => 'bar', 'other' => 'ok']); + + $containerComponent = m::mock(Component::class); + $containerComponent->shouldReceive('withName')->with('container')->once(); + $containerComponent->shouldReceive('shouldRender')->once()->andReturn(true); + $containerComponent->shouldReceive('resolveView')->once()->andReturn(''); + $containerComponent->shouldReceive('data')->once()->andReturn([]); + $containerComponent->shouldReceive('withAttributes')->once(); + + $profileComponent = m::mock(Component::class); + $profileComponent->shouldReceive('withName')->with('profile')->once(); + $profileComponent->shouldReceive('shouldRender')->once()->andReturn(true); + $profileComponent->shouldReceive('resolveView')->once()->andReturn(''); + $profileComponent->shouldReceive('data')->once()->andReturn([]); + $profileComponent->shouldReceive('withAttributes')->with(['attributes' => new ComponentAttributeBag(['other' => 'ok'])])->once(); + + Component::resolveComponentsUsing(fn ($component) => match ($component) { + TestContainerComponent::class => $containerComponent, + TestProfileComponent::class => $profileComponent, + }); + + $__env = m::mock(\Illuminate\View\Factory::class); + $__env->shouldReceive('startComponent')->twice(); + $__env->shouldReceive('renderComponent')->twice(); + + $template = $this->compiler([ + 'container' => TestContainerComponent::class, + 'profile' => TestProfileComponent::class, + ])->compileTags(''); + $template = $this->compiler->compileString($template); + + ob_start(); + eval(" ?> $template assertSame($attributes->get('userId'), 'bar'); $this->assertSame($attributes->get('other'), 'ok'); } @@ -797,3 +847,11 @@ public function render() return 'input'; } } + +class TestContainerComponent extends Component +{ + public function render() + { + return 'container'; + } +} diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 406f9290965f..2615edf91452 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -17,6 +17,7 @@ public function testComponentsAreCompiled() public function testClassComponentsAreCompiled() { $this->assertSame(' + "bar"] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? (array) $attributes->getIterator() : [])); ?> withName(\'test\'); ?> shouldRender()): ?> @@ -36,6 +37,10 @@ public function testEndComponentClassesAreCompiled() $this->assertSame('renderComponent(); ?> + + + + From c59ef1f7bb9bfc916569e4492aedbb43a1c56501 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Mon, 4 Dec 2023 10:56:54 +0100 Subject: [PATCH 145/207] [10.x] Fix typo in PHPDoc comment (#49234) Fixes a typo in a helper method comment (`exntension` => `extension`) --- src/Illuminate/Support/Number.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 5c62564a08a4..954c05875a50 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -236,7 +236,7 @@ public static function useLocale(string $locale) } /** - * Ensure the "intl" PHP exntension is installed. + * Ensure the "intl" PHP extension is installed. * * @return void */ From 0947618d8ca47fb87fbee534b0cfbae35c96e6d3 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 4 Dec 2023 18:02:07 +0800 Subject: [PATCH 146/207] [10.x] `league/flysystem` 3.22.0 now prefer inclusive mime-type instead of `null`. (#49229) * [10.x] Test Improvements Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- tests/Filesystem/FilesystemAdapterTest.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index d6cc99c64dcd..2a98d21a2145 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Filesystem; use Carbon\Carbon; +use Composer\InstalledVersions; use GuzzleHttp\Psr7\Stream; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemManager; @@ -198,9 +199,19 @@ public function testJsonReturnsNullIfJsonDataIsInvalid() $this->assertNull($filesystemAdapter->json('file.json')); } - public function testMimeTypeNotDetected() + public function testMimeTypeDetectedPreferInclusiveMimeTypeOverNullAsEmpty() { + if (version_compare(InstalledVersions::getPrettyVersion('league/flysystem'), '3.22.0', '<')) { + $this->markTestSkipped('Require league/flysystem 3.22.0'); + } + $this->filesystem->write('unknown.mime-type', ''); + $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); + $this->assertSame('application/x-empty', $filesystemAdapter->mimeType('unknown.mime-type')); + } + + public function testMimeTypeNotDetected() + { $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $this->assertFalse($filesystemAdapter->mimeType('unknown.mime-type')); } @@ -531,8 +542,6 @@ public function testThrowExceptionsForPut() public function testThrowExceptionsForMimeType() { - $this->filesystem->write('unknown.mime-type', ''); - $adapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['throw' => true]); try { From be526ee5d4ec4b38f62413f7b5b114377c657531 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 4 Dec 2023 11:39:50 +0100 Subject: [PATCH 147/207] Revert "[10.x] `league/flysystem` 3.22.0 now prefer inclusive mime-type instead of `null`. (#49229)" (#49235) This reverts commit 0947618d8ca47fb87fbee534b0cfbae35c96e6d3. --- tests/Filesystem/FilesystemAdapterTest.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 2a98d21a2145..d6cc99c64dcd 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Filesystem; use Carbon\Carbon; -use Composer\InstalledVersions; use GuzzleHttp\Psr7\Stream; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemManager; @@ -199,19 +198,9 @@ public function testJsonReturnsNullIfJsonDataIsInvalid() $this->assertNull($filesystemAdapter->json('file.json')); } - public function testMimeTypeDetectedPreferInclusiveMimeTypeOverNullAsEmpty() - { - if (version_compare(InstalledVersions::getPrettyVersion('league/flysystem'), '3.22.0', '<')) { - $this->markTestSkipped('Require league/flysystem 3.22.0'); - } - - $this->filesystem->write('unknown.mime-type', ''); - $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); - $this->assertSame('application/x-empty', $filesystemAdapter->mimeType('unknown.mime-type')); - } - public function testMimeTypeNotDetected() { + $this->filesystem->write('unknown.mime-type', ''); $filesystemAdapter = new FilesystemAdapter($this->filesystem, $this->adapter); $this->assertFalse($filesystemAdapter->mimeType('unknown.mime-type')); } @@ -542,6 +531,8 @@ public function testThrowExceptionsForPut() public function testThrowExceptionsForMimeType() { + $this->filesystem->write('unknown.mime-type', ''); + $adapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['throw' => true]); try { From 219daf7b689382be1527079d86abb45a611e2952 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 4 Dec 2023 21:20:06 +0000 Subject: [PATCH 148/207] [11.x] Makes `name` argument required on `config:publish` (#49238) * Makes name argument required on `config:publish` * Uses prompts * Apply fixes from StyleCI * formatting * Apply fixes from StyleCI --------- Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Console/ConfigPublishCommand.php | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php index 65eb8c592968..bc12f35cd4d8 100644 --- a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php @@ -6,6 +6,8 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Finder\Finder; +use function Laravel\Prompts\select; + #[AsCommand(name: 'config:publish')] class ConfigPublishCommand extends Command { @@ -16,6 +18,7 @@ class ConfigPublishCommand extends Command */ protected $signature = 'config:publish {name? : The name of the configuration file to publish} + {--all : Publish all configuration files} {--force : Overwrite any existing configuration files}'; /** @@ -34,7 +37,20 @@ public function handle() { $config = $this->getBaseConfigurationFiles(); - $name = $this->argument('name'); + if (is_null($this->argument('name')) && $this->option('all')) { + foreach ($config as $key => $file) { + $this->publish($key, $file, $this->laravel->configPath().'/'.$key.'.php'); + } + + return; + } + + $name = (string) (is_null($this->argument('name')) ? select( + label: 'Which configuration file would you like to publish?', + options: collect($config)->map(function (string $path) { + return basename($path, '.php'); + }), + ) : $this->argument('name')); if (! is_null($name) && ! isset($config[$name])) { $this->components->error('Unrecognized configuration file.'); @@ -42,11 +58,7 @@ public function handle() return 1; } - foreach ($config as $key => $file) { - if ($key === $name || is_null($name)) { - $this->publish($key, $file, $this->laravel->configPath().'/'.$key.'.php'); - } - } + $this->publish($name, $config[$name], $this->laravel->configPath().'/'.$name.'.php'); } /** From 484e9c2c78637e3484681d7569a0e3f878dcd594 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 5 Dec 2023 01:56:42 +0330 Subject: [PATCH 149/207] [10.x] Determine if the given view exists. (#49231) * add `Schema::hasView()` * override `Schema::hasTable()` on SQL Server * add a test * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Schema/Builder.php | 19 +++++++++++++++++++ src/Illuminate/Support/Facades/Schema.php | 1 + .../Database/SchemaBuilderTest.php | 7 +++++++ 3 files changed, 27 insertions(+) diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 92c5f76884ee..0807bdd76c17 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -168,6 +168,25 @@ public function hasTable($table) return false; } + /** + * Determine if the given view exists. + * + * @param string $view + * @return bool + */ + public function hasView($view) + { + $view = $this->connection->getTablePrefix().$view; + + foreach ($this->getViews() as $value) { + if (strtolower($view) === strtolower($value['name'])) { + return true; + } + } + + return false; + } + /** * Get the tables that belong to the database. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index ddece45e6e56..7a29296130eb 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -11,6 +11,7 @@ * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) * @method static bool hasTable(string $table) + * @method static bool hasView(string $view) * @method static array getTables() * @method static array getViews() * @method static bool hasColumn(string $table, string $column) diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 38f2c34d6cc8..a8ea8fbaba63 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -164,6 +164,13 @@ public function testGetTables() } } + public function testHasView() + { + DB::statement('create view foo (id) as select 1'); + + $this->assertTrue(Schema::hasView('foo')); + } + public function testGetViews() { DB::statement('create view foo (id) as select 1'); From 8efb32342f0fa2aa0286b267f141afe9848dda50 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 5 Dec 2023 14:14:09 +0000 Subject: [PATCH 150/207] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5355399203f2..2a04f0b6775f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.34.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.34.1...10.x) + +## [v10.34.1](https://github.com/laravel/framework/compare/v10.34.0...v10.34.1) - 2023-11-28 + +* [10.x] Streamline `DatabaseMigrations` and `RefreshDatabase` events by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49153 +* [10.x] Use HtmlString in Vite fake by [@jasonvarga](https://github.com/jasonvarga) in https://github.com/laravel/framework/pull/49163 ## [v10.34.0](https://github.com/laravel/framework/compare/v10.33.0...v10.34.0) - 2023-11-28 From 5f00326061eeb7c1474096eaff4872d2b82a0dae Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 5 Dec 2023 14:14:24 +0000 Subject: [PATCH 151/207] Update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a04f0b6775f..aef95ff1e654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.34.1...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.34.2...10.x) + +## [v10.34.2](https://github.com/laravel/framework/compare/v10.34.1...v10.34.2) - 2023-11-28 + +* [v10.x] Add missing methods to newly extended fake `Vite` instance by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/49165 ## [v10.34.1](https://github.com/laravel/framework/compare/v10.34.0...v10.34.1) - 2023-11-28 From 91ec2d92d2f6007e9084fe06438b99c91845da69 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 5 Dec 2023 08:50:33 -0600 Subject: [PATCH 152/207] version --- 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 f0fd0e8e8b3c..a24cdef721d1 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.34.2'; + const VERSION = '10.35.0'; /** * The base path for the Laravel installation. From 5a08bdef7c24ca28cf10b400e763e31105a58f43 Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 5 Dec 2023 15:03:24 +0000 Subject: [PATCH 153/207] Update CHANGELOG --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aef95ff1e654..3eeb22cb8902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.34.2...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.35.0...10.x) + +## [v10.35.0](https://github.com/laravel/framework/compare/v10.34.2...v10.35.0) - 2023-12-05 + +* [10.x] Add `Conditionable` trait to `AssertableJson` by [@khalilst](https://github.com/khalilst) in https://github.com/laravel/framework/pull/49172 +* [10.x] Add `--with-secret` option to Artisan `down` command. by [@jj15asmr](https://github.com/jj15asmr) in https://github.com/laravel/framework/pull/49171 +* [10.x] Add support for `Number::summarize` by [@jcsoriano](https://github.com/jcsoriano) in https://github.com/laravel/framework/pull/49197 +* [10.x] Add Blade [@use](https://github.com/use) directive by [@simonhamp](https://github.com/simonhamp) in https://github.com/laravel/framework/pull/49179 +* [10.x] Fixes retrying failed jobs causes PHP memory exhaustion errors when dealing with thousands of failed jobs by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49186 +* [10.x] Add "substituteImplicitBindingsUsing" method to router by [@calebporzio](https://github.com/calebporzio) in https://github.com/laravel/framework/pull/49200 +* [10.x] Cookies Having Independent Partitioned State (CHIPS) by [@fabricecw](https://github.com/fabricecw) in https://github.com/laravel/framework/pull/48745 +* [10.x] Update InteractsWithDictionary.php to use base InvalidArgumentException by [@Grldk](https://github.com/Grldk) in https://github.com/laravel/framework/pull/49209 +* [10.x] Fix docblock for wasRecentlyCreated by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/49208 +* [10.x] Fix loss of attributes after calling child component by [@rojtjo](https://github.com/rojtjo) in https://github.com/laravel/framework/pull/49216 +* [10.x] Fix typo in PHPDoc comment by [@caendesilva](https://github.com/caendesilva) in https://github.com/laravel/framework/pull/49234 +* [10.x] Determine if the given view exists. by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49231 + +**Full Changelog**: https://github.com/laravel/framework/compare/v10.34.2...v10.35.0 ## [v10.34.2](https://github.com/laravel/framework/compare/v10.34.1...v10.34.2) - 2023-11-28 From d4e968433c44d1e637b2138e2ebfe89714ebf48b Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Tue, 5 Dec 2023 16:04:21 +0100 Subject: [PATCH 154/207] Update CHANGELOG.md --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eeb22cb8902..f9f5534e4b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,6 @@ * [10.x] Fix typo in PHPDoc comment by [@caendesilva](https://github.com/caendesilva) in https://github.com/laravel/framework/pull/49234 * [10.x] Determine if the given view exists. by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49231 -**Full Changelog**: https://github.com/laravel/framework/compare/v10.34.2...v10.35.0 - ## [v10.34.2](https://github.com/laravel/framework/compare/v10.34.1...v10.34.2) - 2023-11-28 * [v10.x] Add missing methods to newly extended fake `Vite` instance by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/49165 From d0d57da3213057d8af523889702cb3d7418eb413 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Tue, 5 Dec 2023 22:02:48 +0000 Subject: [PATCH 155/207] [10.x] Add `engine` method to `Blueprint` (#49250) * Add `engine` method * formatting * Update Blueprint.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Schema/Blueprint.php | 22 +++++++++++++++++++ .../DatabaseMySqlSchemaGrammarTest.php | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index f41123aca642..a3179742491a 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -287,6 +287,28 @@ public function create() return $this->addCommand('create'); } + /** + * Specify the storage engine that should be used for the table. + * + * @param string $engine + * @return void + */ + public function engine($engine) + { + $this->engine = $engine; + } + + /** + * Specify that the InnoDB storage engine should be used for the table (MySQL only). + * + * @param string $engine + * @return void + */ + public function innoDb() + { + $this->engine('InnoDB'); + } + /** * Indicate that the table needs to be temporary. * diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index fa8deab2fff9..2a7821f31cb0 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -86,7 +86,7 @@ public function testEngineCreateTable() $blueprint->create(); $blueprint->increments('id'); $blueprint->string('email'); - $blueprint->engine = 'InnoDB'; + $blueprint->engine('InnoDB'); $conn = $this->getConnection(); $conn->shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8'); From fd208de04ef8d0c101df0048d7cef0f446cb858b Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Tue, 5 Dec 2023 23:14:52 +0100 Subject: [PATCH 156/207] [10.x] Use translator from validator in `Can` and `Enum` rules (#49251) * [10.x] Use translator from validator in `Can` and `Enum` rules * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Validation/Rules/Can.php | 25 ++++++++++++++++++++++-- src/Illuminate/Validation/Rules/Enum.php | 25 ++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Can.php b/src/Illuminate/Validation/Rules/Can.php index 6732bc2f6c9f..8565dd6d0b78 100644 --- a/src/Illuminate/Validation/Rules/Can.php +++ b/src/Illuminate/Validation/Rules/Can.php @@ -3,9 +3,10 @@ namespace Illuminate\Validation\Rules; use Illuminate\Contracts\Validation\Rule; +use Illuminate\Contracts\Validation\ValidatorAwareRule; use Illuminate\Support\Facades\Gate; -class Can implements Rule +class Can implements Rule, ValidatorAwareRule { /** * The ability to check. @@ -21,6 +22,13 @@ class Can implements Rule */ protected $arguments; + /** + * The current validator instance. + * + * @var \Illuminate\Validation\Validator + */ + protected $validator; + /** * Constructor. * @@ -56,10 +64,23 @@ public function passes($attribute, $value) */ public function message() { - $message = trans('validation.can'); + $message = $this->validator->getTranslator()->get('validation.can'); return $message === 'validation.can' ? ['The :attribute field contains an unauthorized value.'] : $message; } + + /** + * Set the current validator. + * + * @param \Illuminate\Validation\Validator $validator + * @return $this + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } } diff --git a/src/Illuminate/Validation/Rules/Enum.php b/src/Illuminate/Validation/Rules/Enum.php index 37c7d3faae1e..d66a16d126bc 100644 --- a/src/Illuminate/Validation/Rules/Enum.php +++ b/src/Illuminate/Validation/Rules/Enum.php @@ -3,9 +3,10 @@ namespace Illuminate\Validation\Rules; use Illuminate\Contracts\Validation\Rule; +use Illuminate\Contracts\Validation\ValidatorAwareRule; use TypeError; -class Enum implements Rule +class Enum implements Rule, ValidatorAwareRule { /** * The type of the enum. @@ -14,6 +15,13 @@ class Enum implements Rule */ protected $type; + /** + * The current validator instance. + * + * @var \Illuminate\Validation\Validator + */ + protected $validator; + /** * Create a new rule instance. * @@ -56,10 +64,23 @@ public function passes($attribute, $value) */ public function message() { - $message = trans('validation.enum'); + $message = $this->validator->getTranslator()->get('validation.enum'); return $message === 'validation.enum' ? ['The selected :attribute is invalid.'] : $message; } + + /** + * Set the current validator. + * + * @param \Illuminate\Validation\Validator $validator + * @return $this + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } } From db7a87d2315634828e6efa09c60c07d04c42dc4f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 5 Dec 2023 22:48:12 +0000 Subject: [PATCH 157/207] [11.x] Adds `ServiceProvider::publishesMigrations()` method (#49246) * Adds `migrationPublishes` to register migration publishes * Adds tests * Adds `ensureUpToDateMigrationNames` * Apply fixes from StyleCI * Improves docs * Fixes test suite * Makes publishes migrations paths unique * Returns early * Increases a second before generating a date for migration * formatting --------- Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Console/VendorPublishCommand.php | 46 ++++++++++++++++++- src/Illuminate/Support/ServiceProvider.php | 31 +++++++++++++ tests/Support/SupportServiceProviderTest.php | 22 ++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index f755c3d0a3fe..037567f53445 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -42,6 +42,13 @@ class VendorPublishCommand extends Command */ protected $tags = []; + /** + * The time the command started. + * + * @var \Illuminate\Support\Carbon|null + */ + protected $publishedAt; + /** * The console command signature. * @@ -81,6 +88,8 @@ public function __construct(Filesystem $files) */ public function handle() { + $this->publishedAt = now(); + $this->determineWhatShouldBePublished(); foreach ($this->tags ?: [null] as $tag) { @@ -243,6 +252,8 @@ protected function publishFile($from, $to) { if ((! $this->option('existing') && (! $this->files->exists($to) || $this->option('force'))) || ($this->option('existing') && $this->files->exists($to))) { + $to = $this->ensureMigrationNameIsUpToDate($from, $to); + $this->createParentDirectory(dirname($to)); $this->files->copy($from, $to); @@ -274,7 +285,7 @@ protected function publishDirectory($from, $to) { $visibility = PortableVisibilityConverter::fromArray([], Visibility::PUBLIC); - $this->moveManagedFiles(new MountManager([ + $this->moveManagedFiles($from, new MountManager([ 'from' => new Flysystem(new LocalAdapter($from)), 'to' => new Flysystem(new LocalAdapter($to, $visibility)), ])); @@ -285,10 +296,11 @@ protected function publishDirectory($from, $to) /** * Move all the files in the given MountManager. * + * @param string $from * @param \League\Flysystem\MountManager $manager * @return void */ - protected function moveManagedFiles($manager) + protected function moveManagedFiles($from, $manager) { foreach ($manager->listContents('from://', true) as $file) { $path = Str::after($file['path'], 'from://'); @@ -300,6 +312,8 @@ protected function moveManagedFiles($manager) || ($this->option('existing') && $manager->fileExists('to://'.$path)) ) ) { + $path = $this->ensureMigrationNameIsUpToDate($from, $path); + $manager->write('to://'.$path, $manager->read($file['path'])); } } @@ -318,6 +332,34 @@ protected function createParentDirectory($directory) } } + /** + * Ensure the given migration name is up-to-date. + * + * @param string $from + * @param string $to + * @return string + */ + protected function ensureMigrationNameIsUpToDate($from, $to) + { + $from = realpath($from); + + foreach (ServiceProvider::publishableMigrationPaths() as $path) { + $path = realpath($path); + + if ($from === $path && preg_match('/\d{4}_(\d{2})_(\d{2})_(\d{6})_/', $to)) { + $this->publishedAt->addSecond(); + + return preg_replace( + '/\d{4}_(\d{2})_(\d{2})_(\d{6})_/', + $this->publishedAt->format('Y_m_d_His').'_', + $to, + ); + } + } + + return $to; + } + /** * Write a status message to the console. * diff --git a/src/Illuminate/Support/ServiceProvider.php b/src/Illuminate/Support/ServiceProvider.php index ad4a9c5837dc..fd632e8c7ed3 100755 --- a/src/Illuminate/Support/ServiceProvider.php +++ b/src/Illuminate/Support/ServiceProvider.php @@ -47,6 +47,13 @@ abstract class ServiceProvider */ public static $publishGroups = []; + /** + * The migration paths available for publishing. + * + * @var array + */ + protected static $publishableMigrationPaths = []; + /** * Create a new service provider instance. * @@ -267,6 +274,20 @@ protected function callAfterResolving($name, $callback) } } + /** + * Register migration paths to be published by the publish command. + * + * @param array $paths + * @param mixed $groups + * @return void + */ + protected function publishesMigrations(array $paths, $groups = null) + { + $this->publishes($paths, $groups); + + static::$publishableMigrationPaths = array_unique(array_merge(static::$publishableMigrationPaths, array_keys($paths))); + } + /** * Register paths to be published by the publish command. * @@ -380,6 +401,16 @@ public static function publishableProviders() return array_keys(static::$publishes); } + /** + * Get the migration paths available for publishing. + * + * @return array + */ + public static function publishableMigrationPaths() + { + return static::$publishableMigrationPaths; + } + /** * Get the groups available for publishing. * diff --git a/tests/Support/SupportServiceProviderTest.php b/tests/Support/SupportServiceProviderTest.php index bceb3046cfdd..4861a9e97291 100644 --- a/tests/Support/SupportServiceProviderTest.php +++ b/tests/Support/SupportServiceProviderTest.php @@ -39,7 +39,14 @@ public function testPublishableServiceProviders() public function testPublishableGroups() { $toPublish = ServiceProvider::publishableGroups(); - $this->assertEquals(['some_tag', 'tag_one', 'tag_two'], $toPublish, 'Publishable groups do not return expected set of groups.'); + $this->assertEquals([ + 'some_tag', + 'tag_one', + 'tag_two', + 'tag_three', + 'tag_four', + 'tag_five', + ], $toPublish, 'Publishable groups do not return expected set of groups.'); } public function testSimpleAssetsArePublishedCorrectly() @@ -47,7 +54,14 @@ public function testSimpleAssetsArePublishedCorrectly() $toPublish = ServiceProvider::pathsToPublish(ServiceProviderForTestingOne::class); $this->assertArrayHasKey('source/unmarked/one', $toPublish, 'Service provider does not return expected published path key.'); $this->assertArrayHasKey('source/tagged/one', $toPublish, 'Service provider does not return expected published path key.'); - $this->assertEquals(['source/unmarked/one' => 'destination/unmarked/one', 'source/tagged/one' => 'destination/tagged/one', 'source/tagged/multiple' => 'destination/tagged/multiple'], $toPublish, 'Service provider does not return expected set of published paths.'); + $this->assertEquals([ + 'source/unmarked/one' => 'destination/unmarked/one', + 'source/tagged/one' => 'destination/tagged/one', + 'source/tagged/multiple' => 'destination/tagged/multiple', + 'source/unmarked/two' => 'destination/unmarked/two', + 'source/tagged/three' => 'destination/tagged/three', + 'source/tagged/multiple_two' => 'destination/tagged/multiple_two', + ], $toPublish, 'Service provider does not return expected set of published paths.'); } public function testMultipleAssetsArePublishedCorrectly() @@ -119,6 +133,10 @@ public function boot() $this->publishes(['source/unmarked/one' => 'destination/unmarked/one']); $this->publishes(['source/tagged/one' => 'destination/tagged/one'], 'some_tag'); $this->publishes(['source/tagged/multiple' => 'destination/tagged/multiple'], ['tag_one', 'tag_two']); + + $this->publishesMigrations(['source/unmarked/two' => 'destination/unmarked/two']); + $this->publishesMigrations(['source/tagged/three' => 'destination/tagged/three'], 'tag_three'); + $this->publishesMigrations(['source/tagged/multiple_two' => 'destination/tagged/multiple_two'], ['tag_four', 'tag_five']); } } From 7d26bfb23fbc2d82adb095d4c6c28fe7f2e4bc4b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 6 Dec 2023 02:30:38 +0330 Subject: [PATCH 158/207] [10.x] Get indexes of a table (#49204) * get indexes * fix tests * minor improvements * minor improvements * minor improvements --- .../Query/Processors/MySqlProcessor.php | 21 +++++ .../Query/Processors/PostgresProcessor.php | 21 +++++ .../Database/Query/Processors/Processor.php | 11 +++ .../Query/Processors/SQLiteProcessor.php | 33 ++++++++ .../Query/Processors/SqlServerProcessor.php | 21 +++++ src/Illuminate/Database/Schema/Builder.php | 15 ++++ .../Database/Schema/Grammars/MySqlGrammar.php | 19 +++++ .../Schema/Grammars/PostgresGrammar.php | 26 ++++++ .../Schema/Grammars/SQLiteGrammar.php | 19 +++++ .../Schema/Grammars/SqlServerGrammar.php | 22 +++++ .../Database/Schema/MySqlBuilder.php | 17 ++++ .../Database/Schema/PostgresBuilder.php | 17 ++++ src/Illuminate/Support/Facades/Schema.php | 1 + .../DatabaseSQLiteSchemaGrammarTest.php | 17 ++-- .../DatabaseSchemaBuilderIntegrationTest.php | 10 ++- .../Database/SchemaBuilderTest.php | 80 +++++++++++++++++++ 16 files changed, 340 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php index 3aa8c9cb9852..7e6c66face3a 100644 --- a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php +++ b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php @@ -42,4 +42,25 @@ public function processColumns($results) ]; }, $results); } + + /** + * Process the results of an indexes query. + * + * @param array $results + * @return array + */ + public function processIndexes($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $name = strtolower($result->name), + 'columns' => explode(',', $result->columns), + 'type' => strtolower($result->type), + 'unique' => (bool) $result->unique, + 'primary' => $name === 'primary', + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index efa6cf027fb6..c45ae4ae57ee 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -70,4 +70,25 @@ public function processColumns($results) ]; }, $results); } + + /** + * Process the results of an indexes query. + * + * @param array $results + * @return array + */ + public function processIndexes($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => strtolower($result->name), + 'columns' => explode(',', $result->columns), + 'type' => strtolower($result->type), + 'unique' => (bool) $result->unique, + 'primary' => (bool) $result->primary, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 03f8ea982763..7abf156ad662 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -88,6 +88,17 @@ public function processColumns($results) return $results; } + /** + * Process the results of an indexes query. + * + * @param array $results + * @return array + */ + public function processIndexes($results) + { + return $results; + } + /** * Process the results of a column listing query. * diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index cb851efb4cc6..6c6da5567dba 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -46,4 +46,37 @@ public function processColumns($results) ]; }, $results); } + + /** + * Process the results of an indexes query. + * + * @param array $results + * @return array + */ + public function processIndexes($results) + { + $primaryCount = 0; + + $indexes = array_map(function ($result) use (&$primaryCount) { + $result = (object) $result; + + if ($isPrimary = (bool) $result->primary) { + $primaryCount += 1; + } + + return [ + 'name' => strtolower($result->name), + 'columns' => explode(',', $result->columns), + 'type' => null, + 'unique' => (bool) $result->unique, + 'primary' => $isPrimary, + ]; + }, $results); + + if ($primaryCount > 1) { + $indexes = array_filter($indexes, fn ($index) => $index['name'] !== 'primary'); + } + + return $indexes; + } } diff --git a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php index 0384335ac6db..15fa4d740745 100755 --- a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php @@ -100,4 +100,25 @@ public function processColumns($results) ]; }, $results); } + + /** + * Process the results of an indexes query. + * + * @param array $results + * @return array + */ + public function processIndexes($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => strtolower($result->name), + 'columns' => explode(',', $result->columns), + 'type' => strtolower($result->type), + 'unique' => (bool) $result->unique, + 'primary' => (bool) $result->primary, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 0807bdd76c17..07698e7fc206 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -342,6 +342,21 @@ public function getColumns($table) ); } + /** + * Get the indexes for a given table. + * + * @param string $table + * @return array + */ + public function getIndexes($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processIndexes( + $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($table)) + ); + } + /** * Modify a table on the schema. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 8ec5fb4e3040..43875591e07f 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -166,6 +166,25 @@ public function compileColumns($database, $table) ); } + /** + * Compile the query to determine the indexes. + * + * @param string $database + * @param string $table + * @return string + */ + public function compileIndexes($database, $table) + { + return sprintf( + 'select index_name as `name`, group_concat(column_name order by seq_in_index) as `columns`, ' + .'index_type as `type`, not non_unique as `unique` ' + .'from information_schema.statistics where table_schema = %s and table_name = %s ' + .'group by index_name, index_type, non_unique', + $this->quoteString($database), + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 061984bdbaed..2995e339d0d5 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -163,6 +163,32 @@ public function compileColumns($database, $schema, $table) ); } + /** + * Compile the query to determine the indexes. + * + * @param string $schema + * @param string $table + * @return string + */ + public function compileIndexes($schema, $table) + { + return sprintf( + "select ic.relname as name, string_agg(a.attname, ',' order by indseq.ord) as columns, " + .'am.amname as "type", i.indisunique as "unique", i.indisprimary as "primary" ' + .'from pg_index i ' + .'join pg_class tc on tc.oid = i.indrelid ' + .'join pg_namespace tn on tn.oid = tc.relnamespace ' + .'join pg_class ic on ic.oid = i.indexrelid ' + .'join pg_am am on am.oid = ic.relam ' + .'join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true ' + .'left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num ' + .'where tc.relname = %s and tn.nspname = %s ' + .'group by ic.relname, am.amname, i.indisunique, i.indisprimary', + $this->quoteString($table), + $this->quoteString($schema) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 02a75eeaf58c..a008fb73b0c6 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -127,6 +127,25 @@ public function compileColumns($table) ); } + /** + * Compile the query to determine the indexes. + * + * @param string $table + * @return string + */ + public function compileIndexes($table) + { + return sprintf( + 'select "primary" as name, group_concat(col) as columns, 1 as "unique", 1 as "primary" ' + .'from (select name as col from pragma_table_info(%s) where pk > 0 order by pk, cid) group by name ' + .'union select name, group_concat(col) as columns, "unique", origin = "pk" as "primary" ' + .'from (select il.*, ii.name as col from pragma_index_list(%s) il, pragma_index_info(il.name) ii order by il.seq, ii.seqno) ' + .'group by name, "unique", "primary"', + $table = $this->wrap(str_replace('.', '__', $table)), + $table + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 084021aa05c6..b4927d5f3432 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -167,6 +167,28 @@ public function compileColumns($table) ); } + /** + * Compile the query to determine the indexes. + * + * @param string $table + * @return string + */ + public function compileIndexes($table) + { + return sprintf( + "select idx.name as name, string_agg(col.name, ',') within group (order by idxcol.key_ordinal) as columns, " + .'idx.type_desc as [type], idx.is_unique as [unique], idx.is_primary_key as [primary] ' + .'from sys.indexes as idx ' + .'join sys.tables as tbl on idx.object_id = tbl.object_id ' + .'join sys.schemas as scm on tbl.schema_id = scm.schema_id ' + .'join sys.index_columns as idxcol on idx.object_id = idxcol.object_id and idx.index_id = idxcol.index_id ' + .'join sys.columns as col on idxcol.object_id = col.object_id and idxcol.column_id = col.column_id ' + .'where tbl.name = %s and scm.name = SCHEMA_NAME() ' + .'group by idx.name, idx.type_desc, idx.is_unique, idx.is_primary_key', + $this->quoteString($table), + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index 1c8c767bd997..e51305002480 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -103,6 +103,23 @@ public function getColumns($table) return $this->connection->getPostProcessor()->processColumns($results); } + /** + * Get the indexes for a given table. + * + * @param string $table + * @return array + */ + public function getIndexes($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processIndexes( + $this->connection->selectFromWriteConnection( + $this->grammar->compileIndexes($this->connection->getDatabaseName(), $table) + ) + ); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 2074e3b17cbb..0efe5dc62161 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -203,6 +203,23 @@ public function getColumns($table) return $this->connection->getPostProcessor()->processColumns($results); } + /** + * Get the indexes for a given table. + * + * @param string $table + * @return array + */ + public function getIndexes($table) + { + [, $schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processIndexes( + $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($schema, $table)) + ); + } + /** * Get the schemas for the connection. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 7a29296130eb..1bf87ba9a151 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -21,6 +21,7 @@ * @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 void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 9e48556d9f12..60339647c0f9 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -164,20 +164,21 @@ public function testRenameIndex() $table->index(['name', 'email'], 'index1'); }); - $manager = $db->getConnection()->getDoctrineSchemaManager(); - $details = $manager->listTableDetails('prefix_users'); - $this->assertTrue($details->hasIndex('index1')); - $this->assertFalse($details->hasIndex('index2')); + $indexes = array_column($schema->getIndexes('users'), 'name'); + + $this->assertContains('index1', $indexes); + $this->assertNotContains('index2', $indexes); $schema->table('users', function (Blueprint $table) { $table->renameIndex('index1', 'index2'); }); - $details = $manager->listTableDetails('prefix_users'); - $this->assertFalse($details->hasIndex('index1')); - $this->assertTrue($details->hasIndex('index2')); + $indexes = $schema->getIndexes('users'); - $this->assertEquals(['name', 'email'], $details->getIndex('index2')->getUnquotedColumns()); + $this->assertNotContains('index1', array_column($indexes, 'name')); + $this->assertTrue(collect($indexes)->contains( + fn ($index) => $index['name'] === 'index2' && $index['columns'] === ['name', 'email'] + )); } public function testAddingPrimaryKey() diff --git a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php index d469645fc8a1..ce261ab4e94d 100644 --- a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php @@ -87,7 +87,10 @@ public function testHasColumnAndIndexWithPrefixIndexDisabled() $table->string('name')->index(); }); - $this->assertArrayHasKey('table1_name_index', $this->db->connection()->getDoctrineSchemaManager()->listTableIndexes('example_table1')); + $this->assertContains( + 'table1_name_index', + array_column($this->db->connection()->getSchemaBuilder()->getIndexes('table1'), 'name') + ); } public function testHasColumnAndIndexWithPrefixIndexEnabled() @@ -104,7 +107,10 @@ public function testHasColumnAndIndexWithPrefixIndexEnabled() $table->string('name')->index(); }); - $this->assertArrayHasKey('example_table1_name_index', $this->db->connection()->getDoctrineSchemaManager()->listTableIndexes('example_table1')); + $this->assertContains( + 'example_table1_name_index', + array_column($this->db->connection()->getSchemaBuilder()->getIndexes('table1'), 'name') + ); } public function testDropColumnWithTablePrefix() diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index a8ea8fbaba63..2e79303b0574 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -181,4 +181,84 @@ public function testGetViews() $this->assertEmpty(array_diff(['foo', 'bar', 'baz'], array_column($views, 'name'))); } + + public function testGetIndexes() + { + Schema::create('foo', function (Blueprint $table) { + $table->string('bar')->index('my_index'); + }); + + $indexes = Schema::getIndexes('foo'); + + $this->assertCount(1, $indexes); + $this->assertTrue( + $indexes[0]['name'] === 'my_index' + && $indexes[0]['columns'] === ['bar'] + && ! $indexes[0]['unique'] + && ! $indexes[0]['primary'] + ); + } + + public function testGetUniqueIndexes() + { + Schema::create('foo', function (Blueprint $table) { + $table->id(); + $table->string('bar'); + $table->integer('baz'); + + $table->unique(['baz', 'bar']); + }); + + $indexes = Schema::getIndexes('foo'); + + $this->assertCount(2, $indexes); + $this->assertTrue(collect($indexes)->contains( + fn ($index) => $index['columns'] === ['id'] && $index['primary'] + )); + $this->assertTrue(collect($indexes)->contains( + fn ($index) => $index['name'] === 'foo_baz_bar_unique' && $index['columns'] === ['baz', 'bar'] && $index['unique'] + )); + } + + public function testGetIndexesWithCompositeKeys() + { + Schema::create('foo', function (Blueprint $table) { + $table->unsignedBigInteger('key'); + $table->string('bar')->unique(); + $table->integer('baz'); + + $table->primary(['baz', 'key']); + }); + + $indexes = Schema::getIndexes('foo'); + + $this->assertCount(2, $indexes); + $this->assertTrue(collect($indexes)->contains( + fn ($index) => $index['columns'] === ['baz', 'key'] && $index['primary'] + )); + $this->assertTrue(collect($indexes)->contains( + fn ($index) => $index['name'] === 'foo_bar_unique' && $index['columns'] === ['bar'] && $index['unique'] + )); + } + + public function testGetFullTextIndexes() + { + if (! in_array($this->driver, ['pgsql', 'mysql'])) { + $this->markTestSkipped('Test requires a MySQL or a PostgreSQL connection.'); + } + + Schema::create('articles', function (Blueprint $table) { + $table->id(); + $table->string('title', 200); + $table->text('body'); + + $table->fulltext(['body', 'title']); + }); + + $indexes = Schema::getIndexes('articles'); + + $this->assertCount(2, $indexes); + $this->assertTrue(collect($indexes)->contains(fn ($index) => $index['columns'] === ['id'] && $index['primary'])); + $this->assertTrue(collect($indexes)->contains('name', 'articles_body_title_fulltext')); + } } From 84e27723f63fc38e4043cdcfbb5b2fccf788ef50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bour?= Date: Wed, 6 Dec 2023 16:24:58 +0100 Subject: [PATCH 159/207] [10.x] Filesystem : can lock file on append of content (#49262) * [10.x] Can lock file on append of content * Update facade docblocks * Update Filesystem.php --------- Co-authored-by: StephaneBour Co-authored-by: Taylor Otwell --- src/Illuminate/Filesystem/Filesystem.php | 5 +++-- src/Illuminate/Support/Facades/File.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Filesystem/Filesystem.php b/src/Illuminate/Filesystem/Filesystem.php index ef732991d867..23fc17eeb03c 100644 --- a/src/Illuminate/Filesystem/Filesystem.php +++ b/src/Illuminate/Filesystem/Filesystem.php @@ -268,11 +268,12 @@ public function prepend($path, $data) * * @param string $path * @param string $data + * @param bool $lock * @return int */ - public function append($path, $data) + public function append($path, $data, $lock = false) { - return file_put_contents($path, $data, FILE_APPEND); + return file_put_contents($path, $data, FILE_APPEND | ($lock ? LOCK_EX : 0)); } /** diff --git a/src/Illuminate/Support/Facades/File.php b/src/Illuminate/Support/Facades/File.php index 8fa9aee26b6d..a1ade8789643 100755 --- a/src/Illuminate/Support/Facades/File.php +++ b/src/Illuminate/Support/Facades/File.php @@ -16,7 +16,7 @@ * @method static void replace(string $path, string $content, int|null $mode = null) * @method static void replaceInFile(array|string $search, array|string $replace, string $path) * @method static int prepend(string $path, string $data) - * @method static int append(string $path, string $data) + * @method static int append(string $path, string $data, bool $lock = false) * @method static mixed chmod(string $path, int|null $mode = null) * @method static bool delete(string|array $paths) * @method static bool move(string $path, string $target) From 9bc7831affdf5221b8305b8c48de7adc03a6db06 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 7 Dec 2023 08:30:33 +0800 Subject: [PATCH 160/207] [10.x] Test Improvements (#49266) Signed-off-by: Mior Muhammad Zaki --- .github/workflows/databases.yml | 10 +++++----- .github/workflows/tests.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 20e008c66cbe..2bb38f0f1ab2 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -36,7 +36,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.1 - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none @@ -81,7 +81,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.1 - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none @@ -126,7 +126,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.1 - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none @@ -172,7 +172,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.1 - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, :php-psr tools: composer:v2 coverage: none @@ -216,7 +216,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.1 - extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc + extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr tools: composer:v2 coverage: none diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8c27dfb9f27b..5cda208db8ec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,7 +54,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis-phpredis/phpredis@5.3.7, igbinary, msgpack, lzf, zstd, lz4, memcached, gmp + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis-phpredis/phpredis@5.3.7, igbinary, msgpack, lzf, zstd, lz4, memcached, gmp, :php-psr ini-values: error_reporting=E_ALL tools: composer:v2 coverage: none @@ -128,7 +128,7 @@ jobs: uses: shivammathur/setup-php@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 + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, gd, pdo_mysql, fileinfo, ftp, redis, memcached, gmp, intl, :php-psr tools: composer:v2 coverage: none From 0f188f38e433da498259104d4688470bf4fb3630 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Thu, 7 Dec 2023 00:31:12 +0000 Subject: [PATCH 161/207] Update facade docblocks --- src/Illuminate/Support/Facades/Cache.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 054b0e24c8ca..a4e8af8e62ed 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -18,12 +18,12 @@ * @method static bool missing(string $key) * @method static mixed get(array|string $key, mixed|\Closure $default = null) * @method static array many(array $keys) - * @method static iterable getMultiple(iterable $keys, mixed $default = null) + * @method static iterable getMultiple(void $keys, void $default = null) * @method static mixed pull(array|string $key, mixed|\Closure $default = null) * @method static bool put(array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) - * @method static bool set(string $key, mixed $value, null|int|\DateInterval $ttl = null) + * @method static bool set(void $key, void $value, void $ttl = null) * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) - * @method static bool setMultiple(iterable $values, null|int|\DateInterval $ttl = null) + * @method static bool setMultiple(void $values, void $ttl = null) * @method static bool add(string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static int|bool increment(string $key, mixed $value = 1) * @method static int|bool decrement(string $key, mixed $value = 1) @@ -32,8 +32,8 @@ * @method static mixed sear(string $key, \Closure $callback) * @method static mixed rememberForever(string $key, \Closure $callback) * @method static bool forget(string $key) - * @method static bool delete(string $key) - * @method static bool deleteMultiple(iterable $keys) + * @method static bool delete(void $key) + * @method static bool deleteMultiple(void $keys) * @method static bool clear() * @method static \Illuminate\Cache\TaggedCache tags(array|mixed $names) * @method static bool supportsTags() From 81d5ed94136bdc5da091b4901c5aec8c2fa1c76b Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 7 Dec 2023 09:25:31 +0800 Subject: [PATCH 162/207] [10.x] Fixes generating facades documentation shouldn't be affected by `php-psr` extension (#49268) * [10.x] Facade Doc Improvements Signed-off-by: Mior Muhammad Zaki * Update facade docblocks * Update .github/workflows/facades.yml --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: crynobone --- .github/workflows/facades.yml | 2 ++ src/Illuminate/Support/Facades/Cache.php | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index 2e246fa2e38c..fe4b95f2f4b3 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -5,6 +5,7 @@ on: branches: - master - '*.x' + workflow_dispatch: jobs: update: @@ -24,6 +25,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: + extensions: :php-psr php-version: 8.1 tools: composer:v2 coverage: none diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index a4e8af8e62ed..054b0e24c8ca 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -18,12 +18,12 @@ * @method static bool missing(string $key) * @method static mixed get(array|string $key, mixed|\Closure $default = null) * @method static array many(array $keys) - * @method static iterable getMultiple(void $keys, void $default = null) + * @method static iterable getMultiple(iterable $keys, mixed $default = null) * @method static mixed pull(array|string $key, mixed|\Closure $default = null) * @method static bool put(array|string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) - * @method static bool set(void $key, void $value, void $ttl = null) + * @method static bool set(string $key, mixed $value, null|int|\DateInterval $ttl = null) * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) - * @method static bool setMultiple(void $values, void $ttl = null) + * @method static bool setMultiple(iterable $values, null|int|\DateInterval $ttl = null) * @method static bool add(string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static int|bool increment(string $key, mixed $value = 1) * @method static int|bool decrement(string $key, mixed $value = 1) @@ -32,8 +32,8 @@ * @method static mixed sear(string $key, \Closure $callback) * @method static mixed rememberForever(string $key, \Closure $callback) * @method static bool forget(string $key) - * @method static bool delete(void $key) - * @method static bool deleteMultiple(void $keys) + * @method static bool delete(string $key) + * @method static bool deleteMultiple(iterable $keys) * @method static bool clear() * @method static \Illuminate\Cache\TaggedCache tags(array|mixed $names) * @method static bool supportsTags() From 7be968ae8afb8be82d57b4acc2eda19197b377c1 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 8 Dec 2023 03:25:55 +0800 Subject: [PATCH 163/207] [10.x] Fixes `AboutCommand::format()` docblock (#49274) * [10.x] Fixes `AboutCommand::format()` docblock Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- src/Illuminate/Foundation/Console/AboutCommand.php | 4 ++-- types/Foundation/AboutCommand.php | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 types/Foundation/AboutCommand.php diff --git a/src/Illuminate/Foundation/Console/AboutCommand.php b/src/Illuminate/Foundation/Console/AboutCommand.php index 3b3c2cd958e8..23d2b23b631a 100644 --- a/src/Illuminate/Foundation/Console/AboutCommand.php +++ b/src/Illuminate/Foundation/Console/AboutCommand.php @@ -273,8 +273,8 @@ protected function sections() * Materialize a function that formats a given value for CLI or JSON output. * * @param mixed $value - * @param (\Closure():(mixed))|null $console - * @param (\Closure():(mixed))|null $json + * @param (\Closure(mixed):(mixed))|null $console + * @param (\Closure(mixed):(mixed))|null $json * @return \Closure(bool):mixed */ public static function format($value, Closure $console = null, Closure $json = null) diff --git a/types/Foundation/AboutCommand.php b/types/Foundation/AboutCommand.php new file mode 100644 index 000000000000..61718f36914a --- /dev/null +++ b/types/Foundation/AboutCommand.php @@ -0,0 +1,11 @@ + $value ? 'ENABLED' : 'OFF'); + +assertType('Closure(bool): mixed', $format); +assertType('mixed', $format(false)); +assertType('mixed', $format(true)); From 88d6037dcda4f5696b85ce24ad11ff7e97fd14f0 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 8 Dec 2023 03:28:35 +0800 Subject: [PATCH 164/207] [10.x] `Route::getController()` should return `null` when the accessing (#49269) closure based route fixes #49267 Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Routing/Route.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index 886d85165f04..7664500784f8 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -268,6 +268,10 @@ protected function runController() */ public function getController() { + if (! $this->isControllerAction()) { + return null; + } + if (! $this->controller) { $class = $this->getControllerClass(); From 1771cd5c28c23baf01c04cfa77c3fab018f62685 Mon Sep 17 00:00:00 2001 From: Anton Cherednichenko Date: Sat, 9 Dec 2023 16:59:30 +0200 Subject: [PATCH 165/207] Added "noActionOnUpdate" in ForeignKeyDefinition (#49297) --- .../Database/Schema/ForeignKeyDefinition.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Illuminate/Database/Schema/ForeignKeyDefinition.php b/src/Illuminate/Database/Schema/ForeignKeyDefinition.php index 3bb8b719ea59..6682da30c81b 100644 --- a/src/Illuminate/Database/Schema/ForeignKeyDefinition.php +++ b/src/Illuminate/Database/Schema/ForeignKeyDefinition.php @@ -34,6 +34,16 @@ public function restrictOnUpdate() return $this->onUpdate('restrict'); } + /** + * Indicate that updates should have "no action". + * + * @return $this + */ + public function noActionOnUpdate() + { + return $this->onUpdate('no action'); + } + /** * Indicate that deletes should cascade. * From ea3d4797d79807f9ad51e198348ad699b023c10f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 9 Dec 2023 15:15:02 +0000 Subject: [PATCH 166/207] [11.x] Improves `RedirectIfAuthenticated` middleware default redirect (#49286) * Improves `RedirectIfAuthenticated` * Apply fixes from StyleCI * Improves sorting * Improves perf * formatting --------- Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Middleware/RedirectIfAuthenticated.php | 27 ++++- .../Configuration/ApplicationBuilder.php | 3 +- .../RedirectIfAuthenticatedTest.php | 108 ++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 tests/Integration/Auth/Middleware/RedirectIfAuthenticatedTest.php diff --git a/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php b/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php index c436ef16ee0a..958229947daf 100644 --- a/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php +++ b/src/Illuminate/Auth/Middleware/RedirectIfAuthenticated.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Route; use Symfony\Component\HttpFoundation\Response; class RedirectIfAuthenticated @@ -12,7 +13,7 @@ class RedirectIfAuthenticated /** * The callback that should be used to generate the authentication redirect path. * - * @var callable + * @var callable|null */ protected static $redirectToCallback; @@ -41,7 +42,29 @@ protected function redirectTo(Request $request): ?string { return static::$redirectToCallback ? call_user_func(static::$redirectToCallback, $request) - : '/dashboard'; + : $this->defaultRedirectUri(); + } + + /** + * Get the default URI the user should be redirected to when they are authenticated. + */ + protected function defaultRedirectUri(): string + { + foreach (['dashboard', 'home'] as $uri) { + if (Route::has($uri)) { + return route($uri); + } + } + + $routes = Route::getRoutes()->get('GET'); + + foreach (['dashboard', 'home'] as $uri) { + if (isset($routes[$uri])) { + return '/'.$uri; + } + } + + return '/'; } /** diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index dd213b977743..bfefaeda3bbe 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -191,8 +191,7 @@ public function withMiddleware(callable $callback) { $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { $middleware = (new Middleware) - ->auth(redirectTo: fn () => route('login')) - ->guest(redirectTo: fn () => route('dashboard')); + ->auth(redirectTo: fn () => route('login')); $callback($middleware); diff --git a/tests/Integration/Auth/Middleware/RedirectIfAuthenticatedTest.php b/tests/Integration/Auth/Middleware/RedirectIfAuthenticatedTest.php new file mode 100644 index 000000000000..c0db2c4eb1ca --- /dev/null +++ b/tests/Integration/Auth/Middleware/RedirectIfAuthenticatedTest.php @@ -0,0 +1,108 @@ +withoutExceptionHandling(); + + /** @var \Illuminate\Contracts\Routing\Registrar $router */ + $this->router = $this->app->make(Registrar::class); + + $this->router->get('/login', function () { + return response('Login Form'); + })->middleware(RedirectIfAuthenticated::class); + + UserFactory::new()->create(); + + $user = AuthenticationTestUser::first(); + $this->router->get('/login', function () { + return response('Login Form'); + })->middleware(RedirectIfAuthenticated::class); + + UserFactory::new()->create(); + + $this->user = AuthenticationTestUser::first(); + } + + protected function defineEnvironment($app) + { + $app['config']->set('app.key', Str::random(32)); + $app['config']->set('auth.providers.users.model', AuthenticationTestUser::class); + } + + protected function defineDatabaseMigrations() + { + $this->loadLaravelMigrations(); + } + + public function testWhenDashboardNamedRouteIsAvailable() + { + $this->router->get('/named-dashboard', function () { + return response('Named Dashboard'); + })->name('dashboard'); + + $response = $this->actingAs($this->user)->get('/login'); + + $response->assertRedirect('/named-dashboard'); + } + + public function testWhenHomeNamedRouteIsAvailable() + { + $this->router->get('/named-home', function () { + return response('Named Home'); + })->name('home'); + + $response = $this->actingAs($this->user)->get('/login'); + + $response->assertRedirect('/named-home'); + } + + public function testWhenDashboardSlugIsAvailable() + { + $this->router->get('/dashboard', function () { + return response('My Dashboard'); + }); + + $response = $this->actingAs($this->user)->get('/login'); + + $response->assertRedirect('/dashboard'); + } + + public function testWhenHomeSlugIsAvailable() + { + $this->router->get('/home', function () { + return response('My Home'); + })->name('home'); + + $response = $this->actingAs($this->user)->get('/login'); + + $response->assertRedirect('/home'); + } + + public function testWhenHomeOrDashboardAreNotAvailable() + { + $response = $this->actingAs($this->user)->get('/login'); + + $response->assertRedirect('/'); + } + + public function testWhenGuest() + { + $response = $this->get('/login'); + + $response->assertOk(); + $response->assertSee('Login Form'); + } +} From c3696ca14a32592396215c4d276c6bbdf8ef2d0c Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+mr-punyapal@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:48:21 +0530 Subject: [PATCH 167/207] [10.x] Fixing number helper for floating 0.0 (#49277) * added test case for 0.0 and 0.00 in Number helper * added int type casting while checking for 0 I have added tests which are failing if we pass 0.0 or 0.00 floating value for 0 giving Division by zero exception... * Fix comparison of float values in summarize method * Refactor number formatting and summarization * Fix formatting in SupportNumberTest.php * Refactor SupportNumberTest abbreviate method --- src/Illuminate/Support/Number.php | 4 ++-- tests/Support/SupportNumberTest.php | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index 954c05875a50..a101e903beb3 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -193,8 +193,8 @@ protected static function summarize(int|float $number, int $precision = 0, ?int } switch (true) { - case $number === 0: - return '0'; + case floatval($number) === 0.0: + return $precision > 0 ? static::format(0, $precision, $maxPrecision) : '0'; case $number < 0: return sprintf('-%s', static::summarize(abs($number), $precision, $maxPrecision, $units)); case $number >= 1e15: diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index ce8463bce324..67644fdcb209 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -12,6 +12,8 @@ public function testFormat() $this->needsIntlExtension(); $this->assertSame('0', Number::format(0)); + $this->assertSame('0', Number::format(0.0)); + $this->assertSame('0', Number::format(0.00)); $this->assertSame('1', Number::format(1)); $this->assertSame('10', Number::format(10)); $this->assertSame('25', Number::format(25)); @@ -193,6 +195,9 @@ public function testToHuman() $this->assertSame('1 thousand quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000000)); $this->assertSame('0', Number::forHumans(0)); + $this->assertSame('0', Number::forHumans(0.0)); + $this->assertSame('0.00', Number::forHumans(0, 2)); + $this->assertSame('0.00', Number::forHumans(0.0, 2)); $this->assertSame('-1', Number::forHumans(-1)); $this->assertSame('-1.00', Number::forHumans(-1, precision: 2)); $this->assertSame('-10', Number::forHumans(-10)); @@ -246,6 +251,9 @@ public function testSummarize() $this->assertSame('1KQQ', Number::abbreviate(1000000000000000000000000000000000)); $this->assertSame('0', Number::abbreviate(0)); + $this->assertSame('0', Number::abbreviate(0.0)); + $this->assertSame('0.00', Number::abbreviate(0, 2)); + $this->assertSame('0.00', Number::abbreviate(0.0, 2)); $this->assertSame('-1', Number::abbreviate(-1)); $this->assertSame('-1.00', Number::abbreviate(-1, precision: 2)); $this->assertSame('-10', Number::abbreviate(-10)); From 6e302e8aadf1c0779cc665176bf52443ceef17a0 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sat, 9 Dec 2023 23:19:58 +0800 Subject: [PATCH 168/207] [11.x] Test Improvements (#49271) Signed-off-by: Mior Muhammad Zaki --- .github/workflows/tests.yml | 2 -- tests/Integration/Http/ThrottleRequestsTest.php | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 55ff3d93fcc5..292e06fd86d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,6 @@ jobs: timeout_minutes: 5 max_attempts: 5 command: composer require guzzlehttp/psr7:^2.4 --no-interaction --no-update - if: matrix.php >= 8.2 - name: Install dependencies uses: nick-fields/retry@v2 @@ -131,7 +130,6 @@ jobs: timeout_minutes: 5 max_attempts: 5 command: composer require guzzlehttp/psr7:~2.4 --no-interaction --no-update - if: matrix.php >= 8.2 - name: Install dependencies uses: nick-fields/retry@v2 diff --git a/tests/Integration/Http/ThrottleRequestsTest.php b/tests/Integration/Http/ThrottleRequestsTest.php index d0f43260eaa2..78f1df58ea32 100644 --- a/tests/Integration/Http/ThrottleRequestsTest.php +++ b/tests/Integration/Http/ThrottleRequestsTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Route; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Throwable; class ThrottleRequestsTest extends TestCase @@ -127,7 +128,7 @@ public static function perMinuteThrottlingDataSet() ]; } - /** @dataProvider perMinuteThrottlingDataSet */ + #[DataProvider('perMinuteThrottlingDataSet')] public function testItCanThrottlePerMinute(string $middleware) { $rateLimiter = Container::getInstance()->make(RateLimiter::class); From 234444e8fb17daf16a703877e3fb8ccb4b84cad4 Mon Sep 17 00:00:00 2001 From: Joostb Date: Sat, 9 Dec 2023 16:31:52 +0100 Subject: [PATCH 169/207] [10.x] Allow checking if lock succesfully restored (#49272) * Add function to validate if the lock is owned by the current owner. * make Lock->isOwnedByCurrentProcess public remove change to contract and add tests * fix styling --------- Co-authored-by: Joost --- src/Illuminate/Cache/Lock.php | 2 +- tests/Integration/Cache/FileCacheLockTest.php | 23 +++++++++++++++++++ .../Cache/MemcachedCacheLockTestCase.php | 23 +++++++++++++++++++ .../Integration/Cache/RedisCacheLockTest.php | 23 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/Lock.php b/src/Illuminate/Cache/Lock.php index ccd1c6474a5f..4868fdf82a50 100644 --- a/src/Illuminate/Cache/Lock.php +++ b/src/Illuminate/Cache/Lock.php @@ -148,7 +148,7 @@ public function owner() * * @return bool */ - protected function isOwnedByCurrentProcess() + public function isOwnedByCurrentProcess() { return $this->getCurrentOwner() === $this->owner; } diff --git a/tests/Integration/Cache/FileCacheLockTest.php b/tests/Integration/Cache/FileCacheLockTest.php index 98ef814d212d..c00409fe9e0e 100644 --- a/tests/Integration/Cache/FileCacheLockTest.php +++ b/tests/Integration/Cache/FileCacheLockTest.php @@ -95,4 +95,27 @@ public function testLocksCanBeReleasedUsingOwnerToken() $this->assertTrue(Cache::lock('foo')->get()); } + + public function testOwnerStatusCanBeCheckedAfterRestoringLock() + { + Cache::lock('foo')->forceRelease(); + + $firstLock = Cache::lock('foo', 10); + $this->assertTrue($firstLock->get()); + $owner = $firstLock->owner(); + + $secondLock = Cache::store('file')->restoreLock('foo', $owner); + $this->assertTrue($secondLock->isOwnedByCurrentProcess()); + } + + public function testOtherOwnerDoesNotOwnLockAfterRestore() + { + Cache::lock('foo')->forceRelease(); + + $firstLock = Cache::lock('foo', 10); + $this->assertTrue($firstLock->get()); + + $secondLock = Cache::store('file')->restoreLock('foo', 'other_owner'); + $this->assertFalse($secondLock->isOwnedByCurrentProcess()); + } } diff --git a/tests/Integration/Cache/MemcachedCacheLockTestCase.php b/tests/Integration/Cache/MemcachedCacheLockTestCase.php index 7f3e93f8c7c0..8dbbd481f17e 100644 --- a/tests/Integration/Cache/MemcachedCacheLockTestCase.php +++ b/tests/Integration/Cache/MemcachedCacheLockTestCase.php @@ -80,4 +80,27 @@ public function testMemcachedLocksCanBeReleasedUsingOwnerToken() $this->assertTrue(Cache::store('memcached')->lock('foo')->get()); } + + public function testOwnerStatusCanBeCheckedAfterRestoringLock() + { + Cache::store('memcached')->lock('foo')->forceRelease(); + + $firstLock = Cache::store('memcached')->lock('foo', 10); + $this->assertTrue($firstLock->get()); + $owner = $firstLock->owner(); + + $secondLock = Cache::store('memcached')->restoreLock('foo', $owner); + $this->assertTrue($secondLock->isOwnedByCurrentProcess()); + } + + public function testOtherOwnerDoesNotOwnLockAfterRestore() + { + Cache::store('memcached')->lock('foo')->forceRelease(); + + $firstLock = Cache::store('memcached')->lock('foo', 10); + $this->assertTrue($firstLock->get()); + + $secondLock = Cache::store('memcached')->restoreLock('foo', 'other_owner'); + $this->assertFalse($secondLock->isOwnedByCurrentProcess()); + } } diff --git a/tests/Integration/Cache/RedisCacheLockTest.php b/tests/Integration/Cache/RedisCacheLockTest.php index a225f47a0780..b5de1eb58891 100644 --- a/tests/Integration/Cache/RedisCacheLockTest.php +++ b/tests/Integration/Cache/RedisCacheLockTest.php @@ -108,4 +108,27 @@ public function testRedisLocksCanBeReleasedUsingOwnerToken() $this->assertTrue(Cache::store('redis')->lock('foo')->get()); } + + public function testOwnerStatusCanBeCheckedAfterRestoringLock() + { + Cache::store('redis')->lock('foo')->forceRelease(); + + $firstLock = Cache::store('redis')->lock('foo', 10); + $this->assertTrue($firstLock->get()); + $owner = $firstLock->owner(); + + $secondLock = Cache::store('redis')->restoreLock('foo', $owner); + $this->assertTrue($secondLock->isOwnedByCurrentProcess()); + } + + public function testOtherOwnerDoesNotOwnLockAfterRestore() + { + Cache::store('redis')->lock('foo')->forceRelease(); + + $firstLock = Cache::store('redis')->lock('foo', 10); + $this->assertTrue($firstLock->get()); + + $secondLock = Cache::store('redis')->restoreLock('foo', 'other_owner'); + $this->assertFalse($secondLock->isOwnedByCurrentProcess()); + } } From d6ecd07dc69ad173f30a23ec71f42e245549d940 Mon Sep 17 00:00:00 2001 From: Sebastien Armand Date: Sat, 9 Dec 2023 07:45:29 -0800 Subject: [PATCH 170/207] [10.x] Enable DynamoDB as a backend for Job Batches (#49169) * Enable DynamoDB as a backend for Job Batches * Update TTL on updates * port * ttl validation * hmm * hmm * hmm * hmm * hmm * hmm * simpler * simpler * move * recorder * correct endpoint * Dynamo pod not accessible from windows * short ttl * Fallback to consistent reads * style ci * style ci * style ci * style ci * style ci * Unnecessary style changes * rollback * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/BusServiceProvider.php | 36 +- src/Illuminate/Bus/DynamoBatchRepository.php | 535 ++++++++++++++++++ tests/Integration/Queue/DynamoBatchTest.php | 198 +++++++ .../Queue/DynamoBatchTestWithTTL.php | 19 + 4 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Bus/DynamoBatchRepository.php create mode 100644 tests/Integration/Queue/DynamoBatchTest.php create mode 100644 tests/Integration/Queue/DynamoBatchTestWithTTL.php diff --git a/src/Illuminate/Bus/BusServiceProvider.php b/src/Illuminate/Bus/BusServiceProvider.php index bd6192d0c48e..4031e15f441c 100644 --- a/src/Illuminate/Bus/BusServiceProvider.php +++ b/src/Illuminate/Bus/BusServiceProvider.php @@ -2,10 +2,12 @@ namespace Illuminate\Bus; +use Aws\DynamoDb\DynamoDbClient; use Illuminate\Contracts\Bus\Dispatcher as DispatcherContract; use Illuminate\Contracts\Bus\QueueingDispatcher as QueueingDispatcherContract; use Illuminate\Contracts\Queue\Factory as QueueFactoryContract; use Illuminate\Contracts\Support\DeferrableProvider; +use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; class BusServiceProvider extends ServiceProvider implements DeferrableProvider @@ -41,7 +43,13 @@ public function register() */ protected function registerBatchServices() { - $this->app->singleton(BatchRepository::class, DatabaseBatchRepository::class); + $this->app->singleton(BatchRepository::class, function ($app) { + $driver = $app->config->get('queue.batching.driver', 'database'); + + return $driver === 'dynamodb' + ? $app->make(DynamoBatchRepository::class) + : $app->make(DatabaseBatchRepository::class); + }); $this->app->singleton(DatabaseBatchRepository::class, function ($app) { return new DatabaseBatchRepository( @@ -50,6 +58,32 @@ protected function registerBatchServices() $app->config->get('queue.batching.table', 'job_batches') ); }); + + $this->app->singleton(DynamoBatchRepository::class, function ($app) { + $config = $app->config->get('queue.batching'); + + $dynamoConfig = [ + 'region' => $config['region'], + 'version' => 'latest', + 'endpoint' => $config['endpoint'] ?? null, + ]; + + if (! empty($config['key']) && ! empty($config['secret'])) { + $dynamoConfig['credentials'] = Arr::only( + $config, + ['key', 'secret', 'token'] + ); + } + + return new DynamoBatchRepository( + $app->make(BatchFactory::class), + new DynamoDbClient($dynamoConfig), + $app->config->get('app.name'), + $app->config->get('queue.batching.table', 'job_batches'), + ttl: $app->config->get('queue.batching.ttl', null), + ttlAttribute: $app->config->get('queue.batching.ttl_attribute', 'ttl'), + ); + }); } /** diff --git a/src/Illuminate/Bus/DynamoBatchRepository.php b/src/Illuminate/Bus/DynamoBatchRepository.php new file mode 100644 index 000000000000..25a4d4a90ebc --- /dev/null +++ b/src/Illuminate/Bus/DynamoBatchRepository.php @@ -0,0 +1,535 @@ +factory = $factory; + $this->dynamoDbClient = $dynamoDbClient; + $this->applicationName = $applicationName; + $this->table = $table; + $this->ttl = $ttl; + $this->ttlAttribute = $ttlAttribute; + $this->marshaler = new Marshaler; + } + + /** + * Retrieve a list of batches. + * + * @param int $limit + * @param mixed $before + * @return \Illuminate\Bus\Batch[] + */ + public function get($limit = 50, $before = null) + { + $condition = 'application = :application'; + + if ($before) { + $condition = 'application = :application AND id < :id'; + } + + $result = $this->dynamoDbClient->query([ + 'TableName' => $this->table, + 'KeyConditionExpression' => $condition, + 'ExpressionAttributeValues' => array_filter([ + ':application' => ['S' => $this->applicationName], + ':id' => array_filter(['S' => $before]), + ]), + 'Limit' => $limit, + ]); + + return array_map( + fn ($b) => $this->toBatch($this->marshaler->unmarshalItem($b, mapAsObject: true)), + $result['Items'] + ); + } + + /** + * Retrieve information about an existing batch. + * + * @param string $batchId + * @return \Illuminate\Bus\Batch|null + */ + public function find(string $batchId) + { + if ($batchId === '') { + return null; + } + + $b = $this->dynamoDbClient->getItem([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + ]); + + if (! isset($b['Item'])) { + // If we didn't find it via a standard read, attempt consistent read... + $b = $this->dynamoDbClient->getItem([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + 'ConsistentRead' => true, + ]); + + if (! isset($b['Item'])) { + return null; + } + } + + $batch = $this->marshaler->unmarshalItem($b['Item'], mapAsObject: true); + + if ($batch) { + return $this->toBatch($batch); + } + } + + /** + * Store a new pending batch. + * + * @param \Illuminate\Bus\PendingBatch $batch + * @return \Illuminate\Bus\Batch + */ + public function store(PendingBatch $batch) + { + $id = (string) Str::orderedUuid(); + + $batch = [ + 'id' => $id, + 'name' => $batch->name, + 'total_jobs' => 0, + 'pending_jobs' => 0, + 'failed_jobs' => 0, + 'failed_job_ids' => [], + 'options' => $this->serialize($batch->options ?? []), + 'created_at' => time(), + 'cancelled_at' => null, + 'finished_at' => null, + ]; + + if (! is_null($this->ttl)) { + $batch[$this->ttlAttribute] = time() + $this->ttl; + } + + $this->dynamoDbClient->putItem([ + 'TableName' => $this->table, + 'Item' => $this->marshaler->marshalItem( + array_merge(['application' => $this->applicationName], $batch) + ), + ]); + + return $this->find($id); + } + + /** + * Increment the total number of jobs within the batch. + * + * @param string $batchId + * @param int $amount + * @return void + */ + public function incrementTotalJobs(string $batchId, int $amount) + { + $update = 'SET total_jobs = total_jobs + :val, pending_jobs = pending_jobs + :val'; + + if ($this->ttl) { + $update = "SET total_jobs = total_jobs + :val, pending_jobs = pending_jobs + :val, #{$this->ttlAttribute} = :ttl"; + } + + $this->dynamoDbClient->updateItem(array_filter([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + 'UpdateExpression' => $update, + 'ExpressionAttributeValues' => array_filter([ + ':val' => ['N' => "$amount"], + ':ttl' => array_filter(['N' => $this->getExpiryTime()]), + ]), + 'ExpressionAttributeNames' => $this->ttlExpressionAttributeName(), + 'ReturnValues' => 'ALL_NEW', + ])); + } + + /** + * Decrement the total number of pending jobs for the batch. + * + * @param string $batchId + * @param string $jobId + * @return \Illuminate\Bus\UpdatedBatchJobCounts + */ + public function decrementPendingJobs(string $batchId, string $jobId) + { + $update = 'SET pending_jobs = pending_jobs - :inc'; + + if ($this->ttl !== null) { + $update = "SET pending_jobs = pending_jobs - :inc, #{$this->ttlAttribute} = :ttl"; + } + + $batch = $this->dynamoDbClient->updateItem(array_filter([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + 'UpdateExpression' => $update, + 'ExpressionAttributeValues' => array_filter([ + ':inc' => ['N' => '1'], + ':ttl' => array_filter(['N' => $this->getExpiryTime()]), + ]), + 'ExpressionAttributeNames' => $this->ttlExpressionAttributeName(), + 'ReturnValues' => 'ALL_NEW', + ])); + + $values = $this->marshaler->unmarshalItem($batch['Attributes']); + + return new UpdatedBatchJobCounts( + $values['pending_jobs'], + $values['failed_jobs'] + ); + } + + /** + * Increment the total number of failed jobs for the batch. + * + * @param string $batchId + * @param string $jobId + * @return \Illuminate\Bus\UpdatedBatchJobCounts + */ + public function incrementFailedJobs(string $batchId, string $jobId) + { + $update = 'SET failed_jobs = failed_jobs + :inc, failed_job_ids = list_append(failed_job_ids, :jobId)'; + + if ($this->ttl !== null) { + $update = "SET failed_jobs = failed_jobs + :inc, failed_job_ids = list_append(failed_job_ids, :jobId), #{$this->ttlAttribute} = :ttl"; + } + + $batch = $this->dynamoDbClient->updateItem(array_filter([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + 'UpdateExpression' => $update, + 'ExpressionAttributeValues' => array_filter([ + ':jobId' => $this->marshaler->marshalValue([$jobId]), + ':inc' => ['N' => '1'], + ':ttl' => array_filter(['N' => $this->getExpiryTime()]), + ]), + 'ExpressionAttributeNames' => $this->ttlExpressionAttributeName(), + 'ReturnValues' => 'ALL_NEW', + ])); + + $values = $this->marshaler->unmarshalItem($batch['Attributes']); + + return new UpdatedBatchJobCounts( + $values['pending_jobs'], + $values['failed_jobs'] + ); + } + + /** + * Mark the batch that has the given ID as finished. + * + * @param string $batchId + * @return void + */ + public function markAsFinished(string $batchId) + { + $update = 'SET finished_at = :timestamp'; + + if ($this->ttl !== null) { + $update = "SET finished_at = :timestamp, #{$this->ttlAttribute} = :ttl"; + } + + $this->dynamoDbClient->updateItem(array_filter([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + 'UpdateExpression' => $update, + 'ExpressionAttributeValues' => array_filter([ + ':timestamp' => ['N' => (string) time()], + ':ttl' => array_filter(['N' => $this->getExpiryTime()]), + ]), + 'ExpressionAttributeNames' => $this->ttlExpressionAttributeName(), + ])); + } + + /** + * Cancel the batch that has the given ID. + * + * @param string $batchId + * @return void + */ + public function cancel(string $batchId) + { + $update = 'SET cancelled_at = :timestamp, finished_at = :timestamp'; + + if ($this->ttl !== null) { + $update = "SET cancelled_at = :timestamp, finished_at = :timestamp, #{$this->ttlAttribute} = :ttl"; + } + + $this->dynamoDbClient->updateItem(array_filter([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + 'UpdateExpression' => $update, + 'ExpressionAttributeValues' => array_filter([ + ':timestamp' => ['N' => (string) time()], + ':ttl' => array_filter(['N' => $this->getExpiryTime()]), + ]), + 'ExpressionAttributeNames' => $this->ttlExpressionAttributeName(), + ])); + } + + /** + * Delete the batch that has the given ID. + * + * @param string $batchId + * @return void + */ + public function delete(string $batchId) + { + $this->dynamoDbClient->deleteItem([ + 'TableName' => $this->table, + 'Key' => [ + 'application' => ['S' => $this->applicationName], + 'id' => ['S' => $batchId], + ], + ]); + } + + /** + * Execute the given Closure within a storage specific transaction. + * + * @param \Closure $callback + * @return mixed + */ + public function transaction(Closure $callback) + { + return $callback(); + } + + /** + * Rollback the last database transaction for the connection. + * + * @return void + */ + public function rollBack() + { + } + + /** + * Convert the given raw batch to a Batch object. + * + * @param object $batch + * @return \Illuminate\Bus\Batch + */ + protected function toBatch($batch) + { + return $this->factory->make( + $this, + $batch->id, + $batch->name, + (int) $batch->total_jobs, + (int) $batch->pending_jobs, + (int) $batch->failed_jobs, + $batch->failed_job_ids, + $this->unserialize($batch->options) ?? [], + CarbonImmutable::createFromTimestamp($batch->created_at), + $batch->cancelled_at ? CarbonImmutable::createFromTimestamp($batch->cancelled_at) : $batch->cancelled_at, + $batch->finished_at ? CarbonImmutable::createFromTimestamp($batch->finished_at) : $batch->finished_at + ); + } + + /** + * Create the underlying DynamoDB table. + * + * @return void + */ + public function createAwsDynamoTable(): void + { + $definition = [ + 'TableName' => $this->table, + 'AttributeDefinitions' => [ + [ + 'AttributeName' => 'application', + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => 'id', + 'AttributeType' => 'S', + ], + ], + 'KeySchema' => [ + [ + 'AttributeName' => 'application', + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => 'id', + 'KeyType' => 'RANGE', + ], + ], + 'BillingMode' => 'PAY_PER_REQUEST', + ]; + + $this->dynamoDbClient->createTable($definition); + + if (! is_null($this->ttl)) { + $this->dynamoDbClient->updateTimeToLive([ + 'TableName' => $this->table, + 'TimeToLiveSpecification' => [ + 'AttributeName' => $this->ttlAttribute, + 'Enabled' => true, + ], + ]); + } + } + + /** + * Delete the underlying DynamoDB table. + */ + public function deleteAwsDynamoTable(): void + { + $this->dynamoDbClient->deleteTable([ + 'TableName' => $this->table, + ]); + } + + /** + * Get the expiry time based on the configured time-to-live. + * + * @return string|null + */ + protected function getExpiryTime(): ?string + { + return is_null($this->ttl) ? null : (string) (time() + $this->ttl); + } + + /** + * Get the expression attribute name for the time-to-live attribute. + * + * @return array + */ + protected function ttlExpressionAttributeName(): array + { + return is_null($this->ttl) ? [] : ["#{$this->ttlAttribute}" => $this->ttlAttribute]; + } + + /** + * Serialize the given value. + * + * @param mixed $value + * @return string + */ + protected function serialize($value) + { + return serialize($value); + } + + /** + * Unserialize the given value. + * + * @param string $serialized + * @return mixed + */ + protected function unserialize($serialized) + { + return unserialize($serialized); + } + + /** + * Get the underlying DynamoDB client instance. + * + * @return \Aws\DynamoDb\DynamoDbClient + */ + public function getDynamoClient(): DynamoDbClient + { + return $this->dynamoDbClient; + } + + /** + * The the name of the table that contains the batch records. + * + * @return string + */ + public function getTable(): string + { + return $this->table; + } +} diff --git a/tests/Integration/Queue/DynamoBatchTest.php b/tests/Integration/Queue/DynamoBatchTest.php new file mode 100644 index 000000000000..43086a39d6aa --- /dev/null +++ b/tests/Integration/Queue/DynamoBatchTest.php @@ -0,0 +1,198 @@ +set('queue.batching', [ + 'driver' => 'dynamodb', + 'region' => 'us-west-2', + 'endpoint' => static::DYNAMODB_ENDPOINT, + 'key' => 'key', + 'secret' => 'secret', + ]); + } + + public function setUp(): void + { + parent::setUp(); + + BatchRunRecorder::reset(); + app(DynamoBatchRepository::class)->createAwsDynamoTable(); + } + + public function tearDown(): void + { + app(DynamoBatchRepository::class)->deleteAwsDynamoTable(); + + parent::tearDown(); + } + + public function test_running_a_batch() + { + Bus::batch([ + new BatchJob('1'), + new BatchJob('2'), + ])->dispatch(); + + $this->assertEquals(['1', '2'], BatchRunRecorder::$results); + } + + public function test_retrieve_batch_by_id() + { + $batch = Bus::batch([ + new BatchJob('1'), + new BatchJob('2'), + ])->dispatch(); + + /** @var DynamoBatchRepository */ + $repo = app(DynamoBatchRepository::class); + $retrieved = $repo->find($batch->id); + $this->assertEquals(2, $retrieved->totalJobs); + $this->assertEquals(0, $retrieved->failedJobs); + $this->assertTrue($retrieved->finishedAt->between(now()->subSecond(30), now())); + } + + public function test_retrieve_non_existent_batch() + { + /** @var DynamoBatchRepository */ + $repo = app(DynamoBatchRepository::class); + $retrieved = $repo->find(Str::orderedUuid()); + $this->assertNull($retrieved); + } + + public function test_delete_batch_by_id() + { + $batch = Bus::batch([ + new BatchJob('1'), + ])->dispatch(); + + /** @var DynamoBatchRepository */ + $repo = app(DynamoBatchRepository::class); + $retrieved = $repo->find($batch->id); + $this->assertNotNull($retrieved); + $repo->delete($retrieved->id); + $retrieved = $repo->find($batch->id); + $this->assertNull($retrieved); + } + + public function test_delete_non_existent_batch() + { + /** @var DynamoBatchRepository */ + $repo = app(DynamoBatchRepository::class); + $repo->delete(Str::orderedUuid()); + // Ensure we didn't throw an exception + $this->assertTrue(true); + } + + public function test_batch_with_failing_job() + { + $batch = Bus::batch([ + new BatchJob('1'), + new FailingJob('2'), + ])->dispatch(); + + /** @var DynamoBatchRepository */ + $repo = app(DynamoBatchRepository::class); + $retrieved = $repo->find($batch->id); + $this->assertEquals(2, $retrieved->totalJobs); + $this->assertEquals(1, $retrieved->failedJobs); + $this->assertTrue($retrieved->finishedAt->between(now()->subSecond(30), now())); + $this->assertTrue($retrieved->cancelledAt->between(now()->subSecond(30), now())); + } + + public function test_get_batches() + { + $batches = [ + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + Bus::batch([new BatchJob('1')])->dispatch(), + ]; + + /** @var DynamoBatchRepository */ + $repo = app(DynamoBatchRepository::class); + $this->assertCount(10, $repo->get()); + $this->assertCount(6, $repo->get(6)); + $this->assertCount(6, $repo->get(100, $batches[6]->id)); + $this->assertCount(0, $repo->get(100, $batches[0]->id)); + $this->assertCount(9, $repo->get(100, $batches[9]->id)); + $this->assertCount(10, $repo->get(100, Str::orderedUuid())); + } +} + +class BatchJob implements ShouldQueue +{ + use Batchable, Dispatchable, InteractsWithQueue, Queueable; + + public static $results = []; + + public string $id; + + public function __construct(string $id) + { + $this->id = $id; + } + + public function handle() + { + BatchRunRecorder::record($this->id); + } +} + +class FailingJob extends BatchJob +{ + public function handle() + { + BatchRunRecorder::recordFailure($this->id); + $this->fail(); + } +} + +class BatchRunRecorder +{ + public static $results = []; + + public static $failures = []; + + public static function record(string $id) + { + self::$results[] = $id; + } + + public static function recordFailure(string $message) + { + self::$failures[] = $message; + + return $message; + } + + public static function reset() + { + self::$results = []; + self::$failures = []; + } +} diff --git a/tests/Integration/Queue/DynamoBatchTestWithTTL.php b/tests/Integration/Queue/DynamoBatchTestWithTTL.php new file mode 100644 index 000000000000..4522b7f3b2fa --- /dev/null +++ b/tests/Integration/Queue/DynamoBatchTestWithTTL.php @@ -0,0 +1,19 @@ +set('queue.batching', [ + 'driver' => 'dynamodb', + 'region' => 'us-west-2', + 'endpoint' => static::DYNAMODB_ENDPOINT, + 'key' => 'key', + 'secret' => 'secret', + 'ttl' => 1, + 'ttlAttribute' => 'ttl_value', + ]); + } +} From 0d5072b9ebb4f1de2be73f28e548b7271c75733c Mon Sep 17 00:00:00 2001 From: Norman Huth Date: Sun, 10 Dec 2023 14:40:04 +0100 Subject: [PATCH 171/207] Removed deprecated and not used argument. (#49304) See: https://github.com/laravel/framework/blob/10.x/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php#L22 --- src/Illuminate/Foundation/Console/ModelMakeCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/ModelMakeCommand.php b/src/Illuminate/Foundation/Console/ModelMakeCommand.php index b4a44dda701f..f4b4bd66df9e 100644 --- a/src/Illuminate/Foundation/Console/ModelMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ModelMakeCommand.php @@ -110,7 +110,6 @@ protected function createMigration() $this->call('make:migration', [ 'name' => "create_{$table}_table", '--create' => $table, - '--fullpath' => true, ]); } From ceb8ed25e7f72f69d3df508765607b8825e046c5 Mon Sep 17 00:00:00 2001 From: Stephen Rees-Carter Date: Sun, 10 Dec 2023 23:41:06 +1000 Subject: [PATCH 172/207] [11.x] Rehash user passwords when validating credentials (#48665) * Rehash user passwords when validating credentials * Fix style violations * Remove hardcoded password when it's changable * Shift rehashing into SessionGuard The Session guard's attempt() method is a better place to apply rehashing than the validateCredentials() method on the provider. The latter shouldn't have side-effects, as per it's name. * Fix style violation * Add config option to disable rehashing on login * Clean up rehash flag injection * Fix contract in DatabaseUserProvider * Fixing return type in the docblocks * Use hash_equals() for a secure string comparison * formatting * formatting, leverage method on logoutOtherDevices * Fix spelling of passwords Co-authored-by: Chrysanthos <48060191+chrysanthos@users.noreply.github.com> --------- Co-authored-by: Taylor Otwell Co-authored-by: Chrysanthos <48060191+chrysanthos@users.noreply.github.com> --- src/Illuminate/Auth/AuthManager.php | 1 + src/Illuminate/Auth/Authenticatable.php | 19 +++++- src/Illuminate/Auth/DatabaseUserProvider.php | 19 ++++++ src/Illuminate/Auth/EloquentUserProvider.php | 19 ++++++ src/Illuminate/Auth/GenericUser.php | 10 ++++ src/Illuminate/Auth/SessionGuard.php | 50 ++++++++++++---- .../Contracts/Auth/Authenticatable.php | 7 +++ .../Contracts/Auth/UserProvider.php | 10 ++++ .../Middleware/AuthenticateSession.php | 4 +- tests/Auth/AuthDatabaseUserProviderTest.php | 58 +++++++++++++++++++ tests/Auth/AuthEloquentUserProviderTest.php | 44 ++++++++++++++ tests/Auth/AuthGuardTest.php | 43 ++++++++++++++ 12 files changed, 270 insertions(+), 14 deletions(-) diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index e95da5ec4ae4..4bba02800ad7 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -128,6 +128,7 @@ public function createSessionDriver($name, $config) $name, $provider, $this->app['session.store'], + rehashOnLogin: $this->app['config']->get('hashing.rehash_on_login', true), ); // When using the remember me functionality of the authentication services we diff --git a/src/Illuminate/Auth/Authenticatable.php b/src/Illuminate/Auth/Authenticatable.php index f1c0115916c8..0145c1cc8901 100644 --- a/src/Illuminate/Auth/Authenticatable.php +++ b/src/Illuminate/Auth/Authenticatable.php @@ -4,6 +4,13 @@ trait Authenticatable { + /** + * The column name of the password field using during authentication. + * + * @var string + */ + protected $authPasswordName = 'password'; + /** * The column name of the "remember me" token. * @@ -41,6 +48,16 @@ public function getAuthIdentifierForBroadcasting() return $this->getAuthIdentifier(); } + /** + * Get the name of the password attribute for the user. + * + * @return string + */ + public function getAuthPasswordName() + { + return $this->authPasswordName; + } + /** * Get the password for the user. * @@ -48,7 +65,7 @@ public function getAuthIdentifierForBroadcasting() */ public function getAuthPassword() { - return $this->password; + return $this->{$this->getAuthPasswordName()}; } /** diff --git a/src/Illuminate/Auth/DatabaseUserProvider.php b/src/Illuminate/Auth/DatabaseUserProvider.php index 16b70ee9c76a..1be060cb831a 100755 --- a/src/Illuminate/Auth/DatabaseUserProvider.php +++ b/src/Illuminate/Auth/DatabaseUserProvider.php @@ -158,4 +158,23 @@ public function validateCredentials(UserContract $user, array $credentials) $credentials['password'], $user->getAuthPassword() ); } + + /** + * Rehash the user's password if required and supported. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param array $credentials + * @param bool $force + * @return void + */ + public function rehashPasswordIfRequired(UserContract $user, array $credentials, bool $force = false) + { + if (! $this->hasher->needsRehash($user->getAuthPassword()) && ! $force) { + return; + } + + $this->connection->table($this->table) + ->where($user->getAuthIdentifierName(), $user->getAuthIdentifier()) + ->update([$user->getAuthPasswordName() => $this->hasher->make($credentials['password'])]); + } } diff --git a/src/Illuminate/Auth/EloquentUserProvider.php b/src/Illuminate/Auth/EloquentUserProvider.php index 39a744e0c098..646c2187f595 100755 --- a/src/Illuminate/Auth/EloquentUserProvider.php +++ b/src/Illuminate/Auth/EloquentUserProvider.php @@ -155,6 +155,25 @@ public function validateCredentials(UserContract $user, array $credentials) return $this->hasher->check($plain, $user->getAuthPassword()); } + /** + * Rehash the user's password if required and supported. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param array $credentials + * @param bool $force + * @return void + */ + public function rehashPasswordIfRequired(UserContract $user, array $credentials, bool $force = false) + { + if (! $this->hasher->needsRehash($user->getAuthPassword()) && ! $force) { + return; + } + + $user->forceFill([ + $user->getAuthPasswordName() => $this->hasher->make($credentials['password']), + ])->save(); + } + /** * Get a new query builder for the model instance. * diff --git a/src/Illuminate/Auth/GenericUser.php b/src/Illuminate/Auth/GenericUser.php index c87bc2382cb2..57bab7c8435d 100755 --- a/src/Illuminate/Auth/GenericUser.php +++ b/src/Illuminate/Auth/GenericUser.php @@ -44,6 +44,16 @@ public function getAuthIdentifier() return $this->attributes[$this->getAuthIdentifierName()]; } + /** + * Get the name of the password attribute for the user. + * + * @return string + */ + public function getAuthPasswordName() + { + return 'password'; + } + /** * Get the password for the user. * diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index b475dbc6ca2e..7966eaa7da12 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -96,6 +96,13 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth */ protected $timebox; + /** + * Indicates if passwords should be rehashed on login if needed. + * + * @var bool + */ + protected $rehashOnLogin; + /** * Indicates if the logout method has been called. * @@ -118,19 +125,22 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth * @param \Illuminate\Contracts\Session\Session $session * @param \Symfony\Component\HttpFoundation\Request|null $request * @param \Illuminate\Support\Timebox|null $timebox + * @param bool $rehashOnLogin * @return void */ public function __construct($name, UserProvider $provider, Session $session, Request $request = null, - Timebox $timebox = null) + Timebox $timebox = null, + bool $rehashOnLogin = true) { $this->name = $name; $this->session = $session; $this->request = $request; $this->provider = $provider; $this->timebox = $timebox ?: new Timebox; + $this->rehashOnLogin = $rehashOnLogin; } /** @@ -384,6 +394,8 @@ public function attempt(array $credentials = [], $remember = false) // to validate the user against the given credentials, and if they are in // fact valid we'll log the users into the application and return true. if ($this->hasValidCredentials($user, $credentials)) { + $this->rehashPasswordIfRequired($user, $credentials); + $this->login($user, $remember); return true; @@ -415,6 +427,8 @@ public function attemptWhen(array $credentials = [], $callbacks = null, $remembe // the user is retrieved and validated. If one of the callbacks returns falsy we do // not login the user. Instead, we will fail the specific authentication attempt. if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) { + $this->rehashPasswordIfRequired($user, $credentials); + $this->login($user, $remember); return true; @@ -465,6 +479,20 @@ protected function shouldLogin($callbacks, AuthenticatableContract $user) return true; } + /** + * Rehash the user's password if enabled and required. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param array $credentials + * @return void + */ + protected function rehashPasswordIfRequired(AuthenticatableContract $user, array $credentials) + { + if ($this->rehashOnLogin) { + $this->provider->rehashPasswordIfRequired($user, $credentials); + } + } + /** * Log the given user ID into the application. * @@ -656,18 +684,17 @@ protected function cycleRememberToken(AuthenticatableContract $user) * The application must be using the AuthenticateSession middleware. * * @param string $password - * @param string $attribute * @return \Illuminate\Contracts\Auth\Authenticatable|null * * @throws \Illuminate\Auth\AuthenticationException */ - public function logoutOtherDevices($password, $attribute = 'password') + public function logoutOtherDevices($password) { if (! $this->user()) { return; } - $result = $this->rehashUserPassword($password, $attribute); + $result = $this->rehashUserPasswordForDeviceLogout($password); if ($this->recaller() || $this->getCookieJar()->hasQueued($this->getRecallerName())) { @@ -680,23 +707,24 @@ public function logoutOtherDevices($password, $attribute = 'password') } /** - * Rehash the current user's password. + * Rehash the current user's password for logging out other devices via AuthenticateSession. * * @param string $password - * @param string $attribute * @return \Illuminate\Contracts\Auth\Authenticatable|null * * @throws \InvalidArgumentException */ - protected function rehashUserPassword($password, $attribute) + protected function rehashUserPasswordForDeviceLogout($password) { - if (! Hash::check($password, $this->user()->{$attribute})) { + $user = $this->user(); + + if (! Hash::check($password, $user->getAuthPassword())) { throw new InvalidArgumentException('The given password does not match the current password.'); } - return tap($this->user()->forceFill([ - $attribute => Hash::make($password), - ]))->save(); + $this->provider->rehashPasswordIfRequired( + $user, ['password' => $password], force: true + ); } /** diff --git a/src/Illuminate/Contracts/Auth/Authenticatable.php b/src/Illuminate/Contracts/Auth/Authenticatable.php index ac4ed886d1b7..f5ed4987b44b 100644 --- a/src/Illuminate/Contracts/Auth/Authenticatable.php +++ b/src/Illuminate/Contracts/Auth/Authenticatable.php @@ -18,6 +18,13 @@ public function getAuthIdentifierName(); */ public function getAuthIdentifier(); + /** + * Get the name of the password attribute for the user. + * + * @return string + */ + public function getAuthPasswordName(); + /** * Get the password for the user. * diff --git a/src/Illuminate/Contracts/Auth/UserProvider.php b/src/Illuminate/Contracts/Auth/UserProvider.php index a2ab122718c6..4ed51bf00e9c 100644 --- a/src/Illuminate/Contracts/Auth/UserProvider.php +++ b/src/Illuminate/Contracts/Auth/UserProvider.php @@ -46,4 +46,14 @@ public function retrieveByCredentials(array $credentials); * @return bool */ public function validateCredentials(Authenticatable $user, array $credentials); + + /** + * Rehash the user's password if required and supported. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param array $credentials + * @param bool $force + * @return void + */ + public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false); } diff --git a/src/Illuminate/Session/Middleware/AuthenticateSession.php b/src/Illuminate/Session/Middleware/AuthenticateSession.php index d525e8f535e1..b32e3ba50283 100644 --- a/src/Illuminate/Session/Middleware/AuthenticateSession.php +++ b/src/Illuminate/Session/Middleware/AuthenticateSession.php @@ -51,7 +51,7 @@ public function handle($request, Closure $next) if ($this->guard()->viaRemember()) { $passwordHash = explode('|', $request->cookies->get($this->guard()->getRecallerName()))[2] ?? null; - if (! $passwordHash || $passwordHash != $request->user()->getAuthPassword()) { + if (! $passwordHash || ! hash_equals($request->user()->getAuthPassword(), $passwordHash)) { $this->logout($request); } } @@ -60,7 +60,7 @@ public function handle($request, Closure $next) $this->storePasswordHashInSession($request); } - if ($request->session()->get('password_hash_'.$this->auth->getDefaultDriver()) !== $request->user()->getAuthPassword()) { + if (! hash_equals($request->session()->get('password_hash_'.$this->auth->getDefaultDriver()), $request->user()->getAuthPassword())) { $this->logout($request); } diff --git a/tests/Auth/AuthDatabaseUserProviderTest.php b/tests/Auth/AuthDatabaseUserProviderTest.php index 382d3532a976..ad317be84253 100755 --- a/tests/Auth/AuthDatabaseUserProviderTest.php +++ b/tests/Auth/AuthDatabaseUserProviderTest.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\Connection; +use Illuminate\Database\ConnectionInterface; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -160,4 +161,61 @@ public function testCredentialValidation() $this->assertTrue($result); } + + public function testCredentialValidationFailed() + { + $conn = m::mock(Connection::class); + $hasher = m::mock(Hasher::class); + $hasher->shouldReceive('check')->once()->with('plain', 'hash')->andReturn(false); + $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $result = $provider->validateCredentials($user, ['password' => 'plain']); + + $this->assertFalse($result); + } + + public function testRehashPasswordIfRequired() + { + $hasher = m::mock(Hasher::class); + $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(true); + $hasher->shouldReceive('make')->once()->with('plain')->andReturn('rehashed'); + + $conn = m::mock(Connection::class); + $table = m::mock(ConnectionInterface::class); + $conn->shouldReceive('table')->once()->with('foo')->andReturn($table); + $table->shouldReceive('where')->once()->with('id', 1)->andReturnSelf(); + $table->shouldReceive('update')->once()->with(['password_attribute' => 'rehashed']); + + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthIdentifierName')->once()->andReturn('id'); + $user->shouldReceive('getAuthIdentifier')->once()->andReturn(1); + $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->shouldReceive('getAuthPasswordName')->once()->andReturn('password_attribute'); + + $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); + $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + } + + public function testDontRehashPasswordIfNotRequired() + { + $hasher = m::mock(Hasher::class); + $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(false); + $hasher->shouldNotReceive('make'); + + $conn = m::mock(Connection::class); + $table = m::mock(ConnectionInterface::class); + $conn->shouldNotReceive('table'); + $table->shouldNotReceive('where'); + $table->shouldNotReceive('update'); + + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->shouldNotReceive('getAuthIdentifierName'); + $user->shouldNotReceive('getAuthIdentifier'); + $user->shouldNotReceive('getAuthPasswordName'); + + $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); + $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + } } diff --git a/tests/Auth/AuthEloquentUserProviderTest.php b/tests/Auth/AuthEloquentUserProviderTest.php index 9ef0e29636b0..21d181b97ad0 100755 --- a/tests/Auth/AuthEloquentUserProviderTest.php +++ b/tests/Auth/AuthEloquentUserProviderTest.php @@ -140,6 +140,50 @@ public function testCredentialValidation() $this->assertTrue($result); } + public function testCredentialValidationFailed() + { + $hasher = m::mock(Hasher::class); + $hasher->shouldReceive('check')->once()->with('plain', 'hash')->andReturn(false); + $provider = new EloquentUserProvider($hasher, 'foo'); + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $result = $provider->validateCredentials($user, ['password' => 'plain']); + + $this->assertFalse($result); + } + + public function testRehashPasswordIfRequired() + { + $hasher = m::mock(Hasher::class); + $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(true); + $hasher->shouldReceive('make')->once()->with('plain')->andReturn('rehashed'); + + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->shouldReceive('getAuthPasswordName')->once()->andReturn('password_attribute'); + $user->shouldReceive('forceFill')->once()->with(['password_attribute' => 'rehashed'])->andReturnSelf(); + $user->shouldReceive('save')->once(); + + $provider = new EloquentUserProvider($hasher, 'foo'); + $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + } + + public function testDontRehashPasswordIfNotRequired() + { + $hasher = m::mock(Hasher::class); + $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(false); + $hasher->shouldNotReceive('make'); + + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->shouldNotReceive('getAuthPasswordName'); + $user->shouldNotReceive('forceFill'); + $user->shouldNotReceive('save'); + + $provider = new EloquentUserProvider($hasher, 'foo'); + $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + } + public function testModelsCanBeCreated() { $hasher = m::mock(Hasher::class); diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php index 52c4cfe7d1c8..e75efd642c37 100755 --- a/tests/Auth/AuthGuardTest.php +++ b/tests/Auth/AuthGuardTest.php @@ -103,6 +103,7 @@ public function testAttemptCallsRetrieveByCredentials() $events->shouldReceive('dispatch')->once()->with(m::type(Failed::class)); $events->shouldNotReceive('dispatch')->with(m::type(Validated::class)); $guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->with(['foo']); + $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $guard->attempt(['foo']); } @@ -119,6 +120,7 @@ public function testAttemptReturnsUserInterface() $user = $this->createMock(Authenticatable::class); $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)); $this->assertTrue($guard->attempt(['foo'])); } @@ -135,6 +137,7 @@ public function testAttemptReturnsFalseIfUserNotGiven() $events->shouldReceive('dispatch')->once()->with(m::type(Failed::class)); $events->shouldNotReceive('dispatch')->with(m::type(Validated::class)); $mock->getProvider()->shouldReceive('retrieveByCredentials')->once()->andReturn(null); + $mock->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $this->assertFalse($mock->attempt(['foo'])); } @@ -159,6 +162,7 @@ public function testAttemptAndWithCallbacks() $mock->getProvider()->shouldReceive('retrieveByCredentials')->times(3)->with(['foo'])->andReturn($user); $mock->getProvider()->shouldReceive('validateCredentials')->twice()->andReturnTrue(); $mock->getProvider()->shouldReceive('validateCredentials')->once()->andReturnFalse(); + $mock->getProvider()->shouldReceive('rehashPasswordIfRequired')->with($user, ['foo'])->once(); $this->assertTrue($mock->attemptWhen(['foo'], function ($user, $guard) { static::assertInstanceOf(Authenticatable::class, $user); @@ -183,6 +187,44 @@ public function testAttemptAndWithCallbacks() $this->assertFalse($executed); } + public function testAttemptRehashesPasswordWhenRequired() + { + [$session, $provider, $request, $cookie, $timebox] = $this->getMocks(); + $guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login'])->setConstructorArgs(['default', $provider, $session, $request, $timebox])->getMock(); + $guard->setDispatcher($events = m::mock(Dispatcher::class)); + $timebox->shouldReceive('call')->once()->andReturnUsing(function ($callback, $microseconds) use ($timebox) { + return $callback($timebox->shouldReceive('returnEarly')->once()->getMock()); + }); + $events->shouldReceive('dispatch')->once()->with(m::type(Attempting::class)); + $events->shouldReceive('dispatch')->once()->with(m::type(Validated::class)); + $user = $this->createMock(Authenticatable::class); + $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)); + $this->assertTrue($guard->attempt(['foo'])); + } + + public function testAttemptDoesntRehashPasswordWhenDisabled() + { + [$session, $provider, $request, $cookie, $timebox] = $this->getMocks(); + $guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login']) + ->setConstructorArgs(['default', $provider, $session, $request, $timebox, $rehashOnLogin = false]) + ->getMock(); + $guard->setDispatcher($events = m::mock(Dispatcher::class)); + $timebox->shouldReceive('call')->once()->andReturnUsing(function ($callback, $microseconds) use ($timebox) { + return $callback($timebox->shouldReceive('returnEarly')->once()->getMock()); + }); + $events->shouldReceive('dispatch')->once()->with(m::type(Attempting::class)); + $events->shouldReceive('dispatch')->once()->with(m::type(Validated::class)); + $user = $this->createMock(Authenticatable::class); + $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)); + $this->assertTrue($guard->attempt(['foo'])); + } + public function testLoginStoresIdentifierInSession() { [$session, $provider, $request, $cookie] = $this->getMocks(); @@ -235,6 +277,7 @@ public function testFailedAttemptFiresFailedEvent() $events->shouldReceive('dispatch')->once()->with(m::type(Failed::class)); $events->shouldNotReceive('dispatch')->with(m::type(Validated::class)); $guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn(null); + $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $guard->attempt(['foo']); } From ae9e287bf7e4387c7e1dd0e5a34eaf03a5ddb351 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Sun, 10 Dec 2023 13:41:37 +0000 Subject: [PATCH 173/207] Update facade docblocks --- src/Illuminate/Support/Facades/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Auth.php b/src/Illuminate/Support/Facades/Auth.php index a93e3244ae44..32b2eb64aa95 100755 --- a/src/Illuminate/Support/Facades/Auth.php +++ b/src/Illuminate/Support/Facades/Auth.php @@ -40,7 +40,7 @@ * @method static \Symfony\Component\HttpFoundation\Response|null onceBasic(string $field = 'email', array $extraConditions = []) * @method static bool attemptWhen(array $credentials = [], array|callable|null $callbacks = null, bool $remember = false) * @method static void logoutCurrentDevice() - * @method static \Illuminate\Contracts\Auth\Authenticatable|null logoutOtherDevices(string $password, string $attribute = 'password') + * @method static \Illuminate\Contracts\Auth\Authenticatable|null logoutOtherDevices(string $password) * @method static void attempting(mixed $callback) * @method static \Illuminate\Contracts\Auth\Authenticatable getLastAttempted() * @method static string getName() From 21a83730d24b2fba1ba7f5e04cea301efc33f848 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 10 Dec 2023 07:45:34 -0600 Subject: [PATCH 174/207] add on rehash on login --- config/hashing.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/config/hashing.php b/config/hashing.php index 2925f725b487..f34179a8f19a 100644 --- a/config/hashing.php +++ b/config/hashing.php @@ -51,4 +51,17 @@ 'verify' => true, ], + /* + |-------------------------------------------------------------------------- + | Rehash On Login + |-------------------------------------------------------------------------- + | + | Setting this option to true will tell Laravel to automatically rehash + | the user's password during login if the configured work factor for + | the algorithm has changed, allowing graceful upgrades of hashes. + | + */ + + 'rehash_on_login' => true, + ]; From 4fe214d65a92fbef8e93213437800014db01bd49 Mon Sep 17 00:00:00 2001 From: Brett B Date: Mon, 11 Dec 2023 12:35:54 +1000 Subject: [PATCH 175/207] Add Conditionable to Batched and Chained jobs (#49310) * Add Conditionable to Batched and Chained jobs * styleci * styleci --------- Co-authored-by: Brett Bailey --- src/Illuminate/Bus/PendingBatch.php | 3 ++ .../Foundation/Bus/PendingChain.php | 3 ++ tests/Integration/Queue/JobChainingTest.php | 39 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index 6cd518311d21..463cebd9cf6b 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -8,11 +8,14 @@ use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Traits\Conditionable; use Laravel\SerializableClosure\SerializableClosure; use Throwable; class PendingBatch { + use Conditionable; + /** * The IoC container instance. * diff --git a/src/Illuminate/Foundation/Bus/PendingChain.php b/src/Illuminate/Foundation/Bus/PendingChain.php index b56b773ea9b8..8d3c6892615a 100644 --- a/src/Illuminate/Foundation/Bus/PendingChain.php +++ b/src/Illuminate/Foundation/Bus/PendingChain.php @@ -5,10 +5,13 @@ use Closure; use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Queue\CallQueuedClosure; +use Illuminate\Support\Traits\Conditionable; use Laravel\SerializableClosure\SerializableClosure; class PendingChain { + use Conditionable; + /** * The class name of the job being dispatched. * diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 9dec15912fbd..3fa00618baa7 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -3,9 +3,11 @@ namespace Illuminate\Tests\Integration\Queue; use Illuminate\Bus\Batchable; +use Illuminate\Bus\PendingBatch; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Bus\PendingChain; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Carbon; @@ -387,6 +389,43 @@ public function testChainBatchFailureNotAllowed() $this->assertEquals(['c1', 'c2', 'b1', 'b3'], JobRunRecorder::$results); $this->assertEquals(['batch failed', 'chain failed'], JobRunRecorder::$failures); } + + public function testChainConditionable() + { + $chain = Bus::chain([]) + ->onConnection('sync1') + ->when(true, function (PendingChain $chain) { + $chain->onConnection('sync2'); + }); + + $this->assertEquals('sync2', $chain->connection); + + $chain = Bus::chain([]) + ->onConnection('sync1') + ->when(false, function (PendingChain $chain) { + $chain->onConnection('sync2'); + }); + + $this->assertEquals('sync1', $chain->connection); + } + + public function testBatchConditionable() + { + $batch = Bus::batch([]) + ->onConnection('sync1') + ->when(true, function (PendingBatch $batch) { + $batch->onConnection('sync2'); + }); + + $this->assertEquals('sync2', $batch->connection()); + $batch = Bus::batch([]) + ->onConnection('sync1') + ->when(false, function (PendingBatch $batch) { + $batch->onConnection('sync2'); + }); + + $this->assertEquals('sync1', $batch->connection()); + } } class JobChainingTestFirstJob implements ShouldQueue From cc5d95cf0458767558d694c960f8dc84650e0384 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 11 Dec 2023 19:43:49 +0330 Subject: [PATCH 176/207] [10.x] Include partitioned tables on PostgreSQL when retrieving tables (#49326) * wip * include partitioned tables on getTables query * fix tests * fix tests * fix getTables on legacy sqlite version * fix tests --- .../Schema/Grammars/PostgresGrammar.php | 4 ++-- .../Database/Schema/SQLiteBuilder.php | 2 +- .../Postgres/PostgresSchemaBuilderTest.php | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 2995e339d0d5..9e18f2d9a773 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -87,7 +87,7 @@ public function compileTables() { return 'select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, ' ."obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " - ."where c.relkind = 'r' and n.oid = c.relnamespace " + ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema')" .'order by c.relname'; } @@ -98,7 +98,7 @@ public function compileTables() */ public function compileViews() { - return 'select viewname as name, schemaname as schema, definition from pg_views order by viewname'; + return "select viewname as name, schemaname as schema, definition from pg_views where schemaname not in ('pg_catalog', 'information_schema') order by viewname"; } /** diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 9e7960ba3a65..e2cae2c07c9d 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -37,7 +37,7 @@ public function dropDatabaseIfExists($name) */ public function getTables() { - $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); + $withSize = rescue(fn () => $this->connection->scalar($this->grammar->compileDbstatExists()), false, false); return $this->connection->getPostProcessor()->processTables( $this->connection->selectFromWriteConnection($this->grammar->compileTables($withSize)) diff --git a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php index 99240f81b665..6f7f042fa405 100644 --- a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php +++ b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php @@ -165,6 +165,29 @@ public function testGetViews() })); } + public function testDropPartitionedTables() + { + DB::statement('create table groups (id bigserial, tenant_id bigint, name varchar, primary key (id, tenant_id)) partition by hash (tenant_id)'); + DB::statement('create table groups_1 partition of groups for values with (modulus 2, remainder 0)'); + DB::statement('create table groups_2 partition of groups for values with (modulus 2, remainder 1)'); + + $tables = array_column(Schema::getTables(), 'name'); + + $this->assertContains('groups', $tables); + $this->assertContains('groups_1', $tables); + $this->assertContains('groups_2', $tables); + + Schema::dropAllTables(); + + $this->artisan('migrate:install'); + + $tables = array_column(Schema::getTables(), 'name'); + + $this->assertNotContains('groups', $tables); + $this->assertNotContains('groups_1', $tables); + $this->assertNotContains('groups_2', $tables); + } + protected function hasView($schema, $table) { return DB::table('information_schema.views') From aeb284959f15f8a5eb79eef5b29734bfd7c1ccbc Mon Sep 17 00:00:00 2001 From: Michael Nabil <46572405+michaelnabil230@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:48:56 +0200 Subject: [PATCH 177/207] [10.x] Allow to pass `Arrayable` or `Stringble` in rules `In` and `NotIn` (#49055) * Allow to pass `Arrayable` or `Stringble` in rules `In` and `NotIn` * Add tests --- src/Illuminate/Validation/Rules/In.php | 11 ++++++--- src/Illuminate/Validation/Rules/NotIn.php | 11 ++++++--- tests/Validation/ValidationInRuleTest.php | 16 +++++++++++++ tests/Validation/ValidationNotInRuleTest.php | 25 ++++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Validation/Rules/In.php b/src/Illuminate/Validation/Rules/In.php index 18c70bdf59be..a140fd399455 100644 --- a/src/Illuminate/Validation/Rules/In.php +++ b/src/Illuminate/Validation/Rules/In.php @@ -3,6 +3,7 @@ namespace Illuminate\Validation\Rules; use BackedEnum; +use Illuminate\Contracts\Support\Arrayable; use UnitEnum; class In @@ -24,12 +25,16 @@ class In /** * Create a new in rule instance. * - * @param array $values + * @param \Illuminate\Contracts\Support\Arrayable|array|string $values * @return void */ - public function __construct(array $values) + public function __construct($values) { - $this->values = $values; + if ($values instanceof Arrayable) { + $values = $values->toArray(); + } + + $this->values = is_array($values) ? $values : func_get_args(); } /** diff --git a/src/Illuminate/Validation/Rules/NotIn.php b/src/Illuminate/Validation/Rules/NotIn.php index 5768bfa0007d..f73648a3073d 100644 --- a/src/Illuminate/Validation/Rules/NotIn.php +++ b/src/Illuminate/Validation/Rules/NotIn.php @@ -3,6 +3,7 @@ namespace Illuminate\Validation\Rules; use BackedEnum; +use Illuminate\Contracts\Support\Arrayable; use UnitEnum; class NotIn @@ -24,12 +25,16 @@ class NotIn /** * Create a new "not in" rule instance. * - * @param array $values + * @param \Illuminate\Contracts\Support\Arrayable|array|string $values * @return void */ - public function __construct(array $values) + public function __construct($values) { - $this->values = $values; + if ($values instanceof Arrayable) { + $values = $values->toArray(); + } + + $this->values = is_array($values) ? $values : func_get_args(); } /** diff --git a/tests/Validation/ValidationInRuleTest.php b/tests/Validation/ValidationInRuleTest.php index 1bf213dbe367..25814076ea4f 100644 --- a/tests/Validation/ValidationInRuleTest.php +++ b/tests/Validation/ValidationInRuleTest.php @@ -17,10 +17,22 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('in:"Laravel","Framework","PHP"', (string) $rule); + $rule = new In(collect(['Taylor', 'Michael', 'Tim'])); + + $this->assertSame('in:"Taylor","Michael","Tim"', (string) $rule); + $rule = new In(['Life, the Universe and Everything', 'this is a "quote"']); $this->assertSame('in:"Life, the Universe and Everything","this is a ""quote"""', (string) $rule); + $rule = Rule::in(collect([1, 2, 3, 4])); + + $this->assertSame('in:"1","2","3","4"', (string) $rule); + + $rule = Rule::in(collect([1, 2, 3, 4])); + + $this->assertSame('in:"1","2","3","4"', (string) $rule); + $rule = new In(["a,b\nc,d"]); $this->assertSame("in:\"a,b\nc,d\"", (string) $rule); @@ -41,6 +53,10 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('in:"1","2","3","4"', (string) $rule); + $rule = new In('1', '2', '3', '4'); + + $this->assertSame('in:"1","2","3","4"', (string) $rule); + $rule = Rule::in([StringStatus::done]); $this->assertSame('in:"done"', (string) $rule); diff --git a/tests/Validation/ValidationNotInRuleTest.php b/tests/Validation/ValidationNotInRuleTest.php index c33778ff3214..ba00d81a3a51 100644 --- a/tests/Validation/ValidationNotInRuleTest.php +++ b/tests/Validation/ValidationNotInRuleTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Validation; +use Illuminate\Tests\Validation\fixtures\Values; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\NotIn; use PHPUnit\Framework\TestCase; @@ -16,6 +17,18 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('not_in:"Laravel","Framework","PHP"', (string) $rule); + $rule = new NotIn(collect(['Taylor', 'Michael', 'Tim'])); + + $this->assertSame('not_in:"Taylor","Michael","Tim"', (string) $rule); + + $rule = Rule::notIn(collect([1, 2, 3, 4])); + + $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + + $rule = Rule::notIn(collect([1, 2, 3, 4])); + + $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + $rule = Rule::notIn([1, 2, 3, 4]); $this->assertSame('not_in:"1","2","3","4"', (string) $rule); @@ -24,10 +37,22 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + $rule = Rule::notIn(new Values); + + $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + + $rule = new NotIn(new Values); + + $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + $rule = Rule::notIn('1', '2', '3', '4'); $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + $rule = new NotIn('1', '2', '3', '4'); + + $this->assertSame('not_in:"1","2","3","4"', (string) $rule); + $rule = Rule::notIn([StringStatus::done]); $this->assertSame('not_in:"done"', (string) $rule); From a111adaff3d76376f3ff9573554bbc0944416478 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 11 Dec 2023 16:50:20 +0000 Subject: [PATCH 178/207] Adds `update_date_on_publish` to configuration file (#49330) --- config/database.php | 5 ++- .../Database/Console/DumpCommand.php | 6 ++- .../Database/MigrationServiceProvider.php | 4 +- .../Foundation/Testing/DatabaseTruncation.php | 8 ++-- src/Illuminate/Support/ServiceProvider.php | 4 +- tests/Support/SupportServiceProviderTest.php | 43 ++++++++++++++++++- 6 files changed, 62 insertions(+), 8 deletions(-) diff --git a/config/database.php b/config/database.php index ff5880bd2e8e..f030206fc946 100644 --- a/config/database.php +++ b/config/database.php @@ -121,7 +121,10 @@ | */ - 'migrations' => 'migrations', + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], /* |-------------------------------------------------------------------------- diff --git a/src/Illuminate/Database/Console/DumpCommand.php b/src/Illuminate/Database/Console/DumpCommand.php index 27121281a3fa..49d80257aa10 100644 --- a/src/Illuminate/Database/Console/DumpCommand.php +++ b/src/Illuminate/Database/Console/DumpCommand.php @@ -69,8 +69,12 @@ public function handle(ConnectionResolverInterface $connections, Dispatcher $dis */ protected function schemaState(Connection $connection) { + $migrations = Config::get('database.migrations', 'migrations'); + + $migrationTable = is_array($migrations) ? ($migrations['table'] ?? 'migrations') : $migrations; + return $connection->getSchemaState() - ->withMigrationTable($connection->getTablePrefix().Config::get('database.migrations', 'migrations')) + ->withMigrationTable($connection->getTablePrefix().$migrationTable) ->handleOutputUsing(function ($type, $buffer) { $this->output->write($buffer); }); diff --git a/src/Illuminate/Database/MigrationServiceProvider.php b/src/Illuminate/Database/MigrationServiceProvider.php index d61f4423240c..4dad13838cb3 100755 --- a/src/Illuminate/Database/MigrationServiceProvider.php +++ b/src/Illuminate/Database/MigrationServiceProvider.php @@ -59,7 +59,9 @@ public function register() protected function registerRepository() { $this->app->singleton('migration.repository', function ($app) { - $table = $app['config']['database.migrations']; + $migrations = $app['config']['database.migrations']; + + $table = is_array($migrations) ? ($migrations['table'] ?? null) : $migrations; return new DatabaseMigrationRepository($app['db'], $table); }); diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index cea2d8d09250..6eb86d9b47a7 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -130,9 +130,11 @@ protected function connectionsToTruncate(): array */ protected function exceptTables(?string $connectionName): array { - if (property_exists($this, 'exceptTables')) { - $migrationsTable = $this->app['config']->get('database.migrations'); + $migrations = $this->app['config']->get('database.migrations'); + + $migrationsTable = is_array($migrations) ? ($migrations['table'] ?? null) : $migrations; + if (property_exists($this, 'exceptTables')) { if (array_is_list($this->exceptTables ?? [])) { return array_merge( $this->exceptTables ?? [], @@ -146,7 +148,7 @@ protected function exceptTables(?string $connectionName): array ); } - return [$this->app['config']->get('database.migrations')]; + return [$migrationsTable]; } /** diff --git a/src/Illuminate/Support/ServiceProvider.php b/src/Illuminate/Support/ServiceProvider.php index fd632e8c7ed3..561eb740902e 100755 --- a/src/Illuminate/Support/ServiceProvider.php +++ b/src/Illuminate/Support/ServiceProvider.php @@ -285,7 +285,9 @@ protected function publishesMigrations(array $paths, $groups = null) { $this->publishes($paths, $groups); - static::$publishableMigrationPaths = array_unique(array_merge(static::$publishableMigrationPaths, array_keys($paths))); + if ($this->app->config->get('database.migrations.update_date_on_publish', false)) { + static::$publishableMigrationPaths = array_unique(array_merge(static::$publishableMigrationPaths, array_keys($paths))); + } } /** diff --git a/tests/Support/SupportServiceProviderTest.php b/tests/Support/SupportServiceProviderTest.php index 4861a9e97291..2cf1cf17b63f 100644 --- a/tests/Support/SupportServiceProviderTest.php +++ b/tests/Support/SupportServiceProviderTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Support; +use Illuminate\Config\Repository as Config; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; use Mockery as m; @@ -9,12 +10,21 @@ class SupportServiceProviderTest extends TestCase { + protected $app; + protected function setUp(): void { ServiceProvider::$publishes = []; ServiceProvider::$publishGroups = []; - $app = m::mock(Application::class)->makePartial(); + $this->app = $app = m::mock(Application::class)->makePartial(); + $config = m::mock(Config::class)->makePartial(); + + $config = new Config(); + + $app->instance('config', $config); + $config->set('database.migrations.update_date_on_publish', true); + $one = new ServiceProviderForTestingOne($app); $one->boot(); $two = new ServiceProviderForTestingTwo($app); @@ -119,6 +129,37 @@ public function testMultipleTaggedAssetsAreMergedCorrectly() ]; $this->assertEquals($expected, $toPublish, 'Service provider does not return expected set of published tagged paths.'); } + + public function testPublishesMigrations() + { + $serviceProvider = new ServiceProviderForTestingOne($this->app); + + (fn () => $this->publishesMigrations(['source/tagged/four' => 'destination/tagged/four'], 'tag_four')) + ->call($serviceProvider); + + $this->assertContains('source/tagged/four', ServiceProvider::publishableMigrationPaths()); + + $this->app->config->set('database.migrations.update_date_on_publish', false); + + (fn () => $this->publishesMigrations(['source/tagged/five' => 'destination/tagged/five'], 'tag_four')) + ->call($serviceProvider); + + $this->assertNotContains('source/tagged/five', ServiceProvider::publishableMigrationPaths()); + + $this->app->config->set('database.migrations', 'migrations'); + + (fn () => $this->publishesMigrations(['source/tagged/five' => 'destination/tagged/five'], 'tag_four')) + ->call($serviceProvider); + + $this->assertNotContains('source/tagged/five', ServiceProvider::publishableMigrationPaths()); + + $this->app->config->set('database.migrations', null); + + (fn () => $this->publishesMigrations(['source/tagged/five' => 'destination/tagged/five'], 'tag_four')) + ->call($serviceProvider); + + $this->assertNotContains('source/tagged/five', ServiceProvider::publishableMigrationPaths()); + } } class ServiceProviderForTestingOne extends ServiceProvider From 062450da9f7821f900fad8204a73cb5f0857f7ea Mon Sep 17 00:00:00 2001 From: Aimeos Date: Mon, 11 Dec 2023 17:51:38 +0100 Subject: [PATCH 179/207] [10.x] Display error message if json_encode() fails (#48856) * Display error message if json_encode() fails Adding the error code to the exception isn't useful. Instead, the error message can give details about the problem. * Added error code as suggested * Fixed coding style issue --- src/Illuminate/Queue/Queue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Queue.php b/src/Illuminate/Queue/Queue.php index 0ce7ad1ac1ce..4f36b80db75b 100755 --- a/src/Illuminate/Queue/Queue.php +++ b/src/Illuminate/Queue/Queue.php @@ -107,7 +107,7 @@ protected function createPayload($job, $queue, $data = '') if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidPayloadException( - 'Unable to JSON encode payload. Error code: '.json_last_error(), $value + 'Unable to JSON encode payload. Error ('.json_last_error().'): '.json_last_error_msg(), $value ); } From 2039e83fc432e2f92642ce55547681015129d0c8 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 12 Dec 2023 06:22:43 +1100 Subject: [PATCH 180/207] [10.x] Allow error list per field (#49309) * Allow error list per field * Fix bug with existing assertion --- src/Illuminate/Testing/TestResponse.php | 28 ++++++++------- tests/Testing/TestResponseTest.php | 48 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 47e4cab56e47..c78cecb64c38 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -1205,26 +1205,28 @@ public function assertInvalid($errors = null, foreach (Arr::wrap($errors) as $key => $value) { PHPUnit::assertArrayHasKey( - (is_int($key)) ? $value : $key, + $resolvedKey = (is_int($key)) ? $value : $key, $sessionErrors, - "Failed to find a validation error in session for key: '{$value}'".PHP_EOL.PHP_EOL.$errorMessage + "Failed to find a validation error in session for key: '{$resolvedKey}'".PHP_EOL.PHP_EOL.$errorMessage ); - if (! is_int($key)) { - $hasError = false; + foreach (Arr::wrap($value) as $message) { + if (! is_int($key)) { + $hasError = false; - foreach (Arr::wrap($sessionErrors[$key]) as $sessionErrorMessage) { - if (Str::contains($sessionErrorMessage, $value)) { - $hasError = true; + foreach (Arr::wrap($sessionErrors[$key]) as $sessionErrorMessage) { + if (Str::contains($sessionErrorMessage, $message)) { + $hasError = true; - break; + break; + } } - } - if (! $hasError) { - PHPUnit::fail( - "Failed to find a validation error for key and message: '$key' => '$value'".PHP_EOL.PHP_EOL.$errorMessage - ); + if (! $hasError) { + PHPUnit::fail( + "Failed to find a validation error for key and message: '$key' => '$message'".PHP_EOL.PHP_EOL.$errorMessage + ); + } } } } diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 46d05ce970bf..6f565e981f8a 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1500,6 +1500,54 @@ public function testAssertSessionValidationErrorsUsingAssertValid() $testResponse->assertValid(); } + public function testAssertingKeyIsInvalidErrorMessage() + { + app()->instance('session.store', $store = new Store('test-session', new ArraySessionHandler(1))); + $store->put('errors', $errorBag = new ViewErrorBag); + $testResponse = TestResponse::fromBaseResponse(new Response); + + try { + $testResponse->assertInvalid(['input_name']); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertStringStartsWith("Failed to find a validation error in session for key: 'input_name'", $e->getMessage()); + } + + try { + $testResponse->assertInvalid(['input_name' => 'Expected error message.']); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertStringStartsWith("Failed to find a validation error in session for key: 'input_name'", $e->getMessage()); + } + } + + public function testInvalidWithListOfErrors() + { + app()->instance('session.store', $store = new Store('test-session', new ArraySessionHandler(1))); + + $store->put('errors', $errorBag = new ViewErrorBag); + + $errorBag->put('default', new MessageBag([ + 'first_name' => [ + 'Your first name is required', + 'Your first name must be at least 1 character', + ], + ])); + + $testResponse = TestResponse::fromBaseResponse(new Response); + + $testResponse->assertInvalid(['first_name' => 'Your first name is required']); + $testResponse->assertInvalid(['first_name' => 'Your first name must be at least 1 character']); + $testResponse->assertInvalid(['first_name' => ['Your first name is required', 'Your first name must be at least 1 character']]); + + try { + $testResponse->assertInvalid(['first_name' => ['Your first name is required', 'FOO']]); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertStringStartsWith("Failed to find a validation error for key and message: 'first_name' => 'FOO'", $e->getMessage()); + } + } + public function testAssertJsonValidationErrorsCustomErrorsName() { $data = [ From 7907bfb50165b3951403e2d9882b4f8e0ee26b9e Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 11 Dec 2023 23:01:00 +0330 Subject: [PATCH 181/207] [10.x] Get foreign keys of a table (#49264) * get foreign keys * fix tests * fix tests * fix tests * fix tests --- .../Query/Processors/MySqlProcessor.php | 23 +++++++++ .../Query/Processors/PostgresProcessor.php | 37 +++++++++++++++ .../Database/Query/Processors/Processor.php | 11 +++++ .../Query/Processors/SQLiteProcessor.php | 23 +++++++++ .../Query/Processors/SqlServerProcessor.php | 23 +++++++++ src/Illuminate/Database/Schema/Builder.php | 15 ++++++ .../Database/Schema/Grammars/MySqlGrammar.php | 26 ++++++++++ .../Schema/Grammars/PostgresGrammar.php | 30 ++++++++++++ .../Schema/Grammars/SQLiteGrammar.php | 17 +++++++ .../Schema/Grammars/SqlServerGrammar.php | 29 ++++++++++++ .../Database/Schema/MySqlBuilder.php | 17 +++++++ .../Database/Schema/PostgresBuilder.php | 17 +++++++ src/Illuminate/Support/Facades/Schema.php | 1 + .../Database/SchemaBuilderTest.php | 47 +++++++++++++++++++ 14 files changed, 316 insertions(+) diff --git a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php index 7e6c66face3a..091fc80b52b0 100644 --- a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php +++ b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php @@ -63,4 +63,27 @@ public function processIndexes($results) ]; }, $results); } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => $result->foreign_schema, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => strtolower($result->on_update), + 'on_delete' => strtolower($result->on_delete), + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index c45ae4ae57ee..ddfbfe722da2 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -91,4 +91,41 @@ public function processIndexes($results) ]; }, $results); } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => $result->foreign_schema, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => match (strtolower($result->on_update)) { + 'a' => 'no action', + 'r' => 'restrict', + 'c' => 'cascade', + 'n' => 'set null', + 'd' => 'set default', + default => null, + }, + 'on_delete' => match (strtolower($result->on_delete)) { + 'a' => 'no action', + 'r' => 'restrict', + 'c' => 'cascade', + 'n' => 'set null', + 'd' => 'set default', + default => null, + }, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 7abf156ad662..87a15c6d7a1d 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -99,6 +99,17 @@ public function processIndexes($results) return $results; } + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return $results; + } + /** * Process the results of a column listing query. * diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index 6c6da5567dba..8f5fb98206e0 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -79,4 +79,27 @@ public function processIndexes($results) return $indexes; } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => null, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => null, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => strtolower($result->on_update), + 'on_delete' => strtolower($result->on_delete), + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php index 15fa4d740745..c089593ed86a 100755 --- a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php @@ -121,4 +121,27 @@ public function processIndexes($results) ]; }, $results); } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => $result->foreign_schema, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => strtolower(str_replace('_', ' ', $result->on_update)), + 'on_delete' => strtolower(str_replace('_', ' ', $result->on_delete)), + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 07698e7fc206..efcad17fced3 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -357,6 +357,21 @@ public function getIndexes($table) ); } + /** + * Get the foreign keys for a given table. + * + * @param string $table + * @return array + */ + public function getForeignKeys($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($table)) + ); + } + /** * Modify a table on the schema. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 43875591e07f..11ec0df9079b 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -185,6 +185,32 @@ public function compileIndexes($database, $table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $database + * @param string $table + * @return string + */ + public function compileForeignKeys($database, $table) + { + return sprintf( + 'select kc.constraint_name as `name`, ' + .'group_concat(kc.column_name order by kc.ordinal_position) as `columns`, ' + .'kc.referenced_table_schema as `foreign_schema`, ' + .'kc.referenced_table_name as `foreign_table`, ' + .'group_concat(kc.referenced_column_name order by kc.ordinal_position) as `foreign_columns`, ' + .'rc.update_rule as `on_update`, ' + .'rc.delete_rule as `on_delete` ' + .'from information_schema.key_column_usage kc join information_schema.referential_constraints rc ' + .'on kc.constraint_schema = rc.constraint_schema and kc.constraint_name = rc.constraint_name ' + .'where kc.table_schema = %s and kc.table_name = %s and kc.referenced_table_name is not null ' + .'group by kc.constraint_name, kc.referenced_table_schema, kc.referenced_table_name, rc.update_rule, rc.delete_rule', + $this->quoteString($database), + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 9e18f2d9a773..094d49605d19 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -189,6 +189,36 @@ public function compileIndexes($schema, $table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $schema + * @param string $table + * @return string + */ + public function compileForeignKeys($schema, $table) + { + return sprintf( + 'select c.conname as name, ' + ."string_agg(la.attname, ',' order by conseq.ord) as columns, " + .'fn.nspname as foreign_schema, fc.relname as foreign_table, ' + ."string_agg(fa.attname, ',' order by conseq.ord) as foreign_columns, " + .'c.confupdtype as on_update, c.confdeltype as on_delete ' + .'from pg_constraint c ' + .'join pg_class tc on c.conrelid = tc.oid ' + .'join pg_namespace tn on tn.oid = tc.relnamespace ' + .'join pg_class fc on c.confrelid = fc.oid ' + .'join pg_namespace fn on fn.oid = fc.relnamespace ' + .'join lateral unnest(c.conkey) with ordinality as conseq(num, ord) on true ' + .'join pg_attribute la on la.attrelid = c.conrelid and la.attnum = conseq.num ' + .'join pg_attribute fa on fa.attrelid = c.confrelid and fa.attnum = c.confkey[conseq.ord] ' + ."where c.contype = 'f' and tc.relname = %s and tn.nspname = %s " + .'group by c.conname, fn.nspname, fc.relname, c.confupdtype, c.confdeltype', + $this->quoteString($table), + $this->quoteString($schema) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index a008fb73b0c6..e9c7d8c80490 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -146,6 +146,23 @@ public function compileIndexes($table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $table + * @return string + */ + public function compileForeignKeys($table) + { + return sprintf( + 'select group_concat("from") as columns, "table" as foreign_table, ' + .'group_concat("to") as foreign_columns, on_update, on_delete ' + .'from (select * from pragma_foreign_key_list(%s) order by id desc, seq) ' + .'group by id, "table", on_update, on_delete', + $this->wrap(str_replace('.', '__', $table)) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index b4927d5f3432..4b9091a1d6ef 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -189,6 +189,35 @@ public function compileIndexes($table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $table + * @return string + */ + public function compileForeignKeys($table) + { + return sprintf( + 'select fk.name as name, ' + ."string_agg(lc.name, ',') within group (order by fkc.constraint_column_id) as columns, " + .'fs.name as foreign_schema, ft.name as foreign_table, ' + ."string_agg(fc.name, ',') within group (order by fkc.constraint_column_id) as foreign_columns, " + .'fk.update_referential_action_desc as on_update, ' + .'fk.delete_referential_action_desc as on_delete ' + .'from sys.foreign_keys as fk ' + .'join sys.foreign_key_columns as fkc on fkc.constraint_object_id = fk.object_id ' + .'join sys.tables as lt on lt.object_id = fk.parent_object_id ' + .'join sys.schemas as ls on lt.schema_id = ls.schema_id ' + .'join sys.columns as lc on fkc.parent_object_id = lc.object_id and fkc.parent_column_id = lc.column_id ' + .'join sys.tables as ft on ft.object_id = fk.referenced_object_id ' + .'join sys.schemas as fs on ft.schema_id = fs.schema_id ' + .'join sys.columns as fc on fkc.referenced_object_id = fc.object_id and fkc.referenced_column_id = fc.column_id ' + .'where lt.name = %s and ls.name = SCHEMA_NAME() ' + .'group by fk.name, fs.name, ft.name, fk.update_referential_action_desc, fk.delete_referential_action_desc', + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index e51305002480..0c537ba980cd 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -120,6 +120,23 @@ public function getIndexes($table) ); } + /** + * Get the foreign keys for a given table. + * + * @param string $table + * @return array + */ + public function getForeignKeys($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection( + $this->grammar->compileForeignKeys($this->connection->getDatabaseName(), $table) + ) + ); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 0efe5dc62161..4990cb445d98 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -220,6 +220,23 @@ public function getIndexes($table) ); } + /** + * Get the foreign keys for a given table. + * + * @param string $table + * @return array + */ + public function getForeignKeys($table) + { + [, $schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($schema, $table)) + ); + } + /** * Get the schemas for the connection. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 1bf87ba9a151..934d27e6f508 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -22,6 +22,7 @@ * @method static array getColumnListing(string $table) * @method static array getColumns(string $table) * @method static array getIndexes(string $table) + * @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) diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 2e79303b0574..ea9a6d905031 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -261,4 +261,51 @@ public function testGetFullTextIndexes() $this->assertTrue(collect($indexes)->contains(fn ($index) => $index['columns'] === ['id'] && $index['primary'])); $this->assertTrue(collect($indexes)->contains('name', 'articles_body_title_fulltext')); } + + public function testGetForeignKeys() + { + Schema::create('users', function (Blueprint $table) { + $table->id(); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->foreignId('user_id')->nullable()->constrained()->cascadeOnUpdate()->nullOnDelete(); + }); + + $foreignKeys = Schema::getForeignKeys('posts'); + + $this->assertCount(1, $foreignKeys); + $this->assertTrue(collect($foreignKeys)->contains( + fn ($foreign) => $foreign['columns'] === ['user_id'] + && $foreign['foreign_table'] === 'users' && $foreign['foreign_columns'] === ['id'] + && $foreign['on_update'] === 'cascade' && $foreign['on_delete'] === 'set null' + )); + } + + public function testGetCompoundForeignKeys() + { + Schema::create('parent', function (Blueprint $table) { + $table->id(); + $table->integer('a'); + $table->integer('b'); + + $table->unique(['b', 'a']); + }); + + Schema::create('child', function (Blueprint $table) { + $table->integer('c'); + $table->integer('d'); + + $table->foreign(['d', 'c'], 'test_fk')->references(['b', 'a'])->on('parent'); + }); + + $foreignKeys = Schema::getForeignKeys('child'); + + $this->assertCount(1, $foreignKeys); + $this->assertTrue(collect($foreignKeys)->contains( + fn ($foreign) => $foreign['columns'] === ['d', 'c'] + && $foreign['foreign_table'] === 'parent' + && $foreign['foreign_columns'] === ['b', 'a'] + )); + } } From f30906eff7bbe90bd5f412cfb37901bb0bf0f1ba Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 12 Dec 2023 21:49:25 +0800 Subject: [PATCH 182/207] [10.x] PHPStan Improvements (#49343) Compatible with PHPStan 1.10.49 Signed-off-by: Mior Muhammad Zaki --- types/Support/Collection.php | 32 ++++++++++++++++---------------- types/Support/LazyCollection.php | 12 ++++++------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/types/Support/Collection.php b/types/Support/Collection.php index f2f5311c8c85..77861041deaf 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -264,7 +264,7 @@ public function __invoke(): string return true; })); -assertType('Illuminate\Support\Collection|void', $collection->when(true, function ($collection) { +assertType('Illuminate\Support\Collection|null', $collection->when(true, function ($collection) { assertType('Illuminate\Support\Collection', $collection); })); assertType('Illuminate\Support\Collection|string', $collection->when(true, function ($collection) { @@ -272,12 +272,12 @@ public function __invoke(): string return 'string'; })); -assertType('Illuminate\Support\Collection|void', $collection->when('Taylor', function ($collection, $name) { +assertType('Illuminate\Support\Collection|null', $collection->when('Taylor', function ($collection, $name) { assertType('Illuminate\Support\Collection', $collection); assertType('string', $name); })); assertType( - 'Illuminate\Support\Collection|void', + 'Illuminate\Support\Collection|null', $collection->when( 'Taylor', function ($collection, $name) { @@ -290,12 +290,12 @@ function ($collection, $name) { } ) ); -assertType('Illuminate\Support\Collection|void', $collection->when(fn () => 'Taylor', function ($collection, $name) { +assertType('Illuminate\Support\Collection|null', $collection->when(fn () => 'Taylor', function ($collection, $name) { assertType('Illuminate\Support\Collection', $collection); assertType('string', $name); })); assertType( - 'Illuminate\Support\Collection|void', + 'Illuminate\Support\Collection|null', $collection->when( function ($collection) { assertType('Illuminate\Support\Collection', $collection); @@ -313,7 +313,7 @@ function ($collection, $count) { ) ); -assertType('Illuminate\Support\Collection|void', $collection->when($invokable, function ($collection, $param) { +assertType('Illuminate\Support\Collection|null', $collection->when($invokable, function ($collection, $param) { assertType('Illuminate\Support\Collection', $collection); assertType('Invokable', $param); })); @@ -323,7 +323,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection|void', $collection->whenEmpty(function ($collection) { +assertType('Illuminate\Support\Collection|null', $collection->whenEmpty(function ($collection) { assertType('Illuminate\Support\Collection', $collection); })); assertType('Illuminate\Support\Collection|string', $collection->whenEmpty(function ($collection) { @@ -337,7 +337,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection|void', $collection->whenNotEmpty(function ($collection) { +assertType('Illuminate\Support\Collection|null', $collection->whenNotEmpty(function ($collection) { assertType('Illuminate\Support\Collection', $collection); })); assertType('Illuminate\Support\Collection|string', $collection->whenNotEmpty(function ($collection) { @@ -351,7 +351,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection|void', $collection->unless(true, function ($collection) { +assertType('Illuminate\Support\Collection|null', $collection->unless(true, function ($collection) { assertType('Illuminate\Support\Collection', $collection); })); assertType('Illuminate\Support\Collection|string', $collection->unless(true, function ($collection) { @@ -359,12 +359,12 @@ function ($collection, $count) { return 'string'; })); -assertType('Illuminate\Support\Collection|void', $collection->unless('Taylor', function ($collection, $name) { +assertType('Illuminate\Support\Collection|null', $collection->unless('Taylor', function ($collection, $name) { assertType('Illuminate\Support\Collection', $collection); assertType('string', $name); })); assertType( - 'Illuminate\Support\Collection|void', + 'Illuminate\Support\Collection|null', $collection->unless( 'Taylor', function ($collection, $name) { @@ -377,12 +377,12 @@ function ($collection, $name) { } ) ); -assertType('Illuminate\Support\Collection|void', $collection->unless(fn () => 'Taylor', function ($collection, $name) { +assertType('Illuminate\Support\Collection|null', $collection->unless(fn () => 'Taylor', function ($collection, $name) { assertType('Illuminate\Support\Collection', $collection); assertType('string', $name); })); assertType( - 'Illuminate\Support\Collection|void', + 'Illuminate\Support\Collection|null', $collection->unless( function ($collection) { assertType('Illuminate\Support\Collection', $collection); @@ -400,7 +400,7 @@ function ($collection, $count) { ) ); -assertType('Illuminate\Support\Collection|void', $collection->unless($invokable, function ($collection, $param) { +assertType('Illuminate\Support\Collection|null', $collection->unless($invokable, function ($collection, $param) { assertType('Illuminate\Support\Collection', $collection); assertType('Invokable', $param); })); @@ -410,7 +410,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection|void', $collection->unlessEmpty(function ($collection) { +assertType('Illuminate\Support\Collection|null', $collection->unlessEmpty(function ($collection) { assertType('Illuminate\Support\Collection', $collection); })); assertType('Illuminate\Support\Collection|string', $collection->unlessEmpty(function ($collection) { @@ -424,7 +424,7 @@ function ($collection, $count) { return true; })); -assertType('Illuminate\Support\Collection|void', $collection->unlessNotEmpty(function ($collection) { +assertType('Illuminate\Support\Collection|null', $collection->unlessNotEmpty(function ($collection) { assertType('Illuminate\Support\Collection', $collection); })); assertType('Illuminate\Support\Collection|string', $collection->unlessNotEmpty(function ($collection) { diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index 104c2c8f3b5c..c9ad444ec154 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -250,7 +250,7 @@ return true; })); -assertType('Illuminate\Support\LazyCollection|void', $collection->when(true, function ($collection) { +assertType('Illuminate\Support\LazyCollection|null', $collection->when(true, function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); })); assertType('Illuminate\Support\LazyCollection|string', $collection->when(true, function ($collection) { @@ -264,7 +264,7 @@ return true; })); -assertType('Illuminate\Support\LazyCollection|void', $collection->whenEmpty(function ($collection) { +assertType('Illuminate\Support\LazyCollection|null', $collection->whenEmpty(function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); })); assertType('Illuminate\Support\LazyCollection|string', $collection->whenEmpty(function ($collection) { @@ -278,7 +278,7 @@ return true; })); -assertType('Illuminate\Support\LazyCollection|void', $collection->whenNotEmpty(function ($collection) { +assertType('Illuminate\Support\LazyCollection|null', $collection->whenNotEmpty(function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); })); assertType('Illuminate\Support\LazyCollection|string', $collection->whenNotEmpty(function ($collection) { @@ -292,7 +292,7 @@ return true; })); -assertType('Illuminate\Support\LazyCollection|void', $collection->unless(true, function ($collection) { +assertType('Illuminate\Support\LazyCollection|null', $collection->unless(true, function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); })); assertType('Illuminate\Support\LazyCollection|string', $collection->unless(true, function ($collection) { @@ -306,7 +306,7 @@ return true; })); -assertType('Illuminate\Support\LazyCollection|void', $collection->unlessEmpty(function ($collection) { +assertType('Illuminate\Support\LazyCollection|null', $collection->unlessEmpty(function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); })); assertType('Illuminate\Support\LazyCollection|string', $collection->unlessEmpty(function ($collection) { @@ -320,7 +320,7 @@ return true; })); -assertType('Illuminate\Support\LazyCollection|void', $collection->unlessNotEmpty(function ($collection) { +assertType('Illuminate\Support\LazyCollection|null', $collection->unlessNotEmpty(function ($collection) { assertType('Illuminate\Support\LazyCollection', $collection); })); assertType('Illuminate\Support\LazyCollection|string', $collection->unlessNotEmpty(function ($collection) { From 265e932e81fff663164444e4c8c950af90fb79ad Mon Sep 17 00:00:00 2001 From: DeanWunder <30644242+DeanWunder@users.noreply.github.com> Date: Wed, 13 Dec 2023 01:56:20 +1100 Subject: [PATCH 183/207] [10.x] Handle missing translations: more robust handling of callback return value (#49341) * handle-missing-translations-fix Ensure that when the user doesn't return a value from their missing translation handler, the translator returns the provided key as per normal behaviour. Avoids the translator returning null which is unexpected and could break things. * handle-missing-translations-fix Add Test for missing translation handler closure without a return statement. --------- Co-authored-by: Dean Wunder --- src/Illuminate/Translation/Translator.php | 2 +- tests/Integration/Translation/TranslatorTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index 41fb2c94922b..f9f8b49cb11c 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -348,7 +348,7 @@ protected function handleMissingTranslationKey($key, $replace, $locale, $fallbac $key = call_user_func( $this->missingTranslationKeyCallback, $key, $replace, $locale, $fallback - ); + ) ?? $key; $this->handleMissingTranslationKeys = true; diff --git a/tests/Integration/Translation/TranslatorTest.php b/tests/Integration/Translation/TranslatorTest.php index 11f449dd487d..5e374e82d1cb 100644 --- a/tests/Integration/Translation/TranslatorTest.php +++ b/tests/Integration/Translation/TranslatorTest.php @@ -56,4 +56,18 @@ public function testItCanHandleMissingKeysUsingCallback() $this->app['translator']->handleMissingKeysUsing(null); } + + public function testItCanHandleMissingKeysNoReturn() + { + $this->app['translator']->handleMissingKeysUsing(function ($key) { + $_SERVER['__missing_translation_key'] = $key; + }); + + $key = $this->app['translator']->get('some missing key'); + + $this->assertSame('some missing key', $key); + $this->assertSame('some missing key', $_SERVER['__missing_translation_key']); + + $this->app['translator']->handleMissingKeysUsing(null); + } } From f286153e137b197520845daf600b26801fa2271b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 12 Dec 2023 10:17:05 -0600 Subject: [PATCH 184/207] formatting --- 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 a24cdef721d1..5298092b8b1b 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.35.0'; + const VERSION = '10.36.0'; /** * The base path for the Laravel installation. From 9693e3287ff4d8b40ba54e915a05530d1cdfa4ee Mon Sep 17 00:00:00 2001 From: driesvints Date: Tue, 12 Dec 2023 16:33:26 +0000 Subject: [PATCH 185/207] Update CHANGELOG --- CHANGELOG.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f5534e4b0f..6feb564990dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.35.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.37.0...10.x) + +## [v10.37.0](https://github.com/laravel/framework/compare/v10.35.0...v10.37.0) - 2023-12-12 + +* [10.x] Add `engine` method to `Blueprint` by [@jbrooksuk](https://github.com/jbrooksuk) in https://github.com/laravel/framework/pull/49250 +* [10.x] Use translator from validator in `Can` and `Enum` rules by [@fancyweb](https://github.com/fancyweb) in https://github.com/laravel/framework/pull/49251 +* [10.x] Get indexes of a table by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49204 +* [10.x] Filesystem : can lock file on append of content by [@StephaneBour](https://github.com/StephaneBour) in https://github.com/laravel/framework/pull/49262 +* [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49266 +* [10.x] Fixes generating facades documentation shouldn't be affected by `php-psr` extension by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49268 +* [10.x] Fixes `AboutCommand::format()` docblock by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49274 +* [10.x] `Route::getController()` should return `null` when the accessing closure based route by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49269 +* [10.x] Add "noActionOnUpdate" method in Illuminate/Database/Schema/ForeignKeyDefinition by [@hrsa](https://github.com/hrsa) in https://github.com/laravel/framework/pull/49297 +* [10.x] Fixing number helper for floating 0.0 by [@mr-punyapal](https://github.com/mr-punyapal) in https://github.com/laravel/framework/pull/49277 +* [10.x] Allow checking if lock succesfully restored by [@Joostb](https://github.com/Joostb) in https://github.com/laravel/framework/pull/49272 +* [10.x] Enable DynamoDB as a backend for Job Batches by [@khepin](https://github.com/khepin) in https://github.com/laravel/framework/pull/49169 +* [10.x] Removed deprecated and not used argument by [@Muetze42](https://github.com/Muetze42) in https://github.com/laravel/framework/pull/49304 +* [10.x] Add Conditionable to Batched and Chained jobs by [@bretto36](https://github.com/bretto36) in https://github.com/laravel/framework/pull/49310 +* [10.x] Include partitioned tables on PostgreSQL when retrieving tables by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49326 +* [10.x] Allow to pass `Arrayable` or `Stringble` in rules `In` and `NotIn` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/49055 +* [10.x] Display error message if json_encode() fails by [@aimeos](https://github.com/aimeos) in https://github.com/laravel/framework/pull/48856 +* [10.x] Allow error list per field by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/49309 +* [10.x] Get foreign keys of a table by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49264 +* [10.x] PHPStan Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49343 +* [10.x] Handle missing translations: more robust handling of callback return value by [@DeanWunder](https://github.com/DeanWunder) in https://github.com/laravel/framework/pull/49341 ## [v10.35.0](https://github.com/laravel/framework/compare/v10.34.2...v10.35.0) - 2023-12-05 From 2f65457e6fc4f17c751fadc71bd2ca93e593cf28 Mon Sep 17 00:00:00 2001 From: KentarouTakeda Date: Wed, 13 Dec 2023 01:35:08 +0900 Subject: [PATCH 186/207] Disconnecting the database connection after testing (#49327) * Disconnection after testing with traits for database testing * Revert "Disconnection after testing with traits for database testing" This reverts commit 71c6653640d56d65d88d5addc85731974430e6d0. * Disconnection at teardown * No need to disconnect for individual traits * fix: Behavior when run without database * Avoid changing the behavior before fixing as much as possible. * Update TestCase.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Foundation/Testing/DatabaseTransactions.php | 2 +- src/Illuminate/Foundation/Testing/RefreshDatabase.php | 2 +- src/Illuminate/Foundation/Testing/TestCase.php | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php index 83a686f3558c..c6fb714ada4b 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php @@ -33,7 +33,7 @@ public function beginDatabaseTransaction() $connection->unsetEventDispatcher(); $connection->rollBack(); $connection->setEventDispatcher($dispatcher); - $connection->disconnect(); + $database->purge($name); } }); } diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabase.php b/src/Illuminate/Foundation/Testing/RefreshDatabase.php index 0f916ac55b51..c68b19bbd54d 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabase.php @@ -109,7 +109,7 @@ public function beginDatabaseTransaction() $connection->unsetEventDispatcher(); $connection->rollBack(); $connection->setEventDispatcher($dispatcher); - $connection->disconnect(); + $database->purge($name); } }); } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index a7315a2c5124..e38e65430a08 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -197,6 +197,12 @@ protected function tearDown(): void ParallelTesting::callTearDownTestCaseCallbacks($this); + $database = $this->app['db'] ?? null; + + foreach (array_keys($database?->getConnections() ?? []) as $name) { + $database->purge($name); + } + $this->app->flush(); $this->app = null; From 57ae89a8acbc316153315272a4af2e68370f5e36 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 12 Dec 2023 20:06:26 +0330 Subject: [PATCH 187/207] [10.x] Get user-defined types on PostgreSQL (#49303) * get user defined types * wip * fix tests * fix tests * wip * fix tests --- .../Query/Processors/PostgresProcessor.php | 48 +++++++++++++++++++ .../Database/Query/Processors/Processor.php | 11 +++++ src/Illuminate/Database/Schema/Builder.php | 10 ++++ .../Schema/Grammars/PostgresGrammar.php | 30 ++++++++++++ .../Database/Schema/PostgresBuilder.php | 37 ++++++++++---- src/Illuminate/Support/Facades/Schema.php | 1 + .../Database/SchemaBuilderTest.php | 27 +++++++++++ 7 files changed, 156 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index ddfbfe722da2..d35ee2dc2c6c 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -45,6 +45,54 @@ public function processColumnListing($results) }, $results); } + /** + * Process the results of a types query. + * + * @param array $results + * @return array + */ + public function processTypes($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'schema' => $result->schema, + 'implicit' => (bool) $result->implicit, + 'type' => match (strtolower($result->type)) { + 'b' => 'base', + 'c' => 'composite', + 'd' => 'domain', + 'e' => 'enum', + 'p' => 'pseudo', + 'r' => 'range', + 'm' => 'multirange', + default => null, + }, + 'category' => match (strtolower($result->category)) { + 'a' => 'array', + 'b' => 'boolean', + 'c' => 'composite', + 'd' => 'date_time', + 'e' => 'enum', + 'g' => 'geometric', + 'i' => 'network_address', + 'n' => 'numeric', + 'p' => 'pseudo', + 'r' => 'range', + 's' => 'string', + 't' => 'timespan', + 'u' => 'user_defined', + 'v' => 'bit_string', + 'x' => 'unknown', + 'z' => 'internal_use', + default => null, + }, + ]; + }, $results); + } + /** * Process the results of a columns query. * diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 87a15c6d7a1d..97a994ebc221 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -77,6 +77,17 @@ public function processViews($results) }, $results); } + /** + * Process the results of a types query. + * + * @param array $results + * @return array + */ + public function processTypes($results) + { + return $results; + } + /** * Process the results of a columns query. * diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index efcad17fced3..c4b3da940265 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -211,6 +211,16 @@ public function getViews() ); } + /** + * Get the user-defined types that belong to the database. + * + * @return array + */ + public function getTypes() + { + throw new LogicException('This database driver does not support user-defined types.'); + } + /** * Get all of the table names for the database. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 094d49605d19..4d5fb7ba1312 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -101,6 +101,23 @@ public function compileViews() return "select viewname as name, schemaname as schema, definition from pg_views where schemaname not in ('pg_catalog', 'information_schema') order by viewname"; } + /** + * Compile the query to determine the user-defined types. + * + * @return string + */ + public function compileTypes() + { + return 'select t.typname as name, n.nspname as schema, t.typtype as type, t.typcategory as category, ' + ."((t.typinput = 'array_in'::regproc and t.typoutput = 'array_out'::regproc) or t.typtype = 'm') as implicit " + .'from pg_type t join pg_namespace n on n.oid = t.typnamespace ' + .'left join pg_class c on c.oid = t.typrelid ' + .'left join pg_type el on el.oid = t.typelem ' + .'left join pg_class ce on ce.oid = el.typrelid ' + ."where ((t.typrelid = 0 and (ce.relkind = 'c' or ce.relkind is null)) or c.relkind = 'c') " + ."and n.nspname not in ('pg_catalog', 'information_schema')"; + } + /** * Compile the SQL needed to retrieve all table names. * @@ -503,9 +520,22 @@ public function compileDropAllTypes($types) return 'drop type '.implode(',', $this->escapeNames($types)).' cascade'; } + /** + * Compile the SQL needed to drop all domains. + * + * @param array $domains + * @return string + */ + public function compileDropAllDomains($domains) + { + return 'drop domain '.implode(',', $this->escapeNames($domains)).' cascade'; + } + /** * Compile the SQL needed to retrieve all type names. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileGetAllTypes() diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 4990cb445d98..5b62187b45f0 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -53,6 +53,18 @@ public function hasTable($table) )) > 0; } + /** + * Get the user-defined types that belong to the database. + * + * @return array + */ + public function getTypes() + { + return $this->connection->getPostProcessor()->processTypes( + $this->connection->selectFromWriteConnection($this->grammar->compileTypes()) + ); + } + /** * Get all of the table names for the database. * @@ -151,6 +163,8 @@ public function dropAllViews() /** * Get all of the type names for the database. * + * @deprecated Will be removed in a future Laravel version. + * * @return array */ public function getAllTypes() @@ -168,20 +182,27 @@ public function getAllTypes() public function dropAllTypes() { $types = []; + $domains = []; - foreach ($this->getAllTypes() as $row) { - $row = (array) $row; + $schemas = $this->grammar->escapeNames($this->getSchemas()); - $types[] = reset($row); + foreach ($this->getTypes() as $type) { + if (! $type['implicit'] && in_array($this->grammar->escapeNames([$type['schema']])[0], $schemas)) { + if ($type['type'] === 'domain') { + $domains[] = $type['schema'].'.'.$type['name']; + } else { + $types[] = $type['schema'].'.'.$type['name']; + } + } } - if (empty($types)) { - return; + if (! empty($types)) { + $this->connection->statement($this->grammar->compileDropAllTypes($types)); } - $this->connection->statement( - $this->grammar->compileDropAllTypes($types) - ); + if (! empty($domains)) { + $this->connection->statement($this->grammar->compileDropAllDomains($domains)); + } } /** diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 934d27e6f508..8d4d9b0c0dc8 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -14,6 +14,7 @@ * @method static bool hasView(string $view) * @method static array getTables() * @method static array getViews() + * @method static array getTypes() * @method static bool hasColumn(string $table, string $column) * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index ea9a6d905031..5d38e0e2a8a2 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -182,6 +182,33 @@ public function testGetViews() $this->assertEmpty(array_diff(['foo', 'bar', 'baz'], array_column($views, 'name'))); } + public function testGetAndDropTypes() + { + if ($this->driver !== 'pgsql') { + $this->markTestSkipped('Test requires a PostgreSQL connection.'); + } + + DB::statement('create type pseudo_foo'); + DB::statement('create type comp_foo as (f1 int, f2 text)'); + DB::statement("create type enum_foo as enum ('new', 'open', 'closed')"); + DB::statement('create type range_foo as range (subtype = float8)'); + DB::statement('create domain domain_foo as text'); + + $types = Schema::getTypes(); + + $this->assertCount(11, $types); + $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'pseudo_foo' && $type['type'] === 'pseudo' && ! $type['implicit'])); + $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'comp_foo' && $type['type'] === 'composite' && ! $type['implicit'])); + $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'enum_foo' && $type['type'] === 'enum' && ! $type['implicit'])); + $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'range_foo' && $type['type'] === 'range' && ! $type['implicit'])); + $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'domain_foo' && $type['type'] === 'domain' && ! $type['implicit'])); + + Schema::dropAllTypes(); + $types = Schema::getTypes(); + + $this->assertEmpty($types); + } + public function testGetIndexes() { Schema::create('foo', function (Blueprint $table) { From b41612c58e358655cda1239e18d8851ff8736e8f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 12 Dec 2023 13:03:09 -0600 Subject: [PATCH 188/207] verion --- 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 5298092b8b1b..049585052154 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.36.0'; + const VERSION = '10.37.1'; /** * The base path for the Laravel installation. From 5654bdf6403bf1f22432ca1c14badadaaa4cfcb1 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Tue, 12 Dec 2023 14:36:09 -0500 Subject: [PATCH 189/207] Ability to test chained job via closure (#49337) --- .../Support/Testing/Fakes/BusFake.php | 12 ++++++++++++ tests/Support/SupportTestingBusFakeTest.php | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index 3d717eae6231..38a3f2c15708 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -421,6 +421,18 @@ protected function assertDispatchedWithChainOfObjects($command, $expectedChain, ! $chain[$index]($chainedBatch->toPendingBatch())) { return false; } + } elseif ($chain[$index] instanceof Closure) { + [$expectedType, $callback] = [$this->firstClosureParameterType($chain[$index]), $chain[$index]]; + + $chainedJob = unserialize($serializedChainedJob); + + if (! $chainedJob instanceof $expectedType) { + throw new RuntimeException('The chained job was expected to be of type '.$expectedType.', '.$chainedJob::class.' chained.'); + } + + if (! $callback($chainedJob)) { + return false; + } } elseif (is_string($chain[$index])) { if ($chain[$index] != get_class(unserialize($serializedChainedJob))) { return false; diff --git a/tests/Support/SupportTestingBusFakeTest.php b/tests/Support/SupportTestingBusFakeTest.php index b1d6d8bfd952..384eb930831a 100644 --- a/tests/Support/SupportTestingBusFakeTest.php +++ b/tests/Support/SupportTestingBusFakeTest.php @@ -534,6 +534,16 @@ public function testAssertChained() ChainedJobStub::class, ]); + $this->fake->chain([ + new ChainedJobStub(123), + new ChainedJobStub(456), + ])->dispatch(); + + $this->fake->assertChained([ + fn (ChainedJobStub $job) => $job->id === 123, + fn (ChainedJobStub $job) => $job->id === 456, + ]); + Container::setInstance(null); } @@ -777,6 +787,13 @@ class BusJobStub class ChainedJobStub { use Queueable; + + public $id; + + public function __construct($id = null) + { + $this->id = $id; + } } class OtherBusJobStub From 4c1aa683ee1e003c731d6f9d1240b3b143e92382 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 12 Dec 2023 23:18:30 +0330 Subject: [PATCH 190/207] [11.x] Make floating-types consistent (#48861) * make floating-types consistent * fix tests * fix tests * fix tests --- src/Illuminate/Database/Schema/Blueprint.php | 59 +++---------------- .../Database/Schema/Grammars/MySqlGrammar.php | 10 ++-- .../Schema/Grammars/PostgresGrammar.php | 6 +- .../Schema/Grammars/SQLiteGrammar.php | 2 +- .../Schema/Grammars/SqlServerGrammar.php | 6 +- .../DatabaseMySqlSchemaGrammarTest.php | 16 +---- .../DatabasePostgresSchemaGrammarTest.php | 6 +- .../DatabaseSQLiteSchemaGrammarTest.php | 6 +- .../Database/DatabaseSchemaBlueprintTest.php | 12 ---- .../DatabaseSqlServerSchemaGrammarTest.php | 8 +-- .../Database/DatabaseSchemaBlueprintTest.php | 4 +- 11 files changed, 38 insertions(+), 97 deletions(-) diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index eee247377c59..640cf0177c37 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -980,28 +980,23 @@ public function foreignIdFor($model, $column = null) * Create a new float column on the table. * * @param string $column - * @param int $total - * @param int $places - * @param bool $unsigned + * @param int $precision * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function float($column, $total = 8, $places = 2, $unsigned = false) + public function float($column, $precision = 53) { - return $this->addColumn('float', $column, compact('total', 'places', 'unsigned')); + return $this->addColumn('float', $column, compact('precision')); } /** * Create a new double column on the table. * * @param string $column - * @param int|null $total - * @param int|null $places - * @param bool $unsigned * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function double($column, $total = 15, $places = 6, $unsigned = false) + public function double($column) { - return $this->addColumn('double', $column, compact('total', 'places', 'unsigned')); + return $this->addColumn('double', $column); } /** @@ -1010,51 +1005,11 @@ public function double($column, $total = 15, $places = 6, $unsigned = false) * @param string $column * @param int $total * @param int $places - * @param bool $unsigned - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function decimal($column, $total = 8, $places = 2, $unsigned = false) - { - return $this->addColumn('decimal', $column, compact('total', 'places', 'unsigned')); - } - - /** - * Create a new unsigned float column on the table. - * - * @param string $column - * @param int $total - * @param int $places - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function unsignedFloat($column, $total = 8, $places = 2) - { - return $this->float($column, $total, $places, true); - } - - /** - * Create a new unsigned double column on the table. - * - * @param string $column - * @param int $total - * @param int $places - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function unsignedDouble($column, $total = null, $places = null) - { - return $this->double($column, $total, $places, true); - } - - /** - * Create a new unsigned decimal column on the table. - * - * @param string $column - * @param int $total - * @param int $places * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function unsignedDecimal($column, $total = 8, $places = 2) + public function decimal($column, $total = 8, $places = 2) { - return $this->decimal($column, $total, $places, true); + return $this->addColumn('decimal', $column, compact('total', 'places')); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 43875591e07f..a65abbf34f97 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -774,7 +774,11 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - return $this->typeDouble($column); + if ($column->precision) { + return "float({$column->precision})"; + } + + return 'float'; } /** @@ -785,10 +789,6 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - if ($column->total && $column->places) { - return "double({$column->total}, {$column->places})"; - } - return 'double'; } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 2995e339d0d5..8ec0f576a0c7 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -809,7 +809,11 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - return $this->typeDouble($column); + if ($column->precision) { + return "float({$column->precision})"; + } + + return 'float'; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 07edd4e2c6f2..2c8c55210531 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -698,7 +698,7 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - return 'float'; + return 'double'; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index b4927d5f3432..01f77a746759 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -708,6 +708,10 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { + if ($column->precision) { + return "float({$column->precision})"; + } + return 'float'; } @@ -719,7 +723,7 @@ protected function typeFloat(Fluent $column) */ protected function typeDouble(Fluent $column) { - return 'float'; + return 'double precision'; } /** diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index 5ffc3aa88d86..7ec3e87f9752 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -754,11 +754,11 @@ public function testAddingTinyInteger() public function testAddingFloat() { $blueprint = new Blueprint('users'); - $blueprint->float('foo', 5, 2); + $blueprint->float('foo', 5); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table `users` add `foo` double(5, 2) not null', $statements[0]); + $this->assertSame('alter table `users` add `foo` float(5) not null', $statements[0]); } public function testAddingDouble() @@ -768,17 +768,7 @@ public function testAddingDouble() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table `users` add `foo` double(15, 6) not null', $statements[0]); - } - - public function testAddingDoubleSpecifyingPrecision() - { - $blueprint = new Blueprint('users'); - $blueprint->double('foo', 15, 8); - $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); - - $this->assertCount(1, $statements); - $this->assertSame('alter table `users` add `foo` double(15, 8) not null', $statements[0]); + $this->assertSame('alter table `users` add `foo` double not null', $statements[0]); } public function testAddingDecimal() diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 9229c6effc8f..20e48d1daf48 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -606,17 +606,17 @@ public function testAddingSmallInteger() public function testAddingFloat() { $blueprint = new Blueprint('users'); - $blueprint->float('foo', 5, 2); + $blueprint->float('foo', 5); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table "users" add column "foo" double precision not null', $statements[0]); + $this->assertSame('alter table "users" add column "foo" float(5) not null', $statements[0]); } public function testAddingDouble() { $blueprint = new Blueprint('users'); - $blueprint->double('foo', 15, 8); + $blueprint->double('foo'); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 60339647c0f9..b43c30a3d2bb 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -465,7 +465,7 @@ public function testAddingSmallInteger() public function testAddingFloat() { $blueprint = new Blueprint('users'); - $blueprint->float('foo', 5, 2); + $blueprint->float('foo', 5); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); @@ -475,11 +475,11 @@ public function testAddingFloat() public function testAddingDouble() { $blueprint = new Blueprint('users'); - $blueprint->double('foo', 15, 8); + $blueprint->double('foo'); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table "users" add column "foo" float not null', $statements[0]); + $this->assertSame('alter table "users" add column "foo" double not null', $statements[0]); } public function testAddingDecimal() diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 884434ddc642..b1bf206988ee 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -146,18 +146,6 @@ public function testDefaultCurrentTimestamp() $this->assertEquals(['alter table "users" add "created" datetime not null default CURRENT_TIMESTAMP'], $blueprint->toSql($connection, new SqlServerGrammar)); } - public function testUnsignedDecimalTable() - { - $base = new Blueprint('users', function ($table) { - $table->unsignedDecimal('money', 10, 2)->useCurrent(); - }); - - $connection = m::mock(Connection::class); - - $blueprint = clone $base; - $this->assertEquals(['alter table `users` add `money` decimal(10, 2) unsigned not null'], $blueprint->toSql($connection, new MySqlGrammar)); - } - public function testRemoveColumn() { $base = new Blueprint('users', function ($table) { diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index f75d8d932966..72e0bc314508 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -510,21 +510,21 @@ public function testAddingSmallInteger() public function testAddingFloat() { $blueprint = new Blueprint('users'); - $blueprint->float('foo', 5, 2); + $blueprint->float('foo', 5); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table "users" add "foo" float not null', $statements[0]); + $this->assertSame('alter table "users" add "foo" float(5) not null', $statements[0]); } public function testAddingDouble() { $blueprint = new Blueprint('users'); - $blueprint->double('foo', 15, 2); + $blueprint->double('foo'); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table "users" add "foo" float not null', $statements[0]); + $this->assertSame('alter table "users" add "foo" double precision not null', $statements[0]); } public function testAddingDecimal() diff --git a/tests/Integration/Database/DatabaseSchemaBlueprintTest.php b/tests/Integration/Database/DatabaseSchemaBlueprintTest.php index 66a217a02f15..3dee06143062 100644 --- a/tests/Integration/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Integration/Database/DatabaseSchemaBlueprintTest.php @@ -123,7 +123,7 @@ public function testNativeColumnModifyingOnMySql() $schema->useNativeSchemaOperationsIfPossible(); $blueprint = new Blueprint('users', function ($table) { - $table->double('amount', 6, 2)->nullable()->invisible()->after('name')->change(); + $table->double('amount')->nullable()->invisible()->after('name')->change(); $table->timestamp('added_at', 4)->nullable(false)->useCurrent()->useCurrentOnUpdate()->change(); $table->enum('difficulty', ['easy', 'hard'])->default('easy')->charset('utf8mb4')->collation('unicode')->change(); $table->multiPolygon('positions')->srid(1234)->storedAs('expression')->change(); @@ -133,7 +133,7 @@ public function testNativeColumnModifyingOnMySql() $this->assertEquals([ 'alter table `users` ' - .'modify `amount` double(6, 2) null invisible after `name`, ' + .'modify `amount` double null invisible after `name`, ' .'modify `added_at` timestamp(4) not null default CURRENT_TIMESTAMP(4) on update CURRENT_TIMESTAMP(4), ' ."modify `difficulty` enum('easy', 'hard') character set utf8mb4 collate 'unicode' not null default 'easy', " .'modify `positions` multipolygon as (expression) stored srid 1234, ' From 00c9c7fe596116c920ce52f7874f0bfa7d53bcca Mon Sep 17 00:00:00 2001 From: Orkhan Ahmadov Date: Tue, 12 Dec 2023 21:04:59 +0100 Subject: [PATCH 191/207] [10.x] Add `progress` option to `PendingBatch` (#49273) * Add `progress` option to pending batch * Invoke `progress` callback also on failure --- src/Illuminate/Bus/Batch.php | 26 ++++++++++++++++++++++++++ src/Illuminate/Bus/PendingBatch.php | 25 +++++++++++++++++++++++++ tests/Bus/BusBatchTest.php | 16 +++++++++++++++- tests/Bus/BusPendingBatchTest.php | 5 ++++- 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Bus/Batch.php b/src/Illuminate/Bus/Batch.php index 644bfbce9dc6..e08b4a975ae4 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -241,6 +241,14 @@ public function recordSuccessfulJob(string $jobId) { $counts = $this->decrementPendingJobs($jobId); + if ($this->hasProgressCallbacks()) { + $batch = $this->fresh(); + + collect($this->options['progress'])->each(function ($handler) use ($batch) { + $this->invokeHandlerCallback($handler, $batch); + }); + } + if ($counts->pendingJobs === 0) { $this->repository->markAsFinished($this->id); } @@ -283,6 +291,16 @@ public function finished() return ! is_null($this->finishedAt); } + /** + * Determine if the batch has "progress" callbacks. + * + * @return bool + */ + public function hasProgressCallbacks() + { + return isset($this->options['progress']) && ! empty($this->options['progress']); + } + /** * Determine if the batch has "success" callbacks. * @@ -328,6 +346,14 @@ public function recordFailedJob(string $jobId, $e) $this->cancel(); } + if ($this->hasProgressCallbacks() && $this->allowsFailures()) { + $batch = $this->fresh(); + + collect($this->options['progress'])->each(function ($handler) use ($batch, $e) { + $this->invokeHandlerCallback($handler, $batch, $e); + }); + } + if ($counts->failedJobs === 1 && $this->hasCatchCallbacks()) { $batch = $this->fresh(); diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index 463cebd9cf6b..60ff3884c8b8 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -74,6 +74,31 @@ public function add($jobs) return $this; } + /** + * Add a callback to be executed after a job in the batch have executed successfully. + * + * @param callable $callback + * @return $this + */ + public function progress($callback) + { + $this->options['progress'][] = $callback instanceof Closure + ? new SerializableClosure($callback) + : $callback; + + return $this; + } + + /** + * Get the "progress" callbacks that have been registered with the pending batch. + * + * @return array + */ + public function progressCallbacks() + { + return $this->options['progress'] ?? []; + } + /** * Add a callback to be executed after all jobs in the batch have executed successfully. * diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 49c394611185..e6137c93843b 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -40,6 +40,7 @@ protected function setUp(): void $this->createSchema(); $_SERVER['__finally.count'] = 0; + $_SERVER['__progress.count'] = 0; $_SERVER['__then.count'] = 0; $_SERVER['__catch.count'] = 0; } @@ -72,7 +73,7 @@ public function createSchema() */ protected function tearDown(): void { - unset($_SERVER['__finally.batch'], $_SERVER['__then.batch'], $_SERVER['__catch.batch'], $_SERVER['__catch.exception']); + unset($_SERVER['__finally.batch'], $_SERVER['__progress.batch'], $_SERVER['__then.batch'], $_SERVER['__catch.batch'], $_SERVER['__catch.exception']); $this->schema()->drop('job_batches'); @@ -201,12 +202,14 @@ public function test_successful_jobs_can_be_recorded() $batch->recordSuccessfulJob('test-id'); $this->assertInstanceOf(Batch::class, $_SERVER['__finally.batch']); + $this->assertInstanceOf(Batch::class, $_SERVER['__progress.batch']); $this->assertInstanceOf(Batch::class, $_SERVER['__then.batch']); $batch = $batch->fresh(); $this->assertEquals(0, $batch->pendingJobs); $this->assertTrue($batch->finished()); $this->assertEquals(1, $_SERVER['__finally.count']); + $this->assertEquals(2, $_SERVER['__progress.count']); $this->assertEquals(1, $_SERVER['__then.count']); } @@ -247,6 +250,7 @@ public function test_failed_jobs_can_be_recorded_while_not_allowing_failures() $this->assertTrue($batch->finished()); $this->assertTrue($batch->cancelled()); $this->assertEquals(1, $_SERVER['__finally.count']); + $this->assertEquals(0, $_SERVER['__progress.count']); $this->assertEquals(1, $_SERVER['__catch.count']); $this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage()); } @@ -288,6 +292,7 @@ public function test_failed_jobs_can_be_recorded_while_allowing_failures() $this->assertFalse($batch->finished()); $this->assertFalse($batch->cancelled()); $this->assertEquals(1, $_SERVER['__catch.count']); + $this->assertEquals(2, $_SERVER['__progress.count']); $this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage()); } @@ -327,6 +332,11 @@ public function test_batch_state_can_be_inspected() $batch->finishedAt = now(); $this->assertTrue($batch->finished()); + $batch->options['progress'] = []; + $this->assertFalse($batch->hasProgressCallbacks()); + $batch->options['progress'] = [1]; + $this->assertTrue($batch->hasProgressCallbacks()); + $batch->options['then'] = []; $this->assertFalse($batch->hasThenCallbacks()); $batch->options['then'] = [1]; @@ -463,6 +473,10 @@ protected function createTestBatch($queue, $allowFailures = false) $repository = new DatabaseBatchRepository(new BatchFactory($queue), DB::connection(), 'job_batches'); $pendingBatch = (new PendingBatch(new Container, collect())) + ->progress(function (Batch $batch) { + $_SERVER['__progress.batch'] = $batch; + $_SERVER['__progress.count']++; + }) ->then(function (Batch $batch) { $_SERVER['__then.batch'] = $batch; $_SERVER['__then.count']++; diff --git a/tests/Bus/BusPendingBatchTest.php b/tests/Bus/BusPendingBatchTest.php index 471330eb8d48..7cd5f7e3a80e 100644 --- a/tests/Bus/BusPendingBatchTest.php +++ b/tests/Bus/BusPendingBatchTest.php @@ -37,7 +37,9 @@ public function test_pending_batch_may_be_configured_and_dispatched() $pendingBatch = new PendingBatch($container, new Collection([$job])); - $pendingBatch = $pendingBatch->then(function () { + $pendingBatch = $pendingBatch->progress(function () { + // + })->then(function () { // })->catch(function () { // @@ -45,6 +47,7 @@ public function test_pending_batch_may_be_configured_and_dispatched() $this->assertSame('test-connection', $pendingBatch->connection()); $this->assertSame('test-queue', $pendingBatch->queue()); + $this->assertCount(1, $pendingBatch->progressCallbacks()); $this->assertCount(1, $pendingBatch->thenCallbacks()); $this->assertCount(1, $pendingBatch->catchCallbacks()); $this->assertArrayHasKey('extra-option', $pendingBatch->options); From 9fe14c8c77c1c0367ace91ac96c129a42ee0d59a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 13 Dec 2023 09:28:59 +0800 Subject: [PATCH 192/207] [10.x] Test Improvements (#49338) * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * 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 * Apply fixes from StyleCI * 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 * 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 * Apply fixes from StyleCI * 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 * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- .github/workflows/queues.yml | 160 ++++++++++++++++++ .../Testing/Concerns/InteractsWithRedis.php | 2 +- .../Queue/CallQueuedHandlerTest.php | 7 - tests/Integration/Queue/CustomPayloadTest.php | 5 +- tests/Integration/Queue/DynamoBatchTest.php | 38 +++-- .../Queue/DynamoBatchTestWithTTL.php | 10 +- tests/Integration/Queue/JobChainingTest.php | 117 ++++++++++--- .../Integration/Queue/JobDispatchingTest.php | 33 +++- .../Integration/Queue/QueueConnectionTest.php | 2 +- tests/Integration/Queue/QueueTestCase.php | 86 ++++++++++ tests/Integration/Queue/RateLimitedTest.php | 7 - .../Queue/RateLimitedWithRedisTest.php | 4 +- .../Queue/RedisQueueTest.php} | 95 ++++------- .../Queue/SkipIfBatchCancelledTest.php | 7 - .../Queue/ThrottlesExceptionsTest.php | 7 - .../ThrottlesExceptionsWithRedisTest.php | 6 +- tests/Integration/Queue/UniqueJobTest.php | 54 +++--- .../Queue/WithoutOverlappingJobsTest.php | 10 +- tests/Integration/Queue/WorkCommandTest.php | 69 ++++---- 19 files changed, 496 insertions(+), 223 deletions(-) create mode 100644 .github/workflows/queues.yml create mode 100644 tests/Integration/Queue/QueueTestCase.php rename tests/{Queue/RedisQueueIntegrationTest.php => Integration/Queue/RedisQueueTest.php} (94%) diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml new file mode 100644 index 000000000000..68e41cfa2aca --- /dev/null +++ b/.github/workflows/queues.yml @@ -0,0 +1,160 @@ +name: queues + +on: + push: + branches: + - master + - '*.x' + pull_request: + +jobs: + sync: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + + name: Sync Driver + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Queue + env: + QUEUE_CONNECTION: sync + + database: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + + name: Database Driver + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Queue + env: + DB_CONNECTION: testing + QUEUE_CONNECTION: database + + redis: + runs-on: ubuntu-22.04 + + services: + redis: + image: redis:7.0 + ports: + - 6379:6379 + options: --entrypoint redis-server + + strategy: + fail-fast: true + + name: Redis Driver + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Queue + env: + QUEUE_CONNECTION: redis + + beanstalkd: + runs-on: ubuntu-22.04 + + name: Beanstalkd Driver + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/checkout@v3 + - name: Download & Extract beanstalkd + run: curl -L https://github.com/beanstalkd/beanstalkd/archive/refs/tags/v1.13.tar.gz | tar xz + - name: Make beanstalkd + run: make + working-directory: beanstalkd-1.13 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Daemonize beanstalkd + run: ./beanstalkd-1.13/beanstalkd & + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Queue + env: + QUEUE_CONNECTION: beanstalkd diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php index 7784736b7ed9..f6263d29cd93 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php @@ -19,7 +19,7 @@ trait InteractsWithRedis /** * Redis manager instance. * - * @var \Illuminate\Redis\RedisManager[] + * @var array */ private $redis; diff --git a/tests/Integration/Queue/CallQueuedHandlerTest.php b/tests/Integration/Queue/CallQueuedHandlerTest.php index 97682983bc06..bd4a179322ac 100644 --- a/tests/Integration/Queue/CallQueuedHandlerTest.php +++ b/tests/Integration/Queue/CallQueuedHandlerTest.php @@ -15,13 +15,6 @@ class CallQueuedHandlerTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testJobCanBeDispatched() { CallQueuedHandlerTestJob::$handled = false; diff --git a/tests/Integration/Queue/CustomPayloadTest.php b/tests/Integration/Queue/CustomPayloadTest.php index 5ae3a8f43017..e3f7cf28b9a8 100644 --- a/tests/Integration/Queue/CustomPayloadTest.php +++ b/tests/Integration/Queue/CustomPayloadTest.php @@ -8,6 +8,7 @@ use Illuminate\Queue\Queue; use Illuminate\Support\ServiceProvider; use Orchestra\Testbench\Concerns\CreatesApplication; +use PHPUnit\Framework\Attributes\DataProvider; class CustomPayloadTest extends TestCase { @@ -25,9 +26,7 @@ public static function websites() yield ['blog.laravel.com']; } - /** - * @dataProvider websites - */ + #[DataProvider('websites')] public function test_custom_payload_gets_cleared_for_each_data_provider(string $websites) { $dispatcher = $this->app->make(QueueingDispatcher::class); diff --git a/tests/Integration/Queue/DynamoBatchTest.php b/tests/Integration/Queue/DynamoBatchTest.php index 43086a39d6aa..76a861d7468f 100644 --- a/tests/Integration/Queue/DynamoBatchTest.php +++ b/tests/Integration/Queue/DynamoBatchTest.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Support\Env; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; @@ -17,34 +18,35 @@ */ class DynamoBatchTest extends TestCase { - const DYNAMODB_ENDPOINT = 'http://localhost:8888'; + public function setUp(): void + { + $this->afterApplicationCreated(function () { + BatchRunRecorder::reset(); + app(DynamoBatchRepository::class)->createAwsDynamoTable(); + }); + + $this->beforeApplicationDestroyed(function () { + app(DynamoBatchRepository::class)->deleteAwsDynamoTable(); + }); - protected function getEnvironmentSetUp($app) + parent::setUp(); + } + + protected function defineEnvironment($app) { + if (is_null($endpoint = Env::get('DYNAMODB_ENDPOINT'))) { + $this->markTestSkipped('Require `dynamodb` to be configured'); + } + $app['config']->set('queue.batching', [ 'driver' => 'dynamodb', 'region' => 'us-west-2', - 'endpoint' => static::DYNAMODB_ENDPOINT, + 'endpoint' => $endpoint, 'key' => 'key', 'secret' => 'secret', ]); } - public function setUp(): void - { - parent::setUp(); - - BatchRunRecorder::reset(); - app(DynamoBatchRepository::class)->createAwsDynamoTable(); - } - - public function tearDown(): void - { - app(DynamoBatchRepository::class)->deleteAwsDynamoTable(); - - parent::tearDown(); - } - public function test_running_a_batch() { Bus::batch([ diff --git a/tests/Integration/Queue/DynamoBatchTestWithTTL.php b/tests/Integration/Queue/DynamoBatchTestWithTTL.php index 4522b7f3b2fa..9aa516737c73 100644 --- a/tests/Integration/Queue/DynamoBatchTestWithTTL.php +++ b/tests/Integration/Queue/DynamoBatchTestWithTTL.php @@ -2,14 +2,20 @@ namespace Illuminate\Tests\Integration\Queue; +use Illuminate\Support\Env; + class DynamoBatchTestWithTTL extends DynamoBatchTest { - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { + if (is_null($endpoint = Env::get('DYNAMODB_ENDPOINT'))) { + $this->markTestSkipped('Require `dynamodb` to be configured'); + } + $app['config']->set('queue.batching', [ 'driver' => 'dynamodb', 'region' => 'us-west-2', - 'endpoint' => static::DYNAMODB_ENDPOINT, + 'endpoint' => $endpoint, 'key' => 'key', 'secret' => 'secret', 'ttl' => 1, diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 3fa00618baa7..7037625b62bf 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -14,39 +14,38 @@ use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Queue; use Orchestra\Testbench\Attributes\WithMigration; -use Orchestra\Testbench\TestCase; #[WithMigration('queue')] -class JobChainingTest extends TestCase +class JobChainingTest extends QueueTestCase { use DatabaseMigrations; public static $catchCallbackRan = false; - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { - $app['config']->set('queue.connections.sync1', [ - 'driver' => 'sync', - ]); + parent::defineEnvironment($app); - $app['config']->set('queue.connections.sync2', [ - 'driver' => 'sync', + $app['config']->set([ + 'queue.connections.sync1' => ['driver' => 'sync'], + 'queue.connections.sync2' => ['driver' => 'sync'], ]); } protected function setUp(): void { - parent::setUp(); + $this->afterApplicationCreated(function () { + JobRunRecorder::reset(); + }); - JobRunRecorder::reset(); - } + $this->beforeApplicationDestroyed(function () { + JobChainingTestFirstJob::$ran = false; + JobChainingTestSecondJob::$ran = false; + JobChainingTestThirdJob::$ran = false; + static::$catchCallbackRan = false; + }); - protected function tearDown(): void - { - JobChainingTestFirstJob::$ran = false; - JobChainingTestSecondJob::$ran = false; - JobChainingTestThirdJob::$ran = false; - static::$catchCallbackRan = false; + parent::setUp(); } public function testJobsCanBeChainedOnSuccess() @@ -55,6 +54,8 @@ public function testJobsCanBeChainedOnSuccess() new JobChainingTestSecondJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } @@ -65,6 +66,8 @@ public function testJobsCanBeChainedOnSuccessUsingPendingChain() new JobChainingTestSecondJob, ])->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } @@ -76,6 +79,8 @@ public function testJobsCanBeChainedOnSuccessUsingBusFacade() new JobChainingTestSecondJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } @@ -87,6 +92,8 @@ public function testJobsCanBeChainedOnSuccessUsingBusFacadeAsArguments() new JobChainingTestSecondJob ); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } @@ -97,6 +104,8 @@ public function testJobsChainedOnExplicitDelete() new JobChainingTestSecondJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestDeletingJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } @@ -108,6 +117,8 @@ public function testJobsCanBeChainedOnSuccessWithSeveralJobs() new JobChainingTestThirdJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); $this->assertTrue(JobChainingTestThirdJob::$ran); @@ -119,45 +130,55 @@ public function testJobsCanBeChainedOnSuccessUsingHelper() new JobChainingTestSecondJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } public function testJobsCanBeChainedViaQueue() { - Queue::connection('sync')->push((new JobChainingTestFirstJob)->chain([ + Queue::push((new JobChainingTestFirstJob)->chain([ new JobChainingTestSecondJob, ])); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(JobChainingTestSecondJob::$ran); } public function testSecondJobIsNotFiredIfFirstFailed() { - Queue::connection('sync')->push((new JobChainingTestFailingJob)->chain([ + Queue::push((new JobChainingTestFailingJob)->chain([ new JobChainingTestSecondJob, ])); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertFalse(JobChainingTestSecondJob::$ran); } public function testSecondJobIsNotFiredIfFirstReleased() { - Queue::connection('sync')->push((new JobChainingTestReleasingJob)->chain([ + Queue::push((new JobChainingTestReleasingJob)->chain([ new JobChainingTestSecondJob, ])); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertFalse(JobChainingTestSecondJob::$ran); } public function testThirdJobIsNotFiredIfSecondFails() { - Queue::connection('sync')->push((new JobChainingTestFirstJob)->chain([ + Queue::push((new JobChainingTestFirstJob)->chain([ new JobChainingTestFailingJob, new JobChainingTestThirdJob, ])); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertFalse(JobChainingTestThirdJob::$ran); } @@ -172,6 +193,8 @@ public function testCatchCallbackIsCalledOnFailure() self::$catchCallbackRan = true; })->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(JobChainingTestFirstJob::$ran); $this->assertTrue(static::$catchCallbackRan); $this->assertFalse(JobChainingTestSecondJob::$ran); @@ -184,6 +207,8 @@ public function testChainJobsUseSameConfig() new JobChainingTestThirdJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertSame('some_queue', JobChainingTestFirstJob::$usedQueue); $this->assertSame('sync1', JobChainingTestFirstJob::$usedConnection); @@ -201,6 +226,8 @@ public function testChainJobsUseOwnConfig() new JobChainingTestThirdJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertSame('some_queue', JobChainingTestFirstJob::$usedQueue); $this->assertSame('sync1', JobChainingTestFirstJob::$usedConnection); @@ -218,6 +245,8 @@ public function testChainJobsUseDefaultConfig() new JobChainingTestThirdJob, ]); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertSame('some_queue', JobChainingTestFirstJob::$usedQueue); $this->assertSame('sync1', JobChainingTestFirstJob::$usedConnection); @@ -232,6 +261,8 @@ public function testChainJobsCanBePrepended() { JobChainAddingPrependingJob::withChain([new JobChainAddingExistingJob])->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertNotNull(JobChainAddingAddedJob::$ranAt); $this->assertNotNull(JobChainAddingExistingJob::$ranAt); $this->assertTrue(JobChainAddingAddedJob::$ranAt->isBefore(JobChainAddingExistingJob::$ranAt)); @@ -241,6 +272,8 @@ public function testChainJobsCanBePrependedWithoutExistingChain() { JobChainAddingPrependingJob::dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertNotNull(JobChainAddingAddedJob::$ranAt); } @@ -248,6 +281,8 @@ public function testChainJobsCanBeAppended() { JobChainAddingAppendingJob::withChain([new JobChainAddingExistingJob])->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertNotNull(JobChainAddingAddedJob::$ranAt); $this->assertNotNull(JobChainAddingExistingJob::$ranAt); $this->assertTrue(JobChainAddingAddedJob::$ranAt->isAfter(JobChainAddingExistingJob::$ranAt)); @@ -257,6 +292,8 @@ public function testChainJobsCanBeAppendedWithoutExistingChain() { JobChainAddingAppendingJob::dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertNotNull(JobChainAddingAddedJob::$ranAt); } @@ -274,6 +311,8 @@ public function testBatchCanBeAddedToChain() new JobChainingNamedTestJob('c3'), ])->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertEquals(['c1', 'c2', 'b1', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); } @@ -291,7 +330,15 @@ public function testDynamicBatchCanBeAddedToChain() new JobChainingNamedTestJob('c3'), ])->dispatch(); - $this->assertEquals(['c1', 'c2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + + if ($this->getQueueDriver() === 'sync') { + $this->assertEquals( + ['c1', 'c2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results + ); + } + + $this->assertCount(11, JobRunRecorder::$results); } public function testChainBatchChain() @@ -312,7 +359,15 @@ public function testChainBatchChain() new JobChainingNamedTestJob('c3'), ])->dispatch(); - $this->assertEquals(['c1', 'c2', 'bc1', 'bc2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + + if ($this->getQueueDriver() === 'sync') { + $this->assertEquals( + ['c1', 'c2', 'bc1', 'bc2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results + ); + } + + $this->assertCount(13, JobRunRecorder::$results); } public function testChainBatchChainBatch() @@ -337,7 +392,15 @@ public function testChainBatchChainBatch() new JobChainingNamedTestJob('c3'), ])->dispatch(); - $this->assertEquals(['c1', 'c2', 'bc1', 'bc2', 'bb1', 'bb2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + + if ($this->getQueueDriver() === 'sync') { + $this->assertEquals( + ['c1', 'c2', 'bc1', 'bc2', 'bb1', 'bb2', 'b1', 'b2-0', 'b2-1', 'b2-2', 'b2-3', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results + ); + } + + $this->assertCount(15, JobRunRecorder::$results); } public function testBatchCatchCallbacks() @@ -351,6 +414,8 @@ public function testBatchCatchCallbacks() new JobChainingNamedTestJob('c3'), ])->catch(fn () => JobRunRecorder::recordFailure('chain failed'))->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertEquals(['c1', 'c2'], JobRunRecorder::$results); $this->assertEquals(['batch failed', 'chain failed'], JobRunRecorder::$failures); } @@ -368,6 +433,8 @@ public function testChainBatchFailureAllowed() new JobChainingNamedTestJob('c3'), ])->catch(fn () => JobRunRecorder::recordFailure('chain failed'))->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertEquals(['c1', 'c2', 'b1', 'b3', 'c3'], JobRunRecorder::$results); // Only the batch failed, but the chain should keep going since the batch allows failures $this->assertEquals(['batch failed'], JobRunRecorder::$failures); @@ -386,6 +453,8 @@ public function testChainBatchFailureNotAllowed() new JobChainingNamedTestJob('c3'), ])->catch(fn () => JobRunRecorder::recordFailure('chain failed'))->dispatch(); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertEquals(['c1', 'c2', 'b1', 'b3'], JobRunRecorder::$results); $this->assertEquals(['batch failed', 'chain failed'], JobRunRecorder::$failures); } diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index 4ea41da42cd1..2fe21725ebb0 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -8,20 +8,27 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Orchestra\Testbench\TestCase; +use Orchestra\Testbench\Attributes\WithMigration; -class JobDispatchingTest extends TestCase +#[WithMigration('queue')] +class JobDispatchingTest extends QueueTestCase { - protected function tearDown(): void + protected function setUp(): void { - Job::$ran = false; - Job::$value = null; + $this->beforeApplicationDestroyed(function () { + Job::$ran = false; + Job::$value = null; + }); + + parent::setUp(); } public function testJobCanUseCustomMethodsAfterDispatch() { Job::dispatch('test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(Job::$ran); $this->assertSame('new-test', Job::$value); } @@ -30,11 +37,15 @@ public function testDispatchesConditionallyWithBoolean() { Job::dispatchIf(false, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertFalse(Job::$ran); $this->assertNull(Job::$value); Job::dispatchIf(true, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(Job::$ran); $this->assertSame('new-test', Job::$value); } @@ -43,10 +54,14 @@ public function testDispatchesConditionallyWithClosure() { Job::dispatchIf(fn ($job) => $job instanceof Job ? 0 : 1, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertFalse(Job::$ran); Job::dispatchIf(fn ($job) => $job instanceof Job ? 1 : 0, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(Job::$ran); } @@ -54,11 +69,15 @@ public function testDoesNotDispatchConditionallyWithBoolean() { Job::dispatchUnless(true, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertFalse(Job::$ran); $this->assertNull(Job::$value); Job::dispatchUnless(false, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(Job::$ran); $this->assertSame('new-test', Job::$value); } @@ -67,10 +86,14 @@ public function testDoesNotDispatchConditionallyWithClosure() { Job::dispatchUnless(fn ($job) => $job instanceof Job ? 1 : 0, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertFalse(Job::$ran); Job::dispatchUnless(fn ($job) => $job instanceof Job ? 0 : 1, 'test')->replaceValue('new-test'); + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + $this->assertTrue(Job::$ran); } diff --git a/tests/Integration/Queue/QueueConnectionTest.php b/tests/Integration/Queue/QueueConnectionTest.php index 397a9e19f6c1..1ae5f1ef9083 100644 --- a/tests/Integration/Queue/QueueConnectionTest.php +++ b/tests/Integration/Queue/QueueConnectionTest.php @@ -23,7 +23,7 @@ protected function tearDown(): void { QueueConnectionTestJob::$ran = false; - m::close(); + parent::tearDown(); } public function testJobWontGetDispatchedInsideATransaction() diff --git a/tests/Integration/Queue/QueueTestCase.php b/tests/Integration/Queue/QueueTestCase.php new file mode 100644 index 000000000000..a885c6a87802 --- /dev/null +++ b/tests/Integration/Queue/QueueTestCase.php @@ -0,0 +1,86 @@ +driver = $app['config']->get('queue.default', 'sync'); + } + + /** + * Run queue worker command. + * + * @param array $options + * @param int $times + * @return void + */ + protected function runQueueWorkerCommand(array $options = [], int $times = 1): void + { + if ($this->getQueueDriver() !== 'sync' && $times > 0) { + $count = 0; + + do { + $this->artisan('queue:work', array_merge($options, [ + '--memory' => 1024, + ]))->assertSuccessful(); + + $count++; + } while ($count < $times); + } + } + + /** + * Mark test as skipped when using given queue drivers. + * + * @param array $drivers + * @return void + */ + protected function markTestSkippedWhenUsingQueueDrivers(array $drivers): void + { + foreach ($drivers as $driver) { + if ($this->getQueueDriver() === $driver) { + $this->markTestSkipped("Unable to use `{$driver}` queue driver for the test"); + } + } + } + + /** + * Mark test as skipped when using "sync" queue driver. + * + * @return void + */ + protected function markTestSkippedWhenUsingSyncQueueDriver(): void + { + $this->markTestSkippedWhenUsingQueueDrivers(['sync']); + } + + /** + * Get the queue driver. + * + * @return string + */ + protected function getQueueDriver(): string + { + return $this->driver; + } +} diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index 80fc594fcd91..f9d90f702854 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -16,13 +16,6 @@ class RateLimitedTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testUnlimitedJobsAreExecuted() { $rateLimiter = $this->app->make(RateLimiter::class); diff --git a/tests/Integration/Queue/RateLimitedWithRedisTest.php b/tests/Integration/Queue/RateLimitedWithRedisTest.php index 768b3c2db9b0..ebd497ce6368 100644 --- a/tests/Integration/Queue/RateLimitedWithRedisTest.php +++ b/tests/Integration/Queue/RateLimitedWithRedisTest.php @@ -29,11 +29,9 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - $this->tearDownRedis(); - m::close(); + parent::tearDown(); } public function testUnlimitedJobsAreExecuted() diff --git a/tests/Queue/RedisQueueIntegrationTest.php b/tests/Integration/Queue/RedisQueueTest.php similarity index 94% rename from tests/Queue/RedisQueueIntegrationTest.php rename to tests/Integration/Queue/RedisQueueTest.php index e7d26522f1e4..74f1dffcec4a 100644 --- a/tests/Queue/RedisQueueIntegrationTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -8,13 +8,14 @@ use Illuminate\Queue\Events\JobQueued; 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; -use PHPUnit\Framework\TestCase; +use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -class RedisQueueIntegrationTest extends TestCase +class RedisQueueTest extends TestCase { use InteractsWithRedis, InteractsWithTime; @@ -37,18 +38,29 @@ protected function setUp(): void protected function tearDown(): void { + $this->tearDownRedis(); + parent::tearDown(); + } - Carbon::setTestNow(null); - $this->tearDownRedis(); - m::close(); + /** + * @param string $driver + * @param string $default + * @param string|null $connection + * @param int $retryAfter + * @param int|null $blockFor + */ + private function setQueue($driver, $default = 'default', $connection = null, $retryAfter = 60, $blockFor = null) + { + $this->queue = new RedisQueue($this->redis[$driver], $default, $connection, $retryAfter, $blockFor); + $this->container = m::spy(Container::class); + $this->queue->setContainer($this->container); } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testExpiredJobsArePopped($driver) { $this->setQueue($driver); @@ -65,8 +77,6 @@ public function testExpiredJobsArePopped($driver) $this->queue->later(-300, $jobs[2]); $this->queue->later(-100, $jobs[3]); - $this->container->shouldHaveReceived('bound')->with('events')->times(4); - $this->assertEquals($jobs[2], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertEquals($jobs[1], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertEquals($jobs[3], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); @@ -77,14 +87,12 @@ public function testExpiredJobsArePopped($driver) } /** - * @dataProvider redisDriverProvider - * - * @requires extension pcntl - * * @param mixed $driver * * @throws \Exception */ + #[RequiresPhpExtension('pcntl')] + #[DataProvider('redisDriverProvider')] public function testBlockingPop($driver) { $this->tearDownRedis(); @@ -105,10 +113,9 @@ public function testBlockingPop($driver) } // /** - // * @dataProvider redisDriverProvider - // * // * @param string $driver // */ + // #[DataProvider('redisDriverProvider')] // public function testMigrateMoreThan100Jobs($driver) // { // $this->setQueue($driver); @@ -122,10 +129,9 @@ public function testBlockingPop($driver) // } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPopProperlyPopsJobOffOfRedis($driver) { $this->setQueue($driver); @@ -157,10 +163,9 @@ public function testPopProperlyPopsJobOffOfRedis($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPopProperlyPopsDelayedJobOffOfRedis($driver) { $this->setQueue($driver); @@ -184,10 +189,9 @@ public function testPopProperlyPopsDelayedJobOffOfRedis($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) { $this->setQueue($driver, 'default', null, null); @@ -214,10 +218,9 @@ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testBlockingPopProperlyPopsJobOffOfRedis($driver) { $this->setQueue($driver, 'default', null, 60, 5); @@ -235,10 +238,9 @@ public function testBlockingPopProperlyPopsJobOffOfRedis($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testBlockingPopProperlyPopsExpiredJobs($driver) { Str::createUuidsUsing(function () { @@ -266,10 +268,9 @@ public function testBlockingPopProperlyPopsExpiredJobs($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testNotExpireJobsWhenExpireNull($driver) { $this->setQueue($driver, 'default', null, null); @@ -315,10 +316,9 @@ public function testNotExpireJobsWhenExpireNull($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testExpireJobsWhenExpireSet($driver) { $this->setQueue($driver, 'default', null, 30); @@ -344,10 +344,9 @@ public function testExpireJobsWhenExpireSet($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testRelease($driver) { $this->setQueue($driver); @@ -385,10 +384,9 @@ public function testRelease($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testReleaseInThePast($driver) { $this->setQueue($driver); @@ -403,10 +401,9 @@ public function testReleaseInThePast($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testDelete($driver) { $this->setQueue($driver); @@ -427,10 +424,9 @@ public function testDelete($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testClear($driver) { $this->setQueue($driver); @@ -447,10 +443,9 @@ public function testClear($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testSize($driver) { $this->setQueue($driver); @@ -468,10 +463,9 @@ public function testSize($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPushJobQueuedEvent($driver) { $events = m::mock(Dispatcher::class); @@ -493,10 +487,9 @@ public function testPushJobQueuedEvent($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testBulkJobQueuedEvent($driver) { $events = m::mock(Dispatcher::class); @@ -515,20 +508,6 @@ public function testBulkJobQueuedEvent($driver) new RedisQueueIntegrationTestJob(15), ]); } - - /** - * @param string $driver - * @param string $default - * @param string|null $connection - * @param int $retryAfter - * @param int|null $blockFor - */ - private function setQueue($driver, $default = 'default', $connection = null, $retryAfter = 60, $blockFor = null) - { - $this->queue = new RedisQueue($this->redis[$driver], $default, $connection, $retryAfter, $blockFor); - $this->container = m::spy(Container::class); - $this->queue->setContainer($this->container); - } } class RedisQueueIntegrationTestJob diff --git a/tests/Integration/Queue/SkipIfBatchCancelledTest.php b/tests/Integration/Queue/SkipIfBatchCancelledTest.php index 91694c0fd444..454d0274903f 100644 --- a/tests/Integration/Queue/SkipIfBatchCancelledTest.php +++ b/tests/Integration/Queue/SkipIfBatchCancelledTest.php @@ -14,13 +14,6 @@ class SkipIfBatchCancelledTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testJobsAreSkippedOnceBatchIsCancelled() { [$beforeCancelled] = (new SkipCancelledBatchableTestJob())->withFakeBatch(); diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index 6eff31a6aabd..40abab630266 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -14,13 +14,6 @@ class ThrottlesExceptionsTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testCircuitIsOpenedForJobErrors() { $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php index 358d0a8b2513..e47d73fb41fd 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -30,13 +30,9 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - $this->tearDownRedis(); - Carbon::setTestNow(); - - m::close(); + parent::tearDown(); } public function testCircuitIsOpenedForJobErrors() diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index eb259a73fb9b..ebcd81c0a171 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -9,22 +9,27 @@ use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Bus; use Orchestra\Testbench\Attributes\WithMigration; -use Orchestra\Testbench\TestCase; +#[WithMigration('cache')] #[WithMigration('queue')] -class UniqueJobTest extends TestCase +class UniqueJobTest extends QueueTestCase { - use DatabaseMigrations; + protected function defineEnvironment($app) + { + parent::defineEnvironment($app); + + $app['config']->set('cache.default', 'database'); + } public function testUniqueJobsAreNotDispatched() { Bus::fake(); UniqueTestJob::dispatch(); + $this->runQueueWorkerCommand(['--once' => true]); Bus::assertDispatched(UniqueTestJob::class); $this->assertFalse( @@ -33,6 +38,7 @@ public function testUniqueJobsAreNotDispatched() Bus::assertDispatchedTimes(UniqueTestJob::class); UniqueTestJob::dispatch(); + $this->runQueueWorkerCommand(['--once' => true]); Bus::assertDispatchedTimes(UniqueTestJob::class); $this->assertFalse( @@ -44,6 +50,7 @@ public function testLockIsReleasedForSuccessfulJobs() { UniqueTestJob::$handled = false; dispatch($job = new UniqueTestJob); + $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue($job::$handled); $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); @@ -56,7 +63,7 @@ public function testLockIsReleasedForFailedJobs() $this->expectException(Exception::class); try { - dispatch($job = new UniqueTestFailJob); + dispatchSync($job = new UniqueTestFailJob); } finally { $this->assertTrue($job::$handled); $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); @@ -65,25 +72,21 @@ public function testLockIsReleasedForFailedJobs() public function testLockIsNotReleasedForJobRetries() { + $this->markTestSkippedWhenUsingSyncQueueDriver(); + UniqueTestRetryJob::$handled = false; dispatch($job = new UniqueTestRetryJob); $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); - $this->artisan('queue:work', [ - 'connection' => 'database', - '--once' => true, - ]); + $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue($job::$handled); $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); UniqueTestRetryJob::$handled = false; - $this->artisan('queue:work', [ - 'connection' => 'database', - '--once' => true, - ]); + $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue($job::$handled); $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); @@ -91,24 +94,20 @@ public function testLockIsNotReleasedForJobRetries() public function testLockIsNotReleasedForJobReleases() { + $this->markTestSkippedWhenUsingSyncQueueDriver(); + UniqueTestReleasedJob::$handled = false; dispatch($job = new UniqueTestReleasedJob); $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); - $this->artisan('queue:work', [ - 'connection' => 'database', - '--once' => true, - ]); + $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue($job::$handled); $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); UniqueTestReleasedJob::$handled = false; - $this->artisan('queue:work', [ - 'connection' => 'database', - '--once' => true, - ]); + $this->runQueueWorkerCommand(['--once' => true]); $this->assertFalse($job::$handled); $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); @@ -116,16 +115,15 @@ public function testLockIsNotReleasedForJobReleases() public function testLockCanBeReleasedBeforeProcessing() { + $this->markTestSkippedWhenUsingSyncQueueDriver(); + UniqueUntilStartTestJob::$handled = false; dispatch($job = new UniqueUntilStartTestJob); $this->assertFalse($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); - $this->artisan('queue:work', [ - 'connection' => 'database', - '--once' => true, - ]); + $this->runQueueWorkerCommand(['--once' => true]); $this->assertTrue($job::$handled); $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); @@ -169,8 +167,6 @@ class UniqueTestReleasedJob extends UniqueTestFailJob { public $tries = 1; - public $connection = 'database'; - public function handle() { static::$handled = true; @@ -182,13 +178,9 @@ public function handle() class UniqueTestRetryJob extends UniqueTestFailJob { public $tries = 2; - - public $connection = 'database'; } class UniqueUntilStartTestJob extends UniqueTestJob implements ShouldBeUniqueUntilProcessing { public $tries = 2; - - public $connection = 'database'; } diff --git a/tests/Integration/Queue/WithoutOverlappingJobsTest.php b/tests/Integration/Queue/WithoutOverlappingJobsTest.php index 049792f750df..98eea03acce6 100644 --- a/tests/Integration/Queue/WithoutOverlappingJobsTest.php +++ b/tests/Integration/Queue/WithoutOverlappingJobsTest.php @@ -11,17 +11,9 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Mockery as m; -use Orchestra\Testbench\TestCase; -class WithoutOverlappingJobsTest extends TestCase +class WithoutOverlappingJobsTest extends QueueTestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testNonOverlappingJobsAreExecuted() { OverlappingTestJob::$handled = false; diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 968b1c5d3548..f9681df5da08 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -7,36 +7,38 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Queue; use Orchestra\Testbench\Attributes\WithMigration; -use Orchestra\Testbench\TestCase; -use Queue; #[WithMigration('queue')] -class WorkCommandTest extends TestCase +class WorkCommandTest extends QueueTestCase { use DatabaseMigrations; - protected function tearDown(): void + protected function setUp(): void { - parent::tearDown(); + $this->beforeApplicationDestroyed(function () { + FirstJob::$ran = false; + SecondJob::$ran = false; + ThirdJob::$ran = false; + }); - FirstJob::$ran = false; - SecondJob::$ran = false; - ThirdJob::$ran = false; + parent::setUp(); + + $this->markTestSkippedWhenUsingSyncQueueDriver(); } public function testRunningOneJob() { - Queue::connection('database')->push(new FirstJob); - Queue::connection('database')->push(new SecondJob); + Queue::push(new FirstJob); + Queue::push(new SecondJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--once' => true, '--memory' => 1024, ])->assertExitCode(0); - $this->assertSame(1, Queue::connection('database')->size()); + $this->assertSame(1, Queue::size()); $this->assertTrue(FirstJob::$ran); $this->assertFalse(SecondJob::$ran); } @@ -45,10 +47,9 @@ public function testRunTimestampOutputWithDefaultAppTimezone() { // queue.output_timezone not set at all $this->travelTo(Carbon::create(2023, 1, 18, 10, 10, 11)); - Queue::connection('database')->push(new FirstJob); + Queue::push(new FirstJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--once' => true, '--memory' => 1024, ])->expectsOutputToContain('2023-01-18 10:10:11') @@ -60,10 +61,9 @@ public function testRunTimestampOutputWithDifferentLogTimezone() $this->app['config']->set('queue.output_timezone', 'Europe/Helsinki'); $this->travelTo(Carbon::create(2023, 1, 18, 10, 10, 11)); - Queue::connection('database')->push(new FirstJob); + Queue::push(new FirstJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--once' => true, '--memory' => 1024, ])->expectsOutputToContain('2023-01-18 12:10:11') @@ -75,10 +75,9 @@ public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault() $this->app['config']->set('queue.output_timezone', 'UTC'); $this->travelTo(Carbon::create(2023, 1, 18, 10, 10, 11)); - Queue::connection('database')->push(new FirstJob); + Queue::push(new FirstJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--once' => true, '--memory' => 1024, ])->expectsOutputToContain('2023-01-18 10:10:11') @@ -87,72 +86,72 @@ public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault() public function testDaemon() { - Queue::connection('database')->push(new FirstJob); - Queue::connection('database')->push(new SecondJob); + Queue::push(new FirstJob); + Queue::push(new SecondJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--daemon' => true, '--stop-when-empty' => true, '--memory' => 1024, ])->assertExitCode(0); - $this->assertSame(0, Queue::connection('database')->size()); + $this->assertSame(0, Queue::size()); $this->assertTrue(FirstJob::$ran); $this->assertTrue(SecondJob::$ran); } public function testMemoryExceeded() { - Queue::connection('database')->push(new FirstJob); - Queue::connection('database')->push(new SecondJob); + Queue::push(new FirstJob); + Queue::push(new SecondJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--daemon' => true, '--stop-when-empty' => true, '--memory' => 0.1, ])->assertExitCode(12); // Memory limit isn't checked until after the first job is attempted. - $this->assertSame(1, Queue::connection('database')->size()); + $this->assertSame(1, Queue::size()); $this->assertTrue(FirstJob::$ran); $this->assertFalse(SecondJob::$ran); } public function testMaxJobsExceeded() { - Queue::connection('database')->push(new FirstJob); - Queue::connection('database')->push(new SecondJob); + $this->markTestSkippedWhenUsingQueueDrivers(['redis', 'beanstalkd']); + + Queue::push(new FirstJob); + Queue::push(new SecondJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--daemon' => true, '--stop-when-empty' => true, '--max-jobs' => 1, ]); // Memory limit isn't checked until after the first job is attempted. - $this->assertSame(1, Queue::connection('database')->size()); + $this->assertSame(1, Queue::size()); $this->assertTrue(FirstJob::$ran); $this->assertFalse(SecondJob::$ran); } public function testMaxTimeExceeded() { - Queue::connection('database')->push(new ThirdJob); - Queue::connection('database')->push(new FirstJob); - Queue::connection('database')->push(new SecondJob); + $this->markTestSkippedWhenUsingQueueDrivers(['redis', 'beanstalkd']); + + Queue::push(new ThirdJob); + Queue::push(new FirstJob); + Queue::push(new SecondJob); $this->artisan('queue:work', [ - 'connection' => 'database', '--daemon' => true, '--stop-when-empty' => true, '--max-time' => 1, ]); // Memory limit isn't checked until after the first job is attempted. - $this->assertSame(2, Queue::connection('database')->size()); + $this->assertSame(2, Queue::size()); $this->assertTrue(ThirdJob::$ran); $this->assertFalse(FirstJob::$ran); $this->assertFalse(SecondJob::$ran); From 02f3144b807b2dcc758f033223042c7d8b8f11ab Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 13 Dec 2023 02:32:58 +0000 Subject: [PATCH 193/207] Apply fixes from StyleCI --- 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 25639b4471f1..9d33842b8071 100644 --- a/src/Illuminate/Validation/Rules/In.php +++ b/src/Illuminate/Validation/Rules/In.php @@ -3,8 +3,8 @@ namespace Illuminate\Validation\Rules; use BackedEnum; -use Stringable; use Illuminate\Contracts\Support\Arrayable; +use Stringable; use UnitEnum; class In implements Stringable diff --git a/src/Illuminate/Validation/Rules/NotIn.php b/src/Illuminate/Validation/Rules/NotIn.php index da0ccf7e1a57..e6b392c60b1b 100644 --- a/src/Illuminate/Validation/Rules/NotIn.php +++ b/src/Illuminate/Validation/Rules/NotIn.php @@ -3,8 +3,8 @@ namespace Illuminate\Validation\Rules; use BackedEnum; -use Stringable; use Illuminate\Contracts\Support\Arrayable; +use Stringable; use UnitEnum; class NotIn implements Stringable From 3e46dc8bce9adb6aadbe036658bb102d6ff288e8 Mon Sep 17 00:00:00 2001 From: driesvints Date: Wed, 13 Dec 2023 08:00:49 +0000 Subject: [PATCH 194/207] Update CHANGELOG --- CHANGELOG.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6feb564990dc..a64288224234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.37.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.36.0...10.x) + +## [v10.36.0](https://github.com/laravel/framework/compare/v10.37.0...v10.36.0) - 2023-12-12 + +* [10.x] Add `engine` method to `Blueprint` by [@jbrooksuk](https://github.com/jbrooksuk) in https://github.com/laravel/framework/pull/49250 +* [10.x] Use translator from validator in `Can` and `Enum` rules by [@fancyweb](https://github.com/fancyweb) in https://github.com/laravel/framework/pull/49251 +* [10.x] Get indexes of a table by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49204 +* [10.x] Filesystem : can lock file on append of content by [@StephaneBour](https://github.com/StephaneBour) in https://github.com/laravel/framework/pull/49262 +* [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49266 +* [10.x] Fixes generating facades documentation shouldn't be affected by `php-psr` extension by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49268 +* [10.x] Fixes `AboutCommand::format()` docblock by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49274 +* [10.x] `Route::getController()` should return `null` when the accessing closure based route by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49269 +* [10.x] Add "noActionOnUpdate" method in Illuminate/Database/Schema/ForeignKeyDefinition by [@hrsa](https://github.com/hrsa) in https://github.com/laravel/framework/pull/49297 +* [10.x] Fixing number helper for floating 0.0 by [@mr-punyapal](https://github.com/mr-punyapal) in https://github.com/laravel/framework/pull/49277 +* [10.x] Allow checking if lock succesfully restored by [@Joostb](https://github.com/Joostb) in https://github.com/laravel/framework/pull/49272 +* [10.x] Enable DynamoDB as a backend for Job Batches by [@khepin](https://github.com/khepin) in https://github.com/laravel/framework/pull/49169 +* [10.x] Removed deprecated and not used argument by [@Muetze42](https://github.com/Muetze42) in https://github.com/laravel/framework/pull/49304 +* [10.x] Add Conditionable to Batched and Chained jobs by [@bretto36](https://github.com/bretto36) in https://github.com/laravel/framework/pull/49310 +* [10.x] Include partitioned tables on PostgreSQL when retrieving tables by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49326 +* [10.x] Allow to pass `Arrayable` or `Stringble` in rules `In` and `NotIn` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/49055 +* [10.x] Display error message if json_encode() fails by [@aimeos](https://github.com/aimeos) in https://github.com/laravel/framework/pull/48856 +* [10.x] Allow error list per field by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/49309 +* [10.x] Get foreign keys of a table by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49264 +* [10.x] PHPStan Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49343 +* [10.x] Handle missing translations: more robust handling of callback return value by [@DeanWunder](https://github.com/DeanWunder) in https://github.com/laravel/framework/pull/49341 ## [v10.37.0](https://github.com/laravel/framework/compare/v10.35.0...v10.37.0) - 2023-12-12 From 605602f6a7ce9b76c7140df1a112cb5e6f2a9bea Mon Sep 17 00:00:00 2001 From: driesvints Date: Wed, 13 Dec 2023 08:01:14 +0000 Subject: [PATCH 195/207] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a64288224234..fa2cfb56c9cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.36.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.37.1...10.x) + +## [v10.37.1](https://github.com/laravel/framework/compare/v10.36.0...v10.37.1) - 2023-12-12 + +* [10.x] Disconnecting the database connection after testing by [@KentarouTakeda](https://github.com/KentarouTakeda) in https://github.com/laravel/framework/pull/49327 +* [10.x] Get user-defined types on PostgreSQL by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49303 ## [v10.36.0](https://github.com/laravel/framework/compare/v10.37.0...v10.36.0) - 2023-12-12 From 3e9301d2e34e07aab56fc500b4ced970e3d27d34 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Wed, 13 Dec 2023 09:02:36 +0100 Subject: [PATCH 196/207] Update CHANGELOG.md --- CHANGELOG.md | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2cfb56c9cf..3338f0d5ac85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,35 +2,11 @@ ## [Unreleased](https://github.com/laravel/framework/compare/v10.37.1...10.x) -## [v10.37.1](https://github.com/laravel/framework/compare/v10.36.0...v10.37.1) - 2023-12-12 +## [v10.37.1](https://github.com/laravel/framework/compare/v10.37.0...v10.37.1) - 2023-12-12 * [10.x] Disconnecting the database connection after testing by [@KentarouTakeda](https://github.com/KentarouTakeda) in https://github.com/laravel/framework/pull/49327 * [10.x] Get user-defined types on PostgreSQL by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49303 -## [v10.36.0](https://github.com/laravel/framework/compare/v10.37.0...v10.36.0) - 2023-12-12 - -* [10.x] Add `engine` method to `Blueprint` by [@jbrooksuk](https://github.com/jbrooksuk) in https://github.com/laravel/framework/pull/49250 -* [10.x] Use translator from validator in `Can` and `Enum` rules by [@fancyweb](https://github.com/fancyweb) in https://github.com/laravel/framework/pull/49251 -* [10.x] Get indexes of a table by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49204 -* [10.x] Filesystem : can lock file on append of content by [@StephaneBour](https://github.com/StephaneBour) in https://github.com/laravel/framework/pull/49262 -* [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49266 -* [10.x] Fixes generating facades documentation shouldn't be affected by `php-psr` extension by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49268 -* [10.x] Fixes `AboutCommand::format()` docblock by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49274 -* [10.x] `Route::getController()` should return `null` when the accessing closure based route by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49269 -* [10.x] Add "noActionOnUpdate" method in Illuminate/Database/Schema/ForeignKeyDefinition by [@hrsa](https://github.com/hrsa) in https://github.com/laravel/framework/pull/49297 -* [10.x] Fixing number helper for floating 0.0 by [@mr-punyapal](https://github.com/mr-punyapal) in https://github.com/laravel/framework/pull/49277 -* [10.x] Allow checking if lock succesfully restored by [@Joostb](https://github.com/Joostb) in https://github.com/laravel/framework/pull/49272 -* [10.x] Enable DynamoDB as a backend for Job Batches by [@khepin](https://github.com/khepin) in https://github.com/laravel/framework/pull/49169 -* [10.x] Removed deprecated and not used argument by [@Muetze42](https://github.com/Muetze42) in https://github.com/laravel/framework/pull/49304 -* [10.x] Add Conditionable to Batched and Chained jobs by [@bretto36](https://github.com/bretto36) in https://github.com/laravel/framework/pull/49310 -* [10.x] Include partitioned tables on PostgreSQL when retrieving tables by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49326 -* [10.x] Allow to pass `Arrayable` or `Stringble` in rules `In` and `NotIn` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/49055 -* [10.x] Display error message if json_encode() fails by [@aimeos](https://github.com/aimeos) in https://github.com/laravel/framework/pull/48856 -* [10.x] Allow error list per field by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/49309 -* [10.x] Get foreign keys of a table by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49264 -* [10.x] PHPStan Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49343 -* [10.x] Handle missing translations: more robust handling of callback return value by [@DeanWunder](https://github.com/DeanWunder) in https://github.com/laravel/framework/pull/49341 - ## [v10.37.0](https://github.com/laravel/framework/compare/v10.35.0...v10.37.0) - 2023-12-12 * [10.x] Add `engine` method to `Blueprint` by [@jbrooksuk](https://github.com/jbrooksuk) in https://github.com/laravel/framework/pull/49250 From b4206275818747ad4a7289b2b0dd398f510ae5c9 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 13 Dec 2023 16:36:10 +0800 Subject: [PATCH 197/207] [11.x] Test Improvements (#49352) * [11.x] Test Improvements Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki --- .github/workflows/queues.yml | 8 ++++---- tests/Integration/Queue/JobChainingTest.php | 1 + tests/Integration/Queue/JobDispatchingTest.php | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 68e41cfa2aca..dbe5c2bcc1b4 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -25,7 +25,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none @@ -59,7 +59,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none @@ -101,7 +101,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none @@ -139,7 +139,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, :php-psr tools: composer:v2 coverage: none diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 7037625b62bf..27674d1e4709 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -15,6 +15,7 @@ use Illuminate\Support\Facades\Queue; use Orchestra\Testbench\Attributes\WithMigration; +#[WithMigration] #[WithMigration('queue')] class JobChainingTest extends QueueTestCase { diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index 6867086392a3..6b5f4928538f 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -10,6 +10,7 @@ use Illuminate\Queue\InteractsWithQueue; use Orchestra\Testbench\Attributes\WithMigration; +#[WithMigration] #[WithMigration('queue')] class JobDispatchingTest extends QueueTestCase { From 94634fe7127449ba438b6fd62cbcc31fa598e603 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 13 Dec 2023 20:12:07 +0800 Subject: [PATCH 198/207] [10.x] Avoid using `rescue()` in standalone `illuminate/database` component. (#49355) fixes laravel/framework#49354 Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Database/Schema/SQLiteBuilder.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index e2cae2c07c9d..8ae272d767b6 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Schema; +use Illuminate\Database\QueryException; use Illuminate\Support\Facades\File; class SQLiteBuilder extends Builder @@ -37,7 +38,13 @@ public function dropDatabaseIfExists($name) */ public function getTables() { - $withSize = rescue(fn () => $this->connection->scalar($this->grammar->compileDbstatExists()), false, false); + $withSize = false; + + try { + $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); + } catch (QueryException $e) { + // + } return $this->connection->getPostProcessor()->processTables( $this->connection->selectFromWriteConnection($this->grammar->compileTables($withSize)) From f6777af6a1256ba61b51a95e8923fbbc6f647a47 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 13 Dec 2023 15:42:40 +0330 Subject: [PATCH 199/207] [10.x] Exclude extension types on PostgreSQL when retrieving types (#49358) * exclude extension types * also exclude implicit extension types --- src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 4d5fb7ba1312..688d4acfac1f 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -115,6 +115,7 @@ public function compileTypes() .'left join pg_type el on el.oid = t.typelem ' .'left join pg_class ce on ce.oid = el.typrelid ' ."where ((t.typrelid = 0 and (ce.relkind = 'c' or ce.relkind is null)) or c.relkind = 'c') " + ."and not exists (select 1 from pg_depend d where d.objid in (t.oid, t.typelem) and d.deptype = 'e') " ."and n.nspname not in ('pg_catalog', 'information_schema')"; } From c13d4c8895206e3a83cdbbe44b6680165bf3a9f3 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Wed, 13 Dec 2023 13:12:51 +0100 Subject: [PATCH 200/207] Revert "Disconnecting the database connection after testing (#49327)" (#49361) This reverts commit 2f65457e6fc4f17c751fadc71bd2ca93e593cf28. --- src/Illuminate/Foundation/Testing/DatabaseTransactions.php | 2 +- src/Illuminate/Foundation/Testing/RefreshDatabase.php | 2 +- src/Illuminate/Foundation/Testing/TestCase.php | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php index c6fb714ada4b..83a686f3558c 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php @@ -33,7 +33,7 @@ public function beginDatabaseTransaction() $connection->unsetEventDispatcher(); $connection->rollBack(); $connection->setEventDispatcher($dispatcher); - $database->purge($name); + $connection->disconnect(); } }); } diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabase.php b/src/Illuminate/Foundation/Testing/RefreshDatabase.php index c68b19bbd54d..0f916ac55b51 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabase.php @@ -109,7 +109,7 @@ public function beginDatabaseTransaction() $connection->unsetEventDispatcher(); $connection->rollBack(); $connection->setEventDispatcher($dispatcher); - $database->purge($name); + $connection->disconnect(); } }); } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index e38e65430a08..a7315a2c5124 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -197,12 +197,6 @@ protected function tearDown(): void ParallelTesting::callTearDownTestCaseCallbacks($this); - $database = $this->app['db'] ?? null; - - foreach (array_keys($database?->getConnections() ?? []) as $name) { - $database->purge($name); - } - $this->app->flush(); $this->app = null; From d97b5263ccd2f3950fd93f45b1b957259d76d0db Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 13 Dec 2023 08:12:36 -0600 Subject: [PATCH 201/207] patch --- 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 049585052154..2142220f8443 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.37.1'; + const VERSION = '10.37.2'; /** * The base path for the Laravel installation. From 2b7a626d2f8d68a706af32bfaa33c706d44986c7 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 13 Dec 2023 22:52:07 +0800 Subject: [PATCH 202/207] [11.x] Pheanstalk 5 (#49351) * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * [11.x] Test Improvements 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 * Apply fixes from StyleCI * 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 Co-authored-by: StyleCI Bot --- composer.json | 2 +- src/Illuminate/Queue/BeanstalkdQueue.php | 35 ++++++++++------- .../Queue/Connectors/BeanstalkdConnector.php | 7 ++-- src/Illuminate/Queue/Jobs/BeanstalkdJob.php | 14 +++---- tests/Integration/Queue/DynamoBatchTest.php | 13 +++---- .../Queue/DynamoBatchTestWithTTL.php | 10 ++--- tests/Queue/QueueBeanstalkdJobTest.php | 9 +++-- tests/Queue/QueueBeanstalkdQueueTest.php | 38 +++++++++++++------ 8 files changed, 75 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index 84c53c70428d..8b4bd32294e8 100644 --- a/composer.json +++ b/composer.json @@ -107,7 +107,7 @@ "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", "orchestra/testbench-core": "^9.0", - "pda/pheanstalk": "^4.0", + "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.1", "predis/predis": "^2.0.2", diff --git a/src/Illuminate/Queue/BeanstalkdQueue.php b/src/Illuminate/Queue/BeanstalkdQueue.php index 3f1e0a5d6a96..f6f6a71ee2d5 100755 --- a/src/Illuminate/Queue/BeanstalkdQueue.php +++ b/src/Illuminate/Queue/BeanstalkdQueue.php @@ -4,15 +4,18 @@ use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Jobs\BeanstalkdJob; -use Pheanstalk\Job as PheanstalkJob; +use Pheanstalk\Contract\JobIdInterface; use Pheanstalk\Pheanstalk; +use Pheanstalk\Values\Job; +use Pheanstalk\Values\JobId; +use Pheanstalk\Values\TubeName; class BeanstalkdQueue extends Queue implements QueueContract { /** * The Pheanstalk instance. * - * @var \Pheanstalk\Pheanstalk + * @var \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface */ protected $pheanstalk; @@ -40,14 +43,14 @@ class BeanstalkdQueue extends Queue implements QueueContract /** * Create a new Beanstalkd queue instance. * - * @param \Pheanstalk\Pheanstalk $pheanstalk + * @param \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface $pheanstalk * @param string $default * @param int $timeToRun * @param int $blockFor * @param bool $dispatchAfterCommit * @return void */ - public function __construct(Pheanstalk $pheanstalk, + public function __construct($pheanstalk, $default, $timeToRun, $blockFor = 0, @@ -68,9 +71,7 @@ public function __construct(Pheanstalk $pheanstalk, */ public function size($queue = null) { - $queue = $this->getQueue($queue); - - return (int) $this->pheanstalk->statsTube($queue)->current_jobs_ready; + return (int) $this->pheanstalk->statsTube(new TubeName($this->getQueue($queue)))->currentJobsReady; } /** @@ -104,7 +105,9 @@ function ($payload, $queue) { */ public function pushRaw($payload, $queue = null, array $options = []) { - return $this->pheanstalk->useTube($this->getQueue($queue))->put( + $this->pheanstalk->useTube(new TubeName($this->getQueue($queue))); + + return $this->pheanstalk->put( $payload, Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY, $this->timeToRun ); } @@ -126,7 +129,9 @@ public function later($delay, $job, $data = '', $queue = null) $queue, $delay, function ($payload, $queue, $delay) { - return $this->pheanstalk->useTube($this->getQueue($queue))->put( + $this->pheanstalk->useTube(new TubeName($this->getQueue($queue))); + + return $this->pheanstalk->put( $payload, Pheanstalk::DEFAULT_PRIORITY, $this->secondsUntil($delay), @@ -165,9 +170,11 @@ public function pop($queue = null) { $queue = $this->getQueue($queue); - $job = $this->pheanstalk->watchOnly($queue)->reserveWithTimeout($this->blockFor); + $this->pheanstalk->watch(new TubeName($queue)); + + $job = $this->pheanstalk->reserveWithTimeout($this->blockFor); - if ($job instanceof PheanstalkJob) { + if ($job instanceof JobIdInterface) { return new BeanstalkdJob( $this->container, $this->pheanstalk, $job, $this->connectionName, $queue ); @@ -183,9 +190,9 @@ public function pop($queue = null) */ public function deleteMessage($queue, $id) { - $queue = $this->getQueue($queue); + $this->pheanstalk->useTube(new TubeName($this->getQueue($queue))); - $this->pheanstalk->useTube($queue)->delete(new PheanstalkJob($id, '')); + $this->pheanstalk->delete(new Job(new JobId($id), '')); } /** @@ -202,7 +209,7 @@ public function getQueue($queue) /** * Get the underlying Pheanstalk instance. * - * @return \Pheanstalk\Pheanstalk + * @return \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface */ public function getPheanstalk() { diff --git a/src/Illuminate/Queue/Connectors/BeanstalkdConnector.php b/src/Illuminate/Queue/Connectors/BeanstalkdConnector.php index fdcdb355594e..6286bed7c1cc 100755 --- a/src/Illuminate/Queue/Connectors/BeanstalkdConnector.php +++ b/src/Illuminate/Queue/Connectors/BeanstalkdConnector.php @@ -3,8 +3,9 @@ namespace Illuminate\Queue\Connectors; use Illuminate\Queue\BeanstalkdQueue; -use Pheanstalk\Connection; +use Pheanstalk\Contract\SocketFactoryInterface; use Pheanstalk\Pheanstalk; +use Pheanstalk\Values\Timeout; class BeanstalkdConnector implements ConnectorInterface { @@ -35,8 +36,8 @@ protected function pheanstalk(array $config) { return Pheanstalk::create( $config['host'], - $config['port'] ?? Pheanstalk::DEFAULT_PORT, - $config['timeout'] ?? Connection::DEFAULT_CONNECT_TIMEOUT + $config['port'] ?? SocketFactoryInterface::DEFAULT_PORT, + isset($config['timeout']) ? new Timeout($config['timeout']) : null, ); } } diff --git a/src/Illuminate/Queue/Jobs/BeanstalkdJob.php b/src/Illuminate/Queue/Jobs/BeanstalkdJob.php index 21c76a98013c..10d0b3627c1d 100755 --- a/src/Illuminate/Queue/Jobs/BeanstalkdJob.php +++ b/src/Illuminate/Queue/Jobs/BeanstalkdJob.php @@ -4,7 +4,7 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Queue\Job as JobContract; -use Pheanstalk\Job as PheanstalkJob; +use Pheanstalk\Contract\JobIdInterface; use Pheanstalk\Pheanstalk; class BeanstalkdJob extends Job implements JobContract @@ -12,7 +12,7 @@ class BeanstalkdJob extends Job implements JobContract /** * The Pheanstalk instance. * - * @var \Pheanstalk\Pheanstalk + * @var \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface */ protected $pheanstalk; @@ -27,13 +27,13 @@ class BeanstalkdJob extends Job implements JobContract * Create a new job instance. * * @param \Illuminate\Container\Container $container - * @param \Pheanstalk\Pheanstalk $pheanstalk - * @param \Pheanstalk\Job $job + * @param \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface $pheanstalk + * @param \Pheanstalk\Contract\JobIdInterface $job * @param string $connectionName * @param string $queue * @return void */ - public function __construct(Container $container, Pheanstalk $pheanstalk, PheanstalkJob $job, $connectionName, $queue) + public function __construct(Container $container, $pheanstalk, JobIdInterface $job, $connectionName, $queue) { $this->job = $job; $this->queue = $queue; @@ -116,7 +116,7 @@ public function getRawBody() /** * Get the underlying Pheanstalk instance. * - * @return \Pheanstalk\Pheanstalk + * @return \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface */ public function getPheanstalk() { @@ -126,7 +126,7 @@ public function getPheanstalk() /** * Get the underlying Pheanstalk job. * - * @return \Pheanstalk\Job + * @return \Pheanstalk\Contract\JobIdInterface */ public function getPheanstalkJob() { diff --git a/tests/Integration/Queue/DynamoBatchTest.php b/tests/Integration/Queue/DynamoBatchTest.php index 76a861d7468f..a395fc414af0 100644 --- a/tests/Integration/Queue/DynamoBatchTest.php +++ b/tests/Integration/Queue/DynamoBatchTest.php @@ -11,11 +11,12 @@ use Illuminate\Support\Env; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Str; +use Orchestra\Testbench\Attributes\RequiresEnv; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; -/** - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresEnv('DYNAMODB_ENDPOINT')] class DynamoBatchTest extends TestCase { public function setUp(): void @@ -34,14 +35,10 @@ public function setUp(): void protected function defineEnvironment($app) { - if (is_null($endpoint = Env::get('DYNAMODB_ENDPOINT'))) { - $this->markTestSkipped('Require `dynamodb` to be configured'); - } - $app['config']->set('queue.batching', [ 'driver' => 'dynamodb', 'region' => 'us-west-2', - 'endpoint' => $endpoint, + 'endpoint' => Env::get('DYNAMODB_ENDPOINT'), 'key' => 'key', 'secret' => 'secret', ]); diff --git a/tests/Integration/Queue/DynamoBatchTestWithTTL.php b/tests/Integration/Queue/DynamoBatchTestWithTTL.php index 9aa516737c73..0a4977867171 100644 --- a/tests/Integration/Queue/DynamoBatchTestWithTTL.php +++ b/tests/Integration/Queue/DynamoBatchTestWithTTL.php @@ -3,19 +3,19 @@ namespace Illuminate\Tests\Integration\Queue; use Illuminate\Support\Env; +use Orchestra\Testbench\Attributes\RequiresEnv; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresEnv('DYNAMODB_ENDPOINT')] class DynamoBatchTestWithTTL extends DynamoBatchTest { protected function defineEnvironment($app) { - if (is_null($endpoint = Env::get('DYNAMODB_ENDPOINT'))) { - $this->markTestSkipped('Require `dynamodb` to be configured'); - } - $app['config']->set('queue.batching', [ 'driver' => 'dynamodb', 'region' => 'us-west-2', - 'endpoint' => $endpoint, + 'endpoint' => Env::get('DYNAMODB_ENDPOINT'), 'key' => 'key', 'secret' => 'secret', 'ttl' => 1, diff --git a/tests/Queue/QueueBeanstalkdJobTest.php b/tests/Queue/QueueBeanstalkdJobTest.php index ec9bd34277c4..514555001e67 100755 --- a/tests/Queue/QueueBeanstalkdJobTest.php +++ b/tests/Queue/QueueBeanstalkdJobTest.php @@ -8,7 +8,10 @@ use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Jobs\BeanstalkdJob; use Mockery as m; -use Pheanstalk\Job; +use Pheanstalk\Contract\JobIdInterface; +use Pheanstalk\Contract\PheanstalkManagerInterface; +use Pheanstalk\Contract\PheanstalkPublisherInterface; +use Pheanstalk\Contract\PheanstalkSubscriberInterface; use Pheanstalk\Pheanstalk; use PHPUnit\Framework\TestCase; use stdClass; @@ -71,8 +74,8 @@ protected function getJob() { return new BeanstalkdJob( m::mock(Container::class), - m::mock(Pheanstalk::class), - m::mock(Job::class), + m::mock(implode(',', [PheanstalkManagerInterface::class, PheanstalkPublisherInterface::class, PheanstalkSubscriberInterface::class])), + m::mock(JobIdInterface::class), 'connection-name', 'default' ); diff --git a/tests/Queue/QueueBeanstalkdQueueTest.php b/tests/Queue/QueueBeanstalkdQueueTest.php index ed4e6f904e20..d71afeec8da4 100755 --- a/tests/Queue/QueueBeanstalkdQueueTest.php +++ b/tests/Queue/QueueBeanstalkdQueueTest.php @@ -7,8 +7,13 @@ use Illuminate\Queue\Jobs\BeanstalkdJob; use Illuminate\Support\Str; use Mockery as m; -use Pheanstalk\Job; +use Pheanstalk\Contract\JobIdInterface; +use Pheanstalk\Contract\PheanstalkManagerInterface; +use Pheanstalk\Contract\PheanstalkPublisherInterface; +use Pheanstalk\Contract\PheanstalkSubscriberInterface; use Pheanstalk\Pheanstalk; +use Pheanstalk\Values\Job; +use Pheanstalk\Values\TubeName; use PHPUnit\Framework\TestCase; class QueueBeanstalkdQueueTest extends TestCase @@ -38,8 +43,8 @@ public function testPushProperlyPushesJobOntoBeanstalkd() $this->setQueue('default', 60); $pheanstalk = $this->queue->getPheanstalk(); - $pheanstalk->shouldReceive('useTube')->once()->with('stack')->andReturn($pheanstalk); - $pheanstalk->shouldReceive('useTube')->once()->with('default')->andReturn($pheanstalk); + $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); + $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), 1024, 0, 60); $this->queue->push('foo', ['data'], 'stack'); @@ -60,8 +65,8 @@ public function testDelayedPushProperlyPushesJobOntoBeanstalkd() $this->setQueue('default', 60); $pheanstalk = $this->queue->getPheanstalk(); - $pheanstalk->shouldReceive('useTube')->once()->with('stack')->andReturn($pheanstalk); - $pheanstalk->shouldReceive('useTube')->once()->with('default')->andReturn($pheanstalk); + $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); + $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR); $this->queue->later(5, 'foo', ['data'], 'stack'); @@ -77,8 +82,10 @@ public function testPopProperlyPopsJobOffOfBeanstalkd() $this->setQueue('default', 60); $pheanstalk = $this->queue->getPheanstalk(); - $pheanstalk->shouldReceive('watchOnly')->once()->with('default')->andReturn($pheanstalk); - $job = m::mock(Job::class); + $pheanstalk->shouldReceive('watch')->once()->with(m::type(TubeName::class)); + $jobId = m::mock(JobIdInterface::class); + $jobId->shouldReceive('getId')->once(); + $job = new Job($jobId, ''); $pheanstalk->shouldReceive('reserveWithTimeout')->once()->with(0)->andReturn($job); $result = $this->queue->pop(); @@ -91,8 +98,10 @@ public function testBlockingPopProperlyPopsJobOffOfBeanstalkd() $this->setQueue('default', 60, 60); $pheanstalk = $this->queue->getPheanstalk(); - $pheanstalk->shouldReceive('watchOnly')->once()->with('default')->andReturn($pheanstalk); - $job = m::mock(Job::class); + $pheanstalk->shouldReceive('watch')->once()->with(m::type(TubeName::class)); + $jobId = m::mock(JobIdInterface::class); + $jobId->shouldReceive('getId')->once(); + $job = new Job($jobId, ''); $pheanstalk->shouldReceive('reserveWithTimeout')->once()->with(60)->andReturn($job); $result = $this->queue->pop(); @@ -105,8 +114,8 @@ public function testDeleteProperlyRemoveJobsOffBeanstalkd() $this->setQueue('default', 60); $pheanstalk = $this->queue->getPheanstalk(); - $pheanstalk->shouldReceive('useTube')->once()->with('default')->andReturn($pheanstalk); - $pheanstalk->shouldReceive('delete')->once()->with(m::type(Job::class)); + $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class))->andReturn($pheanstalk); + $pheanstalk->shouldReceive('delete')->once()->with(m::type(JobIdInterface::class)); $this->queue->deleteMessage('default', 1); } @@ -118,7 +127,12 @@ public function testDeleteProperlyRemoveJobsOffBeanstalkd() */ private function setQueue($default, $timeToRun, $blockFor = 0) { - $this->queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), $default, $timeToRun, $blockFor); + $this->queue = new BeanstalkdQueue( + m::mock(implode(',', [PheanstalkManagerInterface::class, PheanstalkPublisherInterface::class, PheanstalkSubscriberInterface::class])), + $default, + $timeToRun, + $blockFor + ); $this->container = m::spy(Container::class); $this->queue->setContainer($this->container); } From bb49a72c1a839b2b19d0fcea4e8b203a122454ef Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 13 Dec 2023 09:07:28 -0600 Subject: [PATCH 203/207] flush middleware callbacks --- .../Http/Middleware/ConvertEmptyStringsToNull.php | 10 ++++++++++ .../Foundation/Http/Middleware/TrimStrings.php | 10 ++++++++++ src/Illuminate/Foundation/Testing/TestCase.php | 6 +++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php b/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php index 3fb0ee22c19d..89707d7e4dd6 100644 --- a/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php +++ b/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php @@ -53,4 +53,14 @@ public static function skipWhen(Closure $callback) { static::$skipCallbacks[] = $callback; } + + /** + * Flush the middleware's global state. + * + * @return void + */ + public static function flushState() + { + static::$skipCallbacks = []; + } } diff --git a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php index 7ffcd5eaa1f7..3b7c6c4ff268 100644 --- a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php +++ b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php @@ -66,4 +66,14 @@ public static function skipWhen(Closure $callback) { static::$skipCallbacks[] = $callback; } + + /** + * Flush the middleware's global state. + * + * @return void + */ + public static function flushState() + { + static::$skipCallbacks = []; + } } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index a7315a2c5124..b9b76bf2a50c 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -6,6 +6,8 @@ use Illuminate\Console\Application as Artisan; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bootstrap\HandleExceptions; +use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; +use Illuminate\Foundation\Http\Middleware\TrimStrings; use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Facade; @@ -244,9 +246,11 @@ protected function tearDown(): void Component::flushCache(); Component::forgetComponentsResolver(); Component::forgetFactory(); - Queue::createPayloadUsing(null); + ConvertEmptyStringsToNull::flushState(); HandleExceptions::forgetApp(); + Queue::createPayloadUsing(null); Sleep::fake(false); + TrimStrings::flushState(); if ($this->callbackException) { throw $this->callbackException; From 562557fa72539095aa02a1a0a9f866c0f8486f62 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Wed, 13 Dec 2023 17:06:21 +0000 Subject: [PATCH 204/207] [11.x] Fix pluck() casting with qualified column (#49288) * [10.x]: Fix pluck() casting with qualified column * Fix failing test * Update EloquentBelongsToManyTest.php * Update tests/Integration/Database/EloquentBelongsToManyTest.php * Update Builder.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Eloquent/Builder.php | 2 + .../Database/DatabaseEloquentBuilderTest.php | 48 +++++++++++++++++++ .../Database/EloquentBelongsToManyTest.php | 8 +--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 8a230c0146fb..7a87d1524b15 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -888,6 +888,8 @@ public function pluck($column, $key = null) $column = $column instanceof Expression ? $column->getValue($this->getGrammar()) : $column; + $column = Str::after($column, "{$this->model->getTable()}."); + // If the model has a mutator for the requested column, we will spin through // the results and mutate the values so that the mutated version of these // columns are returned as you would expect from these Eloquent models. diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 31354f2c1ab0..21b074811ce5 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -659,6 +659,54 @@ public function testPluckReturnsTheDateAttributesOfAModel() $this->assertEquals(['date_2010-01-01 00:00:00', 'date_2011-01-01 00:00:00'], $builder->pluck('created_at')->all()); } + public function testQualifiedPluckReturnsTheMutatedAttributesOfAModel() + { + $model = $this->getMockModel(); + $model->shouldReceive('qualifyColumn')->with('name')->andReturn('foo_table.name'); + + $builder = $this->getBuilder(); + $builder->getQuery()->shouldReceive('pluck')->with($model->qualifyColumn('name'), '')->andReturn(new BaseCollection(['bar', 'baz'])); + $builder->setModel($model); + $builder->getModel()->shouldReceive('hasGetMutator')->with('name')->andReturn(true); + $builder->getModel()->shouldReceive('newFromBuilder')->with(['name' => 'bar'])->andReturn(new EloquentBuilderTestPluckStub(['name' => 'bar'])); + $builder->getModel()->shouldReceive('newFromBuilder')->with(['name' => 'baz'])->andReturn(new EloquentBuilderTestPluckStub(['name' => 'baz'])); + + $this->assertEquals(['foo_bar', 'foo_baz'], $builder->pluck($model->qualifyColumn('name'))->all()); + } + + public function testQualifiedPluckReturnsTheCastedAttributesOfAModel() + { + $model = $this->getMockModel(); + $model->shouldReceive('qualifyColumn')->with('name')->andReturn('foo_table.name'); + + $builder = $this->getBuilder(); + $builder->getQuery()->shouldReceive('pluck')->with($model->qualifyColumn('name'), '')->andReturn(new BaseCollection(['bar', 'baz'])); + $builder->setModel($model); + $builder->getModel()->shouldReceive('hasGetMutator')->with('name')->andReturn(false); + $builder->getModel()->shouldReceive('hasCast')->with('name')->andReturn(true); + $builder->getModel()->shouldReceive('newFromBuilder')->with(['name' => 'bar'])->andReturn(new EloquentBuilderTestPluckStub(['name' => 'bar'])); + $builder->getModel()->shouldReceive('newFromBuilder')->with(['name' => 'baz'])->andReturn(new EloquentBuilderTestPluckStub(['name' => 'baz'])); + + $this->assertEquals(['foo_bar', 'foo_baz'], $builder->pluck($model->qualifyColumn('name'))->all()); + } + + public function testQualifiedPluckReturnsTheDateAttributesOfAModel() + { + $model = $this->getMockModel(); + $model->shouldReceive('qualifyColumn')->with('created_at')->andReturn('foo_table.created_at'); + + $builder = $this->getBuilder(); + $builder->getQuery()->shouldReceive('pluck')->with($model->qualifyColumn('created_at'), '')->andReturn(new BaseCollection(['2010-01-01 00:00:00', '2011-01-01 00:00:00'])); + $builder->setModel($model); + $builder->getModel()->shouldReceive('hasGetMutator')->with('created_at')->andReturn(false); + $builder->getModel()->shouldReceive('hasCast')->with('created_at')->andReturn(false); + $builder->getModel()->shouldReceive('getDates')->andReturn(['created_at']); + $builder->getModel()->shouldReceive('newFromBuilder')->with(['created_at' => '2010-01-01 00:00:00'])->andReturn(new EloquentBuilderTestPluckDatesStub(['created_at' => '2010-01-01 00:00:00'])); + $builder->getModel()->shouldReceive('newFromBuilder')->with(['created_at' => '2011-01-01 00:00:00'])->andReturn(new EloquentBuilderTestPluckDatesStub(['created_at' => '2011-01-01 00:00:00'])); + + $this->assertEquals(['date_2010-01-01 00:00:00', 'date_2011-01-01 00:00:00'], $builder->pluck($model->qualifyColumn('created_at'))->all()); + } + public function testPluckWithoutModelGetterJustReturnsTheAttributesFoundInDatabase() { $builder = $this->getBuilder(); diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index 161fd4908376..7f77db612656 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -931,14 +931,10 @@ public function testCanTouchRelatedModels() $post->tags()->touch(); foreach ($post->tags()->pluck('tags.updated_at') as $date) { - if ($this->driver === 'sqlsrv') { - $this->assertSame('2017-10-10 10:10:10.000', $date); - } else { - $this->assertSame('2017-10-10 10:10:10', $date); - } + $this->assertSame('2017-10-10 10:10:10', $date->toDateTimeString()); } - $this->assertNotSame('2017-10-10 10:10:10', Tag::find(2)->updated_at); + $this->assertNotSame('2017-10-10 10:10:10', Tag::find(2)->updated_at?->toDateTimeString()); } public function testWherePivotOnString() From 889afc0a8898f2dcc3388f9cef4874a87a7c8bfa Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 13 Dec 2023 21:59:01 +0330 Subject: [PATCH 205/207] [11.x] Fix inconsistent database parsing on PostgreSQL (#49148) * fix inconsistent database parsing on PostgreSQL * fix on getIndexes * fix on getForeignKeys --- .../Schema/Grammars/PostgresGrammar.php | 5 +- .../Database/Schema/PostgresBuilder.php | 36 ++++---- .../Database/DatabasePostgresBuilderTest.php | 90 ++++++++++--------- .../DatabasePostgresSchemaBuilderTest.php | 15 ++-- .../DatabasePostgresSchemaGrammarTest.php | 2 +- 5 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 038ed316e9af..a0b67f01ca60 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -71,6 +71,8 @@ public function compileDropDatabaseIfExists($name) /** * Compile the query to determine if a table exists. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileTableExists() @@ -160,12 +162,11 @@ public function compileColumnListing() /** * Compile the query to determine the columns. * - * @param string $database * @param string $schema * @param string $table * @return string */ - public function compileColumns($database, $schema, $table) + public function compileColumns($schema, $table) { return sprintf( 'select quote_ident(a.attname) as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, ' diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 5b62187b45f0..9cf829721ad2 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Schema; use Illuminate\Database\Concerns\ParsesSearchPath; +use InvalidArgumentException; class PostgresBuilder extends Builder { @@ -44,13 +45,18 @@ public function dropDatabaseIfExists($name) */ public function hasTable($table) { - [$database, $schema, $table] = $this->parseSchemaAndTable($table); + [$schema, $table] = $this->parseSchemaAndTable($table); $table = $this->connection->getTablePrefix().$table; - return count($this->connection->selectFromWriteConnection( - $this->grammar->compileTableExists(), [$database, $schema, $table] - )) > 0; + foreach ($this->getTables() as $value) { + if (strtolower($table) === strtolower($value['name']) + && strtolower($schema) === strtolower($value['schema'])) { + return true; + } + } + + return false; } /** @@ -213,12 +219,12 @@ public function dropAllTypes() */ public function getColumns($table) { - [$database, $schema, $table] = $this->parseSchemaAndTable($table); + [$schema, $table] = $this->parseSchemaAndTable($table); $table = $this->connection->getTablePrefix().$table; $results = $this->connection->selectFromWriteConnection( - $this->grammar->compileColumns($database, $schema, $table) + $this->grammar->compileColumns($schema, $table) ); return $this->connection->getPostProcessor()->processColumns($results); @@ -232,7 +238,7 @@ public function getColumns($table) */ public function getIndexes($table) { - [, $schema, $table] = $this->parseSchemaAndTable($table); + [$schema, $table] = $this->parseSchemaAndTable($table); $table = $this->connection->getTablePrefix().$table; @@ -249,7 +255,7 @@ public function getIndexes($table) */ public function getForeignKeys($table) { - [, $schema, $table] = $this->parseSchemaAndTable($table); + [$schema, $table] = $this->parseSchemaAndTable($table); $table = $this->connection->getTablePrefix().$table; @@ -271,7 +277,7 @@ protected function getSchemas() } /** - * Parse the database object reference and extract the database, schema, and table. + * Parse the database object reference and extract the schema and table. * * @param string $reference * @return array @@ -280,14 +286,10 @@ protected function parseSchemaAndTable($reference) { $parts = explode('.', $reference); - $database = $this->connection->getConfig('database'); - - // If the reference contains a database name, we will use that instead of the - // default database name for the connection. This allows the database name - // to be specified in the query instead of at the full connection level. - if (count($parts) === 3) { + if (count($parts) > 2) { $database = $parts[0]; - array_shift($parts); + + throw new InvalidArgumentException("Using 3-parts reference is not supported, you may use `Schema::connection('$database')` instead."); } // We will use the default schema unless the schema has been specified in the @@ -300,7 +302,7 @@ protected function parseSchemaAndTable($reference) array_shift($parts); } - return [$database, $schema, $parts[0]]; + return [$schema, $parts[0]]; } /** diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php index 4de2c649ebdd..d3a280239d3a 100644 --- a/tests/Database/DatabasePostgresBuilderTest.php +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -52,14 +52,17 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathMissing() $connection->shouldReceive('getConfig')->with('search_path')->andReturn(null); $connection->shouldReceive('getConfig')->with('schema')->andReturn(null); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); - $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'public', 'foo'])->andReturn(['countable_result']); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'public', 'name' => 'foo']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'public', 'name' => 'foo']]); - $builder->hasTable('foo'); + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('public.foo')); } public function testHasTableWhenSchemaUnqualifiedAndSearchPathFilled() @@ -67,14 +70,17 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathFilled() $connection = $this->getConnection(); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public'); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); - $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); - $builder->hasTable('foo'); + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('myapp.foo')); } public function testHasTableWhenSchemaUnqualifiedAndSearchPathFallbackFilled() @@ -83,14 +89,17 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathFallbackFilled() $connection->shouldReceive('getConfig')->with('search_path')->andReturn(null); $connection->shouldReceive('getConfig')->with('schema')->andReturn(['myapp', 'public']); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); - $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); - $builder->hasTable('foo'); + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('myapp.foo')); } public function testHasTableWhenSchemaUnqualifiedAndSearchPathIsUserVariable() @@ -99,14 +108,17 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathIsUserVariable() $connection->shouldReceive('getConfig')->with('username')->andReturn('foouser'); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user'); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); - $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'foouser', 'foo'])->andReturn(['countable_result']); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'foouser', 'name' => 'foo']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'foouser', 'name' => 'foo']]); - $builder->hasTable('foo'); + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('foouser.foo')); } public function testHasTableWhenSchemaQualifiedAndSearchPathMismatches() @@ -114,26 +126,25 @@ public function testHasTableWhenSchemaQualifiedAndSearchPathMismatches() $connection = $this->getConnection(); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $grammar = m::mock(PostgresGrammar::class); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); - $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); - $builder->hasTable('myapp.foo'); + $this->assertTrue($builder->hasTable('myapp.foo')); } public function testHasTableWhenDatabaseAndSchemaQualifiedAndSearchPathMismatches() { + $this->expectException(\InvalidArgumentException::class); + $connection = $this->getConnection(); - $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); - $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['mydatabase', 'myapp', 'foo'])->andReturn(['countable_result']); - $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $builder = $this->getBuilder($connection); $builder->hasTable('mydatabase.myapp.foo'); @@ -146,10 +157,9 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathMissing() $connection->shouldReceive('getConfig')->with('schema')->andReturn(null); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('laravel', 'public', 'foo')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('public', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); @@ -164,10 +174,9 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathFilled() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('laravel', 'myapp', 'foo')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); @@ -183,10 +192,9 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathIsUserVari $connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('laravel', 'foouser', 'foo')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('foouser', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); @@ -201,10 +209,9 @@ public function testGetColumnListingWhenSchemaQualifiedAndSearchPathMismatches() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('laravel', 'myapp', 'foo')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); @@ -215,17 +222,12 @@ public function testGetColumnListingWhenSchemaQualifiedAndSearchPathMismatches() public function testGetColumnWhenDatabaseAndSchemaQualifiedAndSearchPathMismatches() { + $this->expectException(\InvalidArgumentException::class); + $connection = $this->getConnection(); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('mydatabase', 'myapp', 'foo')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); - $connection->shouldReceive('getTablePrefix'); - $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); - $processor = m::mock(PostgresProcessor::class); - $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); $builder = $this->getBuilder($connection); $builder->getColumnListing('mydatabase.myapp.foo'); diff --git a/tests/Database/DatabasePostgresSchemaBuilderTest.php b/tests/Database/DatabasePostgresSchemaBuilderTest.php index 448b41138a84..5794ce44aeea 100755 --- a/tests/Database/DatabasePostgresSchemaBuilderTest.php +++ b/tests/Database/DatabasePostgresSchemaBuilderTest.php @@ -20,17 +20,19 @@ public function testHasTable() { $connection = m::mock(Connection::class); $grammar = m::mock(PostgresGrammar::class); - $connection->shouldReceive('getDatabaseName')->andReturn('db'); + $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); - $connection->shouldReceive('getConfig')->with('database')->andReturn('db'); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); $connection->shouldReceive('getConfig')->with('schema')->andReturn('schema'); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $builder = new PostgresBuilder($connection); - $grammar->shouldReceive('compileTableExists')->once()->andReturn('sql'); - $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); - $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql', ['db', 'public', 'prefix_table'])->andReturn(['prefix_table']); + $grammar->shouldReceive('compileTables')->twice()->andReturn('sql'); + $processor->shouldReceive('processTables')->twice()->andReturn([['schema' => 'public', 'name' => 'prefix_table']]); + $connection->shouldReceive('getTablePrefix')->twice()->andReturn('prefix_'); + $connection->shouldReceive('selectFromWriteConnection')->twice()->with('sql')->andReturn([['schema' => 'public', 'name' => 'prefix_table']]); $this->assertTrue($builder->hasTable('table')); + $this->assertTrue($builder->hasTable('public.table')); } public function testGetColumnListing() @@ -38,13 +40,12 @@ public function testGetColumnListing() $connection = m::mock(Connection::class); $grammar = m::mock(PostgresGrammar::class); $processor = m::mock(PostgresProcessor::class); - $connection->shouldReceive('getDatabaseName')->andReturn('db'); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $connection->shouldReceive('getConfig')->with('database')->andReturn('db'); $connection->shouldReceive('getConfig')->with('schema')->andReturn('schema'); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); - $grammar->shouldReceive('compileColumns')->with('db', 'public', 'prefix_table')->once()->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with('public', 'prefix_table')->once()->andReturn('sql'); $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'column']]); $builder = new PostgresBuilder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 20e48d1daf48..8f1fc47b6019 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -1203,7 +1203,7 @@ public function testCompileTableExists() public function testCompileColumns() { - $statement = $this->getGrammar()->compileColumns('db', 'public', 'table'); + $statement = $this->getGrammar()->compileColumns('public', 'table'); $this->assertStringContainsString("where c.relname = 'table' and n.nspname = 'public'", $statement); } From 996375dd61f8c6e4ac262b57ed485655d71fcbdc Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 13 Dec 2023 14:10:58 -0600 Subject: [PATCH 206/207] version --- 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 2142220f8443..ccd00decb762 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.37.2'; + const VERSION = '10.37.3'; /** * The base path for the Laravel installation. From eb948833d513088249d73775586d8fe0ccf48158 Mon Sep 17 00:00:00 2001 From: fragkp Date: Wed, 13 Dec 2023 22:31:30 +0100 Subject: [PATCH 207/207] [10.x] Add routeRoute method to test request (#49366) * feat: add routeRoute method to test request * Update MakesHttpRequests.php --------- Co-authored-by: Taylor Otwell --- .../Testing/Concerns/MakesHttpRequests.php | 14 +++++++++++++- .../Testing/Concerns/MakesHttpRequestsTest.php | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php index 6a14fb1d30fe..a2b2c8c675a9 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -297,7 +297,7 @@ public function disableCookieEncryption() } /** - * Set the referer header and previous URL session value in order to simulate a previous request. + * Set the referer header and previous URL session value from a given URL in order to simulate a previous request. * * @param string $url * @return $this @@ -309,6 +309,18 @@ public function from(string $url) return $this->withHeader('referer', $url); } + /** + * Set the referer header and previous URL session value from a given route in order to simulate a previous request. + * + * @param string $name + * @param mixed $parameters + * @return $this + */ + public function fromRoute(string $name, $parameters = []) + { + return $this->from($this->app['url']->route($name, $parameters)); + } + /** * Set the Precognition header to "true". * diff --git a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php index 0cbf5d417301..e848cf378a61 100644 --- a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php +++ b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php @@ -18,6 +18,18 @@ public function testFromSetsHeaderAndSession() $this->assertSame('previous/url', $this->app['session']->previousUrl()); } + public function testFromRouteSetsHeaderAndSession() + { + $router = $this->app->make(Registrar::class); + + $router->get('previous/url', fn () => 'ok')->name('previous-url'); + + $this->fromRoute('previous-url'); + + $this->assertSame('http://localhost/previous/url', $this->defaultHeaders['referer']); + $this->assertSame('http://localhost/previous/url', $this->app['session']->previousUrl()); + } + public function testWithTokenSetsAuthorizationHeader() { $this->withToken('foobar');