From ed4884a7f6f1fa5dff0d791adbd239c13b92d859 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 2 Jul 2024 16:51:27 -0500 Subject: [PATCH 01/48] work on defer --- .../Providers/FoundationServiceProvider.php | 30 +++++++++++++++++++ src/Illuminate/Foundation/helpers.php | 18 +++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index c3455b92105b..5f5ff72eabbd 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -22,7 +22,9 @@ use Illuminate\Http\Client\Factory as HttpFactory; use Illuminate\Http\Request; use Illuminate\Log\Events\MessageLogged; +use Illuminate\Queue\Events\JobProcessed; use Illuminate\Support\AggregateServiceProvider; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; use Illuminate\Testing\LoggedExceptionCollection; use Illuminate\Testing\ParallelTestingServiceProvider; @@ -86,6 +88,7 @@ public function register() $this->registerDumper(); $this->registerRequestValidation(); $this->registerRequestSignatureValidation(); + $this->registerDeferHandler(); $this->registerExceptionTracking(); $this->registerExceptionRenderer(); $this->registerMaintenanceModeManager(); @@ -184,6 +187,33 @@ public function registerRequestSignatureValidation() }); } + /** + * Register the "defer" function termination handler. + * + * @return void + */ + protected function registerDeferHandler() + { + $this->app->scoped('illuminate:foundation:deferred', Collection::class); + + $this->callAfterResolving('illuminate:foundation:deferred', function (Collection $defers) { + $run = function () use ($defers) { + while ($callback = $defers->shift()) { + rescue($callback); + } + }; + + // TODO: This should also listen for other events, e.g., `JobFailed`... + app('events')->listen(function (JobProcessed $event) use ($run) { + if ($event->connectionName !== 'sync') { + $run(); + } + }); + + app()->terminating($run); + }); + } + /** * Register an event listener to track logged exceptions. * diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index d73632529bff..bdedf99d7df2 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -399,6 +399,24 @@ function decrypt($value, $unserialize = true) } } +if (! function_exists('defer')) { + /** + * Defer execution of the given callback. + * + * @param callable|null $callback + * @param string|null $name + * @return mixed + */ + function defer(?callable $callback = null, ?string $name = null) + { + if ($callback === null) { + return app('illuminate:foundation:deferred'); + } + + app('illuminate:foundation:deferred')[$name] = $callback; + } +} + if (! function_exists('dispatch')) { /** * Dispatch a job to its appropriate handler. From 50aae67994d433c0b788b9ab60441010f78376c8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Jul 2024 13:55:02 -0500 Subject: [PATCH 02/48] refine defer --- .../Foundation/Configuration/Middleware.php | 1 + .../Foundation/Defer/DeferredCallback.php | 44 +++++++++++++++++++ .../Defer/DeferredCallbackCollection.php | 10 +++++ .../Middleware/InvokeDeferredCallbacks.php | 42 ++++++++++++++++++ .../Providers/FoundationServiceProvider.php | 34 +++++++++----- src/Illuminate/Foundation/helpers.php | 11 +++-- 6 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 src/Illuminate/Foundation/Defer/DeferredCallback.php create mode 100644 src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php create mode 100644 src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index 30e60ed98238..52b83acf518b 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -408,6 +408,7 @@ public function priority(array $priority) public function getGlobalMiddleware() { $middleware = $this->global ?: array_values(array_filter([ + \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class, $this->trustHosts ? \Illuminate\Http\Middleware\TrustHosts::class : null, \Illuminate\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, diff --git a/src/Illuminate/Foundation/Defer/DeferredCallback.php b/src/Illuminate/Foundation/Defer/DeferredCallback.php new file mode 100644 index 000000000000..9da087034d3e --- /dev/null +++ b/src/Illuminate/Foundation/Defer/DeferredCallback.php @@ -0,0 +1,44 @@ +always = $always; + + return $this; + } + + /** + * Invoke the deferred callback. + * + * @return void + */ + public function __invoke(): void + { + call_user_func($this->callback); + } +} diff --git a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php new file mode 100644 index 000000000000..943ecc13359c --- /dev/null +++ b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php @@ -0,0 +1,10 @@ +make(DeferredCallbackCollection::class); + + while ($callback = $deferred->shift()) { + if ($response->isSuccessful() || $callback->always) { + rescue($callback); + } + } + } +} diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 5f5ff72eabbd..f3e283c55a40 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -2,6 +2,7 @@ namespace Illuminate\Foundation\Providers; +use Illuminate\Console\Events\CommandFinished; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Container\Container; @@ -12,6 +13,7 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Database\Grammar; use Illuminate\Foundation\Console\CliDumper; +use Illuminate\Foundation\Defer\DeferredCallbackCollection; use Illuminate\Foundation\Exceptions\Renderer\Listener; use Illuminate\Foundation\Exceptions\Renderer\Mappers\BladeMapper; use Illuminate\Foundation\Exceptions\Renderer\Renderer; @@ -22,6 +24,7 @@ use Illuminate\Http\Client\Factory as HttpFactory; use Illuminate\Http\Request; use Illuminate\Log\Events\MessageLogged; +use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Collection; @@ -194,23 +197,30 @@ public function registerRequestSignatureValidation() */ protected function registerDeferHandler() { - $this->app->scoped('illuminate:foundation:deferred', Collection::class); + $this->app->scoped(DeferredCallbackCollection::class); - $this->callAfterResolving('illuminate:foundation:deferred', function (Collection $defers) { - $run = function () use ($defers) { - while ($callback = $defers->shift()) { + $this->app['events']->listen(function (CommandFinished $event) { + $deferred = app(DeferredCallbackCollection::class); + + while ($callback = $deferred->shift()) { + if (app()->runningInConsole() && ($event->exitCode === 0 || $callback->always)) { rescue($callback); } - }; + } + }); - // TODO: This should also listen for other events, e.g., `JobFailed`... - app('events')->listen(function (JobProcessed $event) use ($run) { - if ($event->connectionName !== 'sync') { - $run(); - } - }); + $this->app['events']->listen(function (JobProcessed|JobFailed $event) { + $deferred = app(DeferredCallbackCollection::class); - app()->terminating($run); + if ($event->connectionName === 'sync') { + return; + } + + while ($callback = $deferred->shift()) { + if ($event instanceof JobProcessed || $callback->always) { + rescue($callback); + } + } }); } diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index bdedf99d7df2..dc60d20b7f5f 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -14,6 +14,8 @@ use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Foundation\Bus\PendingClosureDispatch; use Illuminate\Foundation\Bus\PendingDispatch; +use Illuminate\Foundation\Defer\DeferredCallback; +use Illuminate\Foundation\Defer\DeferredCallbackCollection; use Illuminate\Foundation\Mix; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Log\Context\Repository as ContextRepository; @@ -405,15 +407,18 @@ function decrypt($value, $unserialize = true) * * @param callable|null $callback * @param string|null $name - * @return mixed + * @return \Illuminate\Foundation\Defer\DeferredCallback */ function defer(?callable $callback = null, ?string $name = null) { if ($callback === null) { - return app('illuminate:foundation:deferred'); + return app(DeferredCallbackCollection::class); } - app('illuminate:foundation:deferred')[$name] = $callback; + return tap( + new DeferredCallback($callback), + fn ($deferred) => app(DeferredCallbackCollection::class)[$name] = $deferred + ); } } From efa71ff79609d925bf874512bcd71079e14bebf3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Jul 2024 14:07:48 -0500 Subject: [PATCH 03/48] refactor --- .../Foundation/Defer/DeferredCallback.php | 20 +++-- .../Defer/DeferredCallbackCollection.php | 85 ++++++++++++++++++- .../Middleware/InvokeDeferredCallbacks.php | 6 +- .../Providers/FoundationServiceProvider.php | 24 ++---- 4 files changed, 104 insertions(+), 31 deletions(-) diff --git a/src/Illuminate/Foundation/Defer/DeferredCallback.php b/src/Illuminate/Foundation/Defer/DeferredCallback.php index 9da087034d3e..296b10b322b3 100644 --- a/src/Illuminate/Foundation/Defer/DeferredCallback.php +++ b/src/Illuminate/Foundation/Defer/DeferredCallback.php @@ -4,19 +4,27 @@ class DeferredCallback { - /** - * Indicates if the deferred callback should run even on unsuccessful requests and jobs. - */ - public bool $always = false; - /** * Create a new deferred callback instance. * * @param callable $callback * @return void */ - public function __construct(protected $callback) + public function __construct(public $callback, public ?string $name = null, public bool $always = false) + { + } + + /** + * Specify the name of the deferred callback so it can be cancelled later. + * + * @param string $name + * @return $this + */ + public function name(string $name): self { + $this->name = $name; + + return $this; } /** diff --git a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php index 943ecc13359c..77dff2926b81 100644 --- a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php +++ b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php @@ -2,9 +2,90 @@ namespace Illuminate\Foundation\Defer; +use ArrayAccess; +use Closure; use Illuminate\Support\Collection; -class DeferredCallbackCollection extends Collection +class DeferredCallbackCollection implements ArrayAccess { - // + /** + * All of the deferred callbacks. + * + * @var array + */ + protected array $callbacks = []; + + /** + * Invoke the deferred callbacks. + * + * @return void + */ + public function invoke(): void + { + $this->invokeWhen(fn () => true); + } + + /** + * Invoke the deferred callbacks if the given truth test evaluates to true. + * + * @param \Closure $when + * @return void + */ + public function invokeWhen(?Closure $when = null): void + { + $when ??= fn () => true; + + foreach ($this->callbacks as $index => $callback) { + if ($when($callback)) { + rescue($callback); + } + + unset($this->callbacks[$index]); + } + } + + /** + * Determine if the collection has a callback with the given key. + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->callbacks[$offset]); + } + + /** + * Get the callback with the given key. + * + * @param mixed $offset + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->callbacks[$offset]; + } + + /** + * Set teh callback with the given key. + * + * @param mixed $offset + * @param mixed $value + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->callbacks[$offset] = $value; + } + + /** + * Remove the callback with the given key from the collection. + * + * @param mixed $offset + * @return void + */ + public function offsetUnset(mixed $offset): void + { + unset($this->callbacks[$offset]); + } } diff --git a/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php b/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php index 8331e51a717c..7a05bdcdb753 100644 --- a/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php +++ b/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php @@ -33,10 +33,6 @@ public function terminate(Request $request, Response $response) { $deferred = Container::getInstance()->make(DeferredCallbackCollection::class); - while ($callback = $deferred->shift()) { - if ($response->isSuccessful() || $callback->always) { - rescue($callback); - } - } + $deferred->invokeWhen(fn ($callback) => $response->isSuccessful() || $callback->always); } } diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index f3e283c55a40..90ec3b1eed83 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -200,27 +200,15 @@ protected function registerDeferHandler() $this->app->scoped(DeferredCallbackCollection::class); $this->app['events']->listen(function (CommandFinished $event) { - $deferred = app(DeferredCallbackCollection::class); - - while ($callback = $deferred->shift()) { - if (app()->runningInConsole() && ($event->exitCode === 0 || $callback->always)) { - rescue($callback); - } - } + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => + app()->runningInConsole() && ($event->exitCode === 0 || $callback->always) + ); }); $this->app['events']->listen(function (JobProcessed|JobFailed $event) { - $deferred = app(DeferredCallbackCollection::class); - - if ($event->connectionName === 'sync') { - return; - } - - while ($callback = $deferred->shift()) { - if ($event instanceof JobProcessed || $callback->always) { - rescue($callback); - } - } + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => + $event->connectionName !== 'sync' && ($event instanceof JobProcessed || $callback->always) + ); }); } From 714cef2c8dffb6eb814fa4505b13cfe40610f42b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Jul 2024 14:13:16 -0500 Subject: [PATCH 04/48] formatting --- .../Defer/DeferredCallbackCollection.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php index 77dff2926b81..3e4cb2237867 100644 --- a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php +++ b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php @@ -44,6 +44,20 @@ public function invokeWhen(?Closure $when = null): void } } + /** + * Remove any deferred callbacks with the given name. + * + * @param string $name + * @return void + */ + public function forget(string $name): void + { + $this->callbacks = collect($this->callbacks) + ->reject(fn ($callback) => $callback->name === $name) + ->values() + ->all(); + } + /** * Determine if the collection has a callback with the given key. * From 98b4d4c4311caf7d8318d775292ab254214672b3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Jul 2024 14:19:00 -0500 Subject: [PATCH 05/48] fix new --- src/Illuminate/Foundation/helpers.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index dc60d20b7f5f..8ce88f468033 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -407,17 +407,18 @@ function decrypt($value, $unserialize = true) * * @param callable|null $callback * @param string|null $name + * @param bool $always * @return \Illuminate\Foundation\Defer\DeferredCallback */ - function defer(?callable $callback = null, ?string $name = null) + function defer(?callable $callback = null, ?string $name = null, bool $always = false) { if ($callback === null) { return app(DeferredCallbackCollection::class); } return tap( - new DeferredCallback($callback), - fn ($deferred) => app(DeferredCallbackCollection::class)[$name] = $deferred + new DeferredCallback($callback, $name, $always), + fn ($deferred) => app(DeferredCallbackCollection::class)[] = $deferred ); } } From cfd40dd11bb4a2143f5b3fc4385b2a6daf88d102 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Jul 2024 11:40:02 -0500 Subject: [PATCH 06/48] fix status code --- .../Foundation/Http/Middleware/InvokeDeferredCallbacks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php b/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php index 7a05bdcdb753..f142c1de83f5 100644 --- a/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php +++ b/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php @@ -33,6 +33,6 @@ public function terminate(Request $request, Response $response) { $deferred = Container::getInstance()->make(DeferredCallbackCollection::class); - $deferred->invokeWhen(fn ($callback) => $response->isSuccessful() || $callback->always); + $deferred->invokeWhen(fn ($callback) => $response->getStatusCode() < 400 || $callback->always); } } From f39acdc98259ae4efde5c612a0632d499c0e9a61 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Jul 2024 15:08:32 -0500 Subject: [PATCH 07/48] handle job releases after exceptions --- .../Foundation/Providers/FoundationServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 90ec3b1eed83..570e54186465 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -26,6 +26,7 @@ use Illuminate\Log\Events\MessageLogged; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; @@ -205,7 +206,7 @@ protected function registerDeferHandler() ); }); - $this->app['events']->listen(function (JobProcessed|JobFailed $event) { + $this->app['events']->listen(function (JobProcessed|JobReleasedAfterException|JobFailed $event) { app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => $event->connectionName !== 'sync' && ($event instanceof JobProcessed || $callback->always) ); From b4b7a75bbee85121e61df840b5baf95b89a3518d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Jul 2024 16:11:25 -0500 Subject: [PATCH 08/48] add job attempted event --- .../Providers/FoundationServiceProvider.php | 9 ++-- src/Illuminate/Queue/Events/JobAttempted.php | 52 +++++++++++++++++++ src/Illuminate/Queue/Worker.php | 7 +++ 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/Illuminate/Queue/Events/JobAttempted.php diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 570e54186465..11cef04e3e98 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -24,11 +24,8 @@ use Illuminate\Http\Client\Factory as HttpFactory; use Illuminate\Http\Request; use Illuminate\Log\Events\MessageLogged; -use Illuminate\Queue\Events\JobFailed; -use Illuminate\Queue\Events\JobProcessed; -use Illuminate\Queue\Events\JobReleasedAfterException; +use Illuminate\Queue\Events\JobAttempted; use Illuminate\Support\AggregateServiceProvider; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; use Illuminate\Testing\LoggedExceptionCollection; use Illuminate\Testing\ParallelTestingServiceProvider; @@ -206,9 +203,9 @@ protected function registerDeferHandler() ); }); - $this->app['events']->listen(function (JobProcessed|JobReleasedAfterException|JobFailed $event) { + $this->app['events']->listen(function (JobAttempted $event) { app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => - $event->connectionName !== 'sync' && ($event instanceof JobProcessed || $callback->always) + $event->connectionName !== 'sync' && ($event->successful() || $callback->always) ); }); } diff --git a/src/Illuminate/Queue/Events/JobAttempted.php b/src/Illuminate/Queue/Events/JobAttempted.php new file mode 100644 index 000000000000..6dfa2df148ab --- /dev/null +++ b/src/Illuminate/Queue/Events/JobAttempted.php @@ -0,0 +1,52 @@ +job = $job; + $this->connectionName = $connectionName; + $this->exceptionOccurred = $exceptionOccurred; + } + + /** + * Determine if the job completed with failing or an unhandled exception occurring. + * + * @return bool + */ + public function successful(): bool + { + return ! $this->job->hasFailed() && ! $this->exceptionOccurred; + } +} diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index c9f335a66467..83b4e284d40c 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Queue\Factory as QueueManager; use Illuminate\Database\DetectsLostConnections; +use Illuminate\Queue\Events\JobAttempted; use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Events\JobPopped; use Illuminate\Queue\Events\JobPopping; @@ -440,7 +441,13 @@ public function process($connectionName, $job, WorkerOptions $options) $this->raiseAfterJobEvent($connectionName, $job); } catch (Throwable $e) { + $exceptionOccurred = true; + $this->handleJobException($connectionName, $job, $options, $e); + } finally { + $this->events->dispatch(new JobAttempted( + $connectionName, $job, $exceptionOccurred ?? false + )); } } From cff79e450fd5fc7c811143e4293b265905033493 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Jul 2024 16:24:59 -0500 Subject: [PATCH 09/48] formatting --- .../Foundation/Defer/DeferredCallback.php | 3 +++ .../Foundation/Defer/DeferredCallbackCollection.php | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Defer/DeferredCallback.php b/src/Illuminate/Foundation/Defer/DeferredCallback.php index 296b10b322b3..14b8a85690dd 100644 --- a/src/Illuminate/Foundation/Defer/DeferredCallback.php +++ b/src/Illuminate/Foundation/Defer/DeferredCallback.php @@ -2,6 +2,8 @@ namespace Illuminate\Foundation\Defer; +use Illuminate\Support\Str; + class DeferredCallback { /** @@ -12,6 +14,7 @@ class DeferredCallback */ public function __construct(public $callback, public ?string $name = null, public bool $always = false) { + $this->name = $name ?? (string) Str::uuid(); } /** diff --git a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php index 3e4cb2237867..4c041ee240a5 100644 --- a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php +++ b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php @@ -35,6 +35,13 @@ public function invokeWhen(?Closure $when = null): void { $when ??= fn () => true; + $this->callbacks = collect($this->callbacks) + ->reverse() + ->unique(fn ($c) => $c->name) + ->reverse() + ->values() + ->all(); + foreach ($this->callbacks as $index => $callback) { if ($when($callback)) { rescue($callback); @@ -89,7 +96,11 @@ public function offsetGet(mixed $offset): mixed */ public function offsetSet(mixed $offset, mixed $value): void { - $this->callbacks[$offset] = $value; + if (is_null($offset)) { + $this->callbacks[] = $value; + } else { + $this->callbacks[$offset] = $value; + } } /** From 3ec696c4d0694998cd2a6845442712c554736ffb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Jul 2024 16:33:45 -0500 Subject: [PATCH 10/48] formatting " --- .../Foundation/Http/Middleware/InvokeDeferredCallbacks.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php b/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php index f142c1de83f5..9a804332e73e 100644 --- a/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php +++ b/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php @@ -31,8 +31,8 @@ public function handle(Request $request, Closure $next) */ public function terminate(Request $request, Response $response) { - $deferred = Container::getInstance()->make(DeferredCallbackCollection::class); - - $deferred->invokeWhen(fn ($callback) => $response->getStatusCode() < 400 || $callback->always); + Container::getInstance() + ->make(DeferredCallbackCollection::class) + ->invokeWhen(fn ($callback) => $response->getStatusCode() < 400 || $callback->always); } } From b7fa85f7385cf13c9bb01057751116e6a6dfb09b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 16 Jul 2024 17:04:46 -0500 Subject: [PATCH 11/48] first pass at swr --- src/Illuminate/Cache/Repository.php | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index af09dd6cdbc0..94f68d2d96dc 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -471,6 +471,61 @@ public function rememberForever($key, Closure $callback) return $value; } + /** + * Retrieve an item from the cache by key, refreshing it in the background if it is stale. + * + * @template TCacheValue + * + * @param string $key + * @param array{ 0: int, 1: int } $ttl + * @param (callable(): TCacheValue) $callback + * @param array{ seconds?: int, owner?: string }|null $lock + * @return TCacheValue + */ + public function staleWhileRevalidate($key, $ttl, $callback, $lock = null) + { + [ + $key => $value, + "{$key}:created" => $created, + ] = $this->many([$key, "{$key}:created"]); + + if ($created === null) { + return tap(value($callback), fn ($value) => $this->putMany([ + $key => $value, + "{$key}:created" => Carbon::now()->getTimestamp(), + ], $ttl[1])); + } + + if (($created + $this->getSeconds($ttl[0])) > Carbon::now()->getTimestamp()) { + return $value; + } + + $refresh = function () use ($key, $ttl, $callback, $lock, $created) { + $this->store->lock( + "illuminate:cache:refresh:lock:{$key}", + $lock['seconds'] ?? 0, + $lock['owner'] ?? null, + )->get(function () use ($key, $callback, $created, $ttl, $lock) { + if ($created !== $this->get("{$key}:created")) { + return; + } + + $this->putMany([ + $key => value($callback), + "{$key}:created" => Carbon::now()->getTimestamp(), + ], $ttl[1]); + }); + }; + + if (function_exists('defer')) { + defer($refresh); + } else { + $refresh(); + } + + return $value; + } + /** * Remove an item from the cache. * From c9e1b7d7150481fb96cc79a2a6eb4b0561a5b44d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 18 Jul 2024 11:05:23 -0500 Subject: [PATCH 12/48] rename method --- src/Illuminate/Cache/Repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 94f68d2d96dc..6f48f9679d77 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -482,7 +482,7 @@ public function rememberForever($key, Closure $callback) * @param array{ seconds?: int, owner?: string }|null $lock * @return TCacheValue */ - public function staleWhileRevalidate($key, $ttl, $callback, $lock = null) + public function flexible($key, $ttl, $callback, $lock = null) { [ $key => $value, From 11977e7e063d74c5cd3af6ad0b0460f4a5ceb237 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 18 Jul 2024 15:43:35 -0500 Subject: [PATCH 13/48] add swr test --- src/Illuminate/Cache/Repository.php | 2 +- .../Defer/DeferredCallbackCollection.php | 55 ++++++- tests/Integration/Cache/RepositoryTest.php | 150 ++++++++++++++++++ 3 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 tests/Integration/Cache/RepositoryTest.php diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 6f48f9679d77..a0e45fcd3fdf 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -518,7 +518,7 @@ public function flexible($key, $ttl, $callback, $lock = null) }; if (function_exists('defer')) { - defer($refresh); + defer($refresh, "illuminate:cache:refresh:{$key}"); } else { $refresh(); } diff --git a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php index 4c041ee240a5..55749481da59 100644 --- a/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php +++ b/src/Illuminate/Foundation/Defer/DeferredCallbackCollection.php @@ -4,9 +4,10 @@ use ArrayAccess; use Closure; +use Countable; use Illuminate\Support\Collection; -class DeferredCallbackCollection implements ArrayAccess +class DeferredCallbackCollection implements ArrayAccess, Countable { /** * All of the deferred callbacks. @@ -15,6 +16,16 @@ class DeferredCallbackCollection implements ArrayAccess */ protected array $callbacks = []; + /** + * Get the first callback in the collection. + * + * @return callable + */ + public function first() + { + return array_values($this->callbacks)[0]; + } + /** * Invoke the deferred callbacks. * @@ -35,12 +46,7 @@ public function invokeWhen(?Closure $when = null): void { $when ??= fn () => true; - $this->callbacks = collect($this->callbacks) - ->reverse() - ->unique(fn ($c) => $c->name) - ->reverse() - ->values() - ->all(); + $this->forgetDuplicates(); foreach ($this->callbacks as $index => $callback) { if ($when($callback)) { @@ -65,6 +71,23 @@ public function forget(string $name): void ->all(); } + /** + * Remove any duplicate callbacks. + * + * @return $this + */ + protected function forgetDuplicates(): self + { + $this->callbacks = collect($this->callbacks) + ->reverse() + ->unique(fn ($c) => $c->name) + ->reverse() + ->values() + ->all(); + + return $this; + } + /** * Determine if the collection has a callback with the given key. * @@ -73,6 +96,8 @@ public function forget(string $name): void */ public function offsetExists(mixed $offset): bool { + $this->forgetDuplicates(); + return isset($this->callbacks[$offset]); } @@ -84,6 +109,8 @@ public function offsetExists(mixed $offset): bool */ public function offsetGet(mixed $offset): mixed { + $this->forgetDuplicates(); + return $this->callbacks[$offset]; } @@ -111,6 +138,20 @@ public function offsetSet(mixed $offset, mixed $value): void */ public function offsetUnset(mixed $offset): void { + $this->forgetDuplicates(); + unset($this->callbacks[$offset]); } + + /** + * Determine how many callbacks are in the collection. + * + * @return int + */ + public function count(): int + { + $this->forgetDuplicates(); + + return count($this->callbacks); + } } diff --git a/tests/Integration/Cache/RepositoryTest.php b/tests/Integration/Cache/RepositoryTest.php new file mode 100644 index 000000000000..1d453a66f269 --- /dev/null +++ b/tests/Integration/Cache/RepositoryTest.php @@ -0,0 +1,150 @@ +flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + + $this->assertSame(1, $value); + $this->assertCount(0, defer()); + $this->assertSame(1, $cache->get('foo')); + $this->assertSame(946684800, $cache->get('foo:created')); + + // Cache is fresh. The value should be retrieved from the cache and used... + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(1, $value); + $this->assertCount(0, defer()); + $this->assertSame(1, $cache->get('foo')); + $this->assertSame(946684800, $cache->get('foo:created')); + + Carbon::setTestNow(now()->addSeconds(11)); + + // Cache is now "stale". The stored value should be used and a deferred + // callback should be registered to refresh the cache. + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(1, $value); + $this->assertCount(1, defer()); + $this->assertSame(1, $cache->get('foo')); + $this->assertSame(946684800, $cache->get('foo:created')); + + // We will hit it again within the same request. This should not queue + // up an additional deferred callback as only one can be registered at + // a time for each key. + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(1, $value); + $this->assertCount(1, defer()); + $this->assertSame(1, $cache->get('foo')); + $this->assertSame(946684800, $cache->get('foo:created')); + + // We will now simulate the end of the request lifecycle by executing the + // deferred callback. This should refresh the cache. + defer()->invoke(); + $this->assertCount(0, defer()); + $this->assertSame(2, $cache->get('foo')); // this has been updated! + $this->assertSame(946684811, $cache->get('foo:created')); // this has been updated! + + // Now the cache is fresh again... + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(2, $value); + $this->assertCount(0, defer()); + $this->assertSame(2, $cache->get('foo')); + $this->assertSame(946684811, $cache->get('foo:created')); + + // Let's now progress time beyond the stale TTL... + Carbon::setTestNow(now()->addSeconds(21)); + + // Now the values should have left the cache. We should refresh. + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(3, $value); + $this->assertCount(0, defer()); + $this->assertSame(3, $cache->get('foo')); + $this->assertSame(946684832, $cache->get('foo:created')); + + + // Now lets see what happens when another request, job, or command is + // also trying to refresh the same key at the same time. Will push past + // the "fresh" TTL and register a deferred callback. + Carbon::setTestNow(now()->addSeconds(11)); + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(3, $value); + $this->assertCount(1, defer()); + $this->assertSame(3, $cache->get('foo')); + $this->assertSame(946684832, $cache->get('foo:created')); + + // Now we will execute the deferred callback but we will first aquire + // our own lock. This means that the value should not be refreshed by + // deferred callback. + /** @var Lock */ + $lock = $cache->lock('illuminate:cache:refresh:lock:foo'); + + $this->assertTrue($lock->acquire()); + defer()->first()(); + $this->assertSame(3, $value); + $this->assertCount(1, defer()); + $this->assertSame(3, $cache->get('foo')); + $this->assertSame(946684832, $cache->get('foo:created')); + $this->assertTrue($lock->release()); + + // Now we have cleared the lock we will, one last time, confirm that + // the deferred callack does refresh the value when the lock is not active. + defer()->invoke(); + $this->assertCount(0, defer()); + $this->assertSame(4, $cache->get('foo')); + $this->assertSame(946684843, $cache->get('foo:created')); + + // The last thing is to check that we don't refresh the cache in the + // deferred callback if another thread has already done the work for us. + // We will make the cache stale... + Carbon::setTestNow(now()->addSeconds(11)); + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(4, $value); + $this->assertCount(1, defer()); + $this->assertSame(4, $cache->get('foo')); + $this->assertSame(946684843, $cache->get('foo:created')); + + // There is now a deferred callback ready to refresh the cache. We will + // simulate another thread updating the value. + $cache->putMany([ + 'foo' => 99, + 'foo:created' => 946684863, + ]); + + // then we will run the refresh callback + defer()->invoke(); + $value = $cache->flexible('foo', [10, 20], function () use (&$count) { + return ++$count; + }); + $this->assertSame(99, $value); + $this->assertCount(0, defer()); + $this->assertSame(99, $cache->get('foo')); + $this->assertSame(946684863, $cache->get('foo:created')); + } +} From 569c6f54d59b1b25a28e159b262c6480a12f70cb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 2 Feb 2024 22:39:07 -0600 Subject: [PATCH 14/48] starting concurrency --- src/Illuminate/Concurrency/.gitattributes | 2 ++ src/Illuminate/Concurrency/Factory.php | 22 ++++++++++++++ src/Illuminate/Concurrency/LICENSE.md | 21 ++++++++++++++ src/Illuminate/Concurrency/composer.json | 35 +++++++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 src/Illuminate/Concurrency/.gitattributes create mode 100644 src/Illuminate/Concurrency/Factory.php create mode 100644 src/Illuminate/Concurrency/LICENSE.md create mode 100644 src/Illuminate/Concurrency/composer.json diff --git a/src/Illuminate/Concurrency/.gitattributes b/src/Illuminate/Concurrency/.gitattributes new file mode 100644 index 000000000000..7e54581c2a32 --- /dev/null +++ b/src/Illuminate/Concurrency/.gitattributes @@ -0,0 +1,2 @@ +/.github export-ignore +.gitattributes export-ignore diff --git a/src/Illuminate/Concurrency/Factory.php b/src/Illuminate/Concurrency/Factory.php new file mode 100644 index 000000000000..fdefec6b879e --- /dev/null +++ b/src/Illuminate/Concurrency/Factory.php @@ -0,0 +1,22 @@ + Date: Fri, 2 Feb 2024 23:27:51 -0600 Subject: [PATCH 15/48] wip --- .../InvokeSerializedClosureCommand.php | 63 +++++++++++++++++++ src/Illuminate/Concurrency/Factory.php | 37 ++++++++++- .../Providers/ArtisanServiceProvider.php | 2 + .../Support/Facades/Concurrency.php | 21 +++++++ src/Illuminate/Support/Facades/Facade.php | 1 + 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php create mode 100644 src/Illuminate/Support/Facades/Concurrency.php diff --git a/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php b/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php new file mode 100644 index 000000000000..a7ad6c5b58c7 --- /dev/null +++ b/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php @@ -0,0 +1,63 @@ +output->write(json_encode([ + 'successful' => true, + 'result' => serialize($this->laravel->call(match (true) { + ! is_null($this->argument('code')) => unserialize($this->argument('code')), + isset($_SERVER['LARAVEL_INVOKABLE_CLOSURE']) => unserialize($_SERVER['LARAVEL_INVOKABLE_CLOSURE']), + default => fn () => null, + })) + ])); + } catch (Throwable $e) { + report($e); + + $this->output->write(json_encode([ + 'successful' => false, + 'exception' => get_class($e), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ])); + } + } +} diff --git a/src/Illuminate/Concurrency/Factory.php b/src/Illuminate/Concurrency/Factory.php index fdefec6b879e..2fb15c3dc2d1 100644 --- a/src/Illuminate/Concurrency/Factory.php +++ b/src/Illuminate/Concurrency/Factory.php @@ -2,21 +2,54 @@ namespace Illuminate\Concurrency; +use Closure; +use Illuminate\Process\Factory as ProcessFactory; +use Illuminate\Process\Pool; +use Illuminate\Support\Arr; +use Laravel\SerializableClosure\SerializableClosure; + class Factory { /** * Create a new concurrency factory instance. */ - public function __construct(protected Factory $processFactory) + public function __construct(protected ProcessFactory $processFactory) { // } + public function run(Closure|array $tasks): array + { + $results = $this->processFactory->pool(function (Pool $pool) use ($tasks) { + foreach (Arr::wrap($tasks) as $task) { + $pool->path(base_path())->env([ + 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), + ])->command('php artisan invoke-serialized-closure'); + } + })->start()->wait(); + + return $results->collect()->map(function ($result) { + $result = json_decode($result->output(), true); + + if (! $result['successful']) { + $exceptionClass = $result['exception']; + + throw new $exceptionClass($result['message']); + } + + return unserialize($result['result']); + })->all(); + } + /** * Start the given task(s) in the background. */ public function background(Closure|array $tasks): void { - + foreach (Arr::wrap($tasks) as $task) { + $this->processFactory->path(base_path())->env([ + 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), + ])->run('php artisan invoke-serialized-closure 2>&1 &'); + } } } diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index 4ec8289653d7..6ed57c76326e 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Cache\Console\ClearCommand as CacheClearCommand; use Illuminate\Cache\Console\ForgetCommand as CacheForgetCommand; use Illuminate\Cache\Console\PruneStaleTagsCommand; +use Illuminate\Concurrency\Console\InvokeSerializedClosureCommand; use Illuminate\Console\Scheduling\ScheduleClearCacheCommand; use Illuminate\Console\Scheduling\ScheduleFinishCommand; use Illuminate\Console\Scheduling\ScheduleInterruptCommand; @@ -135,6 +136,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'EventCache' => EventCacheCommand::class, 'EventClear' => EventClearCommand::class, 'EventList' => EventListCommand::class, + 'InvokeSerializedClosure' => InvokeSerializedClosureCommand::class, 'KeyGenerate' => KeyGenerateCommand::class, 'Optimize' => OptimizeCommand::class, 'OptimizeClear' => OptimizeClearCommand::class, diff --git a/src/Illuminate/Support/Facades/Concurrency.php b/src/Illuminate/Support/Facades/Concurrency.php new file mode 100644 index 000000000000..bbae33229b16 --- /dev/null +++ b/src/Illuminate/Support/Facades/Concurrency.php @@ -0,0 +1,21 @@ + Broadcast::class, 'Bus' => Bus::class, 'Cache' => Cache::class, + 'Concurrency' => Concurrency::class, 'Config' => Config::class, 'Context' => Context::class, 'Cookie' => Cookie::class, From dc3d5d7c4d3a0a170d2f514e819a775807eea45b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 7 Feb 2024 13:30:27 +0100 Subject: [PATCH 16/48] formatting --- src/Illuminate/Concurrency/Factory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Concurrency/Factory.php b/src/Illuminate/Concurrency/Factory.php index 2fb15c3dc2d1..a699e8e3ff9b 100644 --- a/src/Illuminate/Concurrency/Factory.php +++ b/src/Illuminate/Concurrency/Factory.php @@ -32,9 +32,9 @@ public function run(Closure|array $tasks): array $result = json_decode($result->output(), true); if (! $result['successful']) { - $exceptionClass = $result['exception']; - - throw new $exceptionClass($result['message']); + throw new $result['exception']( + $result['message'] + ); } return unserialize($result['result']); From 8ce4659355f4a40b5a175466875721b8952f4488 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 7 Feb 2024 13:53:12 +0100 Subject: [PATCH 17/48] driver based --- .../Concurrency/ConcurrencyManager.php | 79 +++++++++++++++++++ .../ConcurrencyServiceProvider.php | 34 ++++++++ .../{Factory.php => ProcessDriver.php} | 9 ++- src/Illuminate/Concurrency/SyncDriver.php | 27 +++++++ .../Contracts/Concurrency/Driver.php | 18 +++++ src/Illuminate/Support/DefaultProviders.php | 1 + .../Support/Facades/Concurrency.php | 6 +- .../Support/MultipleInstanceManager.php | 13 +++ 8 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 src/Illuminate/Concurrency/ConcurrencyManager.php create mode 100644 src/Illuminate/Concurrency/ConcurrencyServiceProvider.php rename src/Illuminate/Concurrency/{Factory.php => ProcessDriver.php} (87%) create mode 100644 src/Illuminate/Concurrency/SyncDriver.php create mode 100644 src/Illuminate/Contracts/Concurrency/Driver.php diff --git a/src/Illuminate/Concurrency/ConcurrencyManager.php b/src/Illuminate/Concurrency/ConcurrencyManager.php new file mode 100644 index 000000000000..8beb11e687ef --- /dev/null +++ b/src/Illuminate/Concurrency/ConcurrencyManager.php @@ -0,0 +1,79 @@ +instance($name); + } + + /** + * Create an instance of the process concurrency driver. + * + * @param array $config + * @return \Illuminate\Concurrency\ProcessDriver + */ + public function createProcessDriver(array $config) + { + return new ProcessDriver($this->app->make(ProcessFactory::class)); + } + + /** + * Create an instance of the sync concurrency driver. + * + * @param array $config + * @return \Illuminate\Concurrency\SyncDriver + */ + public function createSyncDriver(array $config) + { + return new SyncDriver; + } + + /** + * Get the default instance name. + * + * @return string + */ + public function getDefaultInstance() + { + return $this->app['config']['concurrency.default']; + } + + /** + * Set the default instance name. + * + * @param string $name + * @return void + */ + public function setDefaultInstance($name) + { + $this->app['config']['concurrency.default'] = $name; + } + + /** + * Get the instance specific configuration. + * + * @param string $name + * @return array + */ + public function getInstanceConfig($name) + { + return $this->app['config']->get( + 'concurrency.drivers.'.$name, ['driver' => $name], + ); + } +} diff --git a/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php b/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php new file mode 100644 index 000000000000..698ccc395598 --- /dev/null +++ b/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php @@ -0,0 +1,34 @@ +app->singleton(ConcurrencyManager::class, function ($app) { + return new ConcurrencyManager($app); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return [ + ConcurrencyManager::class, + ]; + } +} diff --git a/src/Illuminate/Concurrency/Factory.php b/src/Illuminate/Concurrency/ProcessDriver.php similarity index 87% rename from src/Illuminate/Concurrency/Factory.php rename to src/Illuminate/Concurrency/ProcessDriver.php index a699e8e3ff9b..ce79964ddee9 100644 --- a/src/Illuminate/Concurrency/Factory.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -8,16 +8,19 @@ use Illuminate\Support\Arr; use Laravel\SerializableClosure\SerializableClosure; -class Factory +class ProcessDriver { /** - * Create a new concurrency factory instance. + * Create a new process based concurrency driver. */ public function __construct(protected ProcessFactory $processFactory) { // } + /** + * Run the given tasks concurrently and return an array containing the results. + */ public function run(Closure|array $tasks): array { $results = $this->processFactory->pool(function (Pool $pool) use ($tasks) { @@ -42,7 +45,7 @@ public function run(Closure|array $tasks): array } /** - * Start the given task(s) in the background. + * Start the given tasks in the background. */ public function background(Closure|array $tasks): void { diff --git a/src/Illuminate/Concurrency/SyncDriver.php b/src/Illuminate/Concurrency/SyncDriver.php new file mode 100644 index 000000000000..cb3e9eda814b --- /dev/null +++ b/src/Illuminate/Concurrency/SyncDriver.php @@ -0,0 +1,27 @@ +map( + fn ($task) => $task() + )->all(); + } + + /** + * Start the given tasks in the background. + */ + public function background(Closure|array $tasks): void + { + collect(Arr::wrap($tasks))->each(fn ($task) => $task()); + } +} diff --git a/src/Illuminate/Contracts/Concurrency/Driver.php b/src/Illuminate/Contracts/Concurrency/Driver.php new file mode 100644 index 000000000000..ce8f2ae0d900 --- /dev/null +++ b/src/Illuminate/Contracts/Concurrency/Driver.php @@ -0,0 +1,18 @@ +app = $app; + + return $this; + } + /** * Dynamically call the default instance. * From c89d85a52085616c2ff41c6ac58f4ea5719247cc Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 7 Feb 2024 14:17:41 +0100 Subject: [PATCH 18/48] formatting --- .../Concurrency/ConcurrencyManager.php | 19 +++++++++++++- src/Illuminate/Concurrency/ForkDriver.php | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Concurrency/ForkDriver.php diff --git a/src/Illuminate/Concurrency/ConcurrencyManager.php b/src/Illuminate/Concurrency/ConcurrencyManager.php index 8beb11e687ef..347036aedd99 100644 --- a/src/Illuminate/Concurrency/ConcurrencyManager.php +++ b/src/Illuminate/Concurrency/ConcurrencyManager.php @@ -4,6 +4,8 @@ use Illuminate\Process\Factory as ProcessFactory; use Illuminate\Support\MultipleInstanceManager; +use RuntimeException; +use Spatie\Fork\Fork; /** * @mixin \Illuminate\Contracts\Concurrency\Driver @@ -32,6 +34,21 @@ public function createProcessDriver(array $config) return new ProcessDriver($this->app->make(ProcessFactory::class)); } + /** + * Create an instance of the fork concurrency driver. + * + * @param array $config + * @return \Illuminate\Concurrency\SyncDriver + */ + public function createForkDriver(array $config) + { + if (! class_exists(Fork::class)) { + throw new RuntimeException('Please install the "spatie/fork" Composer package in order to utilize the "fork" driver.'); + } + + return new ForkDriver; + } + /** * Create an instance of the sync concurrency driver. * @@ -50,7 +67,7 @@ public function createSyncDriver(array $config) */ public function getDefaultInstance() { - return $this->app['config']['concurrency.default']; + return $this->app['config']['concurrency.default'] ?? 'process'; } /** diff --git a/src/Illuminate/Concurrency/ForkDriver.php b/src/Illuminate/Concurrency/ForkDriver.php new file mode 100644 index 000000000000..73cc6b0c92c4 --- /dev/null +++ b/src/Illuminate/Concurrency/ForkDriver.php @@ -0,0 +1,26 @@ +run(...Arr::wrap($tasks)); + } + + /** + * Start the given tasks in the background. + */ + public function background(Closure|array $tasks): void + { + $this->run($tasks); + } +} From ecd894927f7a4216783d4777f59217728e332eac Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 18 Jul 2024 14:25:11 -0500 Subject: [PATCH 19/48] block on web --- src/Illuminate/Concurrency/ConcurrencyManager.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Concurrency/ConcurrencyManager.php b/src/Illuminate/Concurrency/ConcurrencyManager.php index 347036aedd99..2dc189f9af0a 100644 --- a/src/Illuminate/Concurrency/ConcurrencyManager.php +++ b/src/Illuminate/Concurrency/ConcurrencyManager.php @@ -42,6 +42,10 @@ public function createProcessDriver(array $config) */ public function createForkDriver(array $config) { + if (! $this->app->runningInConsole()) { + throw new RuntimeException('Due to PHP limitations, the fork driver may not be used within web requests.'); + } + if (! class_exists(Fork::class)) { throw new RuntimeException('Please install the "spatie/fork" Composer package in order to utilize the "fork" driver.'); } From 5e68fb34f948120757365f5ff0369f48b0b130f2 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 18 Jul 2024 14:25:31 -0500 Subject: [PATCH 20/48] fix docblock --- src/Illuminate/Concurrency/ConcurrencyManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Concurrency/ConcurrencyManager.php b/src/Illuminate/Concurrency/ConcurrencyManager.php index 2dc189f9af0a..27646401bd21 100644 --- a/src/Illuminate/Concurrency/ConcurrencyManager.php +++ b/src/Illuminate/Concurrency/ConcurrencyManager.php @@ -38,7 +38,7 @@ public function createProcessDriver(array $config) * Create an instance of the fork concurrency driver. * * @param array $config - * @return \Illuminate\Concurrency\SyncDriver + * @return \Illuminate\Concurrency\ForkDriver */ public function createForkDriver(array $config) { From 65556fa0a43d6cb72ea7c56ee4f6899d05b397f7 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 19 Jul 2024 12:52:48 -0500 Subject: [PATCH 21/48] first pass at local temporary files --- .../Filesystem/FilesystemManager.php | 22 +++-- .../Filesystem/FilesystemServiceProvider.php | 40 +++++++- .../Filesystem/LocalFilesystemAdapter.php | 96 +++++++++++++++++++ .../Configuration/ApplicationBuilder.php | 13 +++ 4 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 src/Illuminate/Filesystem/LocalFilesystemAdapter.php diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index 20ba1dd054f9..0fa06eaf6d1f 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -137,19 +137,19 @@ protected function resolve($name, $config = null) throw new InvalidArgumentException("Disk [{$name}] does not have a configured driver."); } - $name = $config['driver']; + $driver = $config['driver']; - if (isset($this->customCreators[$name])) { + if (isset($this->customCreators[$driver])) { return $this->callCustomCreator($config); } - $driverMethod = 'create'.ucfirst($name).'Driver'; + $driverMethod = 'create'.ucfirst($driver).'Driver'; if (! method_exists($this, $driverMethod)) { - throw new InvalidArgumentException("Driver [{$name}] is not supported."); + throw new InvalidArgumentException("Driver [{$driver}] is not supported."); } - return $this->{$driverMethod}($config); + return $this->{$driverMethod}($config, $name); } /** @@ -167,9 +167,10 @@ protected function callCustomCreator(array $config) * Create an instance of the local driver. * * @param array $config + * @param string $name * @return \Illuminate\Contracts\Filesystem\Filesystem */ - public function createLocalDriver(array $config) + public function createLocalDriver(array $config, string $name) { $visibility = PortableVisibilityConverter::fromArray( $config['permissions'] ?? [], @@ -184,7 +185,14 @@ public function createLocalDriver(array $config) $config['root'], $visibility, $config['lock'] ?? LOCK_EX, $links ); - return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config); + return (new LocalFilesystemAdapter( + $this->createFlysystem($adapter, $config), $adapter, $config + ))->diskName( + $name + )->shouldServeSignedUrls( + $config['serve'] ?? false, + fn () => $this->app['url'], + ); } /** diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index ff348a224921..25623d016e40 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -2,7 +2,11 @@ namespace Illuminate\Filesystem; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; +use League\Flysystem\PathTraversalDetected; class FilesystemServiceProvider extends ServiceProvider { @@ -14,8 +18,8 @@ class FilesystemServiceProvider extends ServiceProvider public function register() { $this->registerNativeFilesystem(); - $this->registerFlysystem(); + $this->registerFileServing(); } /** @@ -60,6 +64,40 @@ protected function registerManager() }); } + /** + * Register protected file serving. + * + * @return void + */ + protected function registerFileServing() + { + foreach ($this->app['config']['filesystems.disks'] ?? [] as $disk => $config) { + if ($config['driver'] !== 'local' || ! ($config['serve'] ?? false)) { + continue; + } + + $this->app->booted(function () use ($disk, $config) { + $path = isset($config['url']) + ? rtrim(parse_url($config['url'])['path'], '/') + : '/storage'; + + Route::get($path.'/{file}', function (Request $request, $file) use ($disk, $config) { + if (! $request->hasValidRelativeSignature()) { + abort(403); + } + + try { + return Storage::disk($disk)->exists($file) + ? Storage::disk($disk)->download($file) + : abort(404); + } catch (PathTraversalDetected $e) { + abort(404); + } + })->where('file', '.*')->name('storage.'.$disk); + }); + } + } + /** * Get the default file driver. * diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php new file mode 100644 index 000000000000..3231ff34a679 --- /dev/null +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -0,0 +1,96 @@ +shouldServeSignedUrls && $this->urlGeneratorResolver instanceof Closure; + } + + /** + * Get a temporary URL for the file at the given path. + * + * @param string $path + * @param \DateTimeInterface $expiration + * @param array $options + * @return string + */ + public function temporaryUrl($path, $expiration, array $options = []) + { + if (! $this->providesTemporaryUrls()) { + throw new RuntimeException('This driver does not support creating temporary URLs.'); + } + + $url = call_user_func($this->urlGeneratorResolver); + + return $url->to($url->temporarySignedRoute( + 'storage.'.$this->disk, + $expiration, + ['file' => $path], + absolute: false + )); + } + + /** + * Specify the name of the disk the adapter is managing. + * + * @param string $disk + * @return $this + */ + public function diskName(string $disk) + { + $this->disk = $disk; + + return $this; + } + + /** + * Indiate that signed URLs should serve the corresponding files. + * + * @param bool $serve + * @param \Closure $urlGeneratorResolver + * @return $this + */ + public function shouldServeSignedUrls(bool $serve = true, ?Closure $urlGeneratorResolver = null) + { + $this->shouldServeSignedUrls = $serve; + $this->urlGeneratorResolver = $urlGeneratorResolver; + + return $this; + } +} diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index 6065424e1198..d32c60e4deae 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -15,8 +15,10 @@ use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\View; use Laravel\Folio\Folio; +use League\Flysystem\PathTraversalDetected; class ApplicationBuilder { @@ -27,6 +29,13 @@ class ApplicationBuilder */ protected array $pendingProviders = []; + /** + * Any additional routing callbacks that should be invoked while registering routes. + * + * @var array + */ + protected array $additionalRoutingCallbacks = []; + /** * The Folio / page middleware that have been defined by the user. * @@ -222,6 +231,10 @@ protected function buildRoutingCallback(array|string|null $web, } } + foreach ($this->additionalRoutingCallbacks as $callback) { + $callback(); + } + if (is_string($pages) && realpath($pages) !== false && class_exists(Folio::class)) { From f467116fca404afa3bea4c68608c21b7f9d2d46f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 19 Jul 2024 12:53:01 -0500 Subject: [PATCH 22/48] remove imports --- src/Illuminate/Foundation/Configuration/ApplicationBuilder.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index d32c60e4deae..89ec3b9cc50f 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -15,10 +15,8 @@ use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\View; use Laravel\Folio\Folio; -use League\Flysystem\PathTraversalDetected; class ApplicationBuilder { From 69b658bbeef8ab2601ab8416fc6643a7c23ac11a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 19 Jul 2024 13:05:02 -0500 Subject: [PATCH 23/48] docblock --- src/Illuminate/Filesystem/LocalFilesystemAdapter.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index 3231ff34a679..6801ffc19ee0 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -39,7 +39,9 @@ class LocalFilesystemAdapter extends FilesystemAdapter */ public function providesTemporaryUrls() { - return $this->shouldServeSignedUrls && $this->urlGeneratorResolver instanceof Closure; + return $this->temporaryUrlCallback || ( + $this->shouldServeSignedUrls && $this->urlGeneratorResolver instanceof Closure + ); } /** @@ -52,6 +54,12 @@ public function providesTemporaryUrls() */ public function temporaryUrl($path, $expiration, array $options = []) { + if ($this->temporaryUrlCallback) { + return $this->temporaryUrlCallback->bindTo($this, static::class)( + $path, $expiration, $options + ); + } + if (! $this->providesTemporaryUrls()) { throw new RuntimeException('This driver does not support creating temporary URLs.'); } @@ -83,7 +91,7 @@ public function diskName(string $disk) * Indiate that signed URLs should serve the corresponding files. * * @param bool $serve - * @param \Closure $urlGeneratorResolver + * @param \Closure|null $urlGeneratorResolver * @return $this */ public function shouldServeSignedUrls(bool $serve = true, ?Closure $urlGeneratorResolver = null) From d2db712ba950497be354f11283c6c8c8cfc553c0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 24 Jul 2024 15:54:08 -0500 Subject: [PATCH 24/48] work on routing --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 25623d016e40..3c06f37ec1f5 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -82,7 +82,8 @@ protected function registerFileServing() : '/storage'; Route::get($path.'/{file}', function (Request $request, $file) use ($disk, $config) { - if (! $request->hasValidRelativeSignature()) { + if (($config['visibility'] ?? 'private') !== 'public' && + ! $request->hasValidRelativeSignature()) { abort(403); } From 989a5f5c833047acc0571b2ac3e6c01c3ae45bcf Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 24 Jul 2024 21:16:54 -0500 Subject: [PATCH 25/48] allow customization of serve behavior --- .../Filesystem/FilesystemAdapter.php | 33 +++++++++++++++++++ .../Filesystem/FilesystemServiceProvider.php | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 088135358fcf..cb75a870abdb 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystemContract; use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract; use Illuminate\Http\File; +use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -73,6 +74,13 @@ class FilesystemAdapter implements CloudFilesystemContract */ protected $prefixer; + /** + * The file server callback. + * + * @var \Closure|null + */ + protected $serveCallback; + /** * The temporary URL builder callback. * @@ -313,6 +321,20 @@ public function response($path, $name = null, array $headers = [], $disposition return $response; } + /** + * Create a streamed download response for a given file. + * + * @param \Illuminate\Http\Request $request + * @param string $path + * @return \Symfony\Component\HttpFoundation\StreamedResponse + */ + public function serve(Request $request, $path) + { + return isset($this->serveCallback) + ? call_user_func($this->serveCallback, $request, $path) + : $this->download($path); + } + /** * Create a streamed download response for a given file. * @@ -948,6 +970,17 @@ protected function parseVisibility($visibility) }; } + /** + * Define a custom callback that generates file download responses. + * + * @param \Closure $callback + * @return void + */ + public function serveUsing(Closure $callback) + { + $this->serveCallback = $callback; + } + /** * Define a custom temporary URL builder callback. * diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 3c06f37ec1f5..8b2bf3f00e7e 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -82,14 +82,14 @@ protected function registerFileServing() : '/storage'; Route::get($path.'/{file}', function (Request $request, $file) use ($disk, $config) { - if (($config['visibility'] ?? 'private') !== 'public' && + if (($config['visibility'] ?? 'private') === 'private' && ! $request->hasValidRelativeSignature()) { abort(403); } try { return Storage::disk($disk)->exists($file) - ? Storage::disk($disk)->download($file) + ? Storage::disk($disk)->serve($request, $file) : abort(404); } catch (PathTraversalDetected $e) { abort(404); From 10580236e4eba3bcbbf125a2d450df165c1a62a9 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 24 Jul 2024 22:12:27 -0500 Subject: [PATCH 26/48] work on local file serving --- .../Filesystem/FilesystemAdapter.php | 9 ++++++--- .../Filesystem/FilesystemServiceProvider.php | 18 ++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index cb75a870abdb..e4a90420dc1e 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -326,13 +326,15 @@ public function response($path, $name = null, array $headers = [], $disposition * * @param \Illuminate\Http\Request $request * @param string $path + * @param string|null $name + * @param array $headers * @return \Symfony\Component\HttpFoundation\StreamedResponse */ - public function serve(Request $request, $path) + public function serve(Request $request, $path, $name = null, array $headers = []) { return isset($this->serveCallback) - ? call_user_func($this->serveCallback, $request, $path) - : $this->download($path); + ? call_user_func($this->serveCallback, $request, $path, $headers) + : $this->response($path, $name, $headers); } /** @@ -340,6 +342,7 @@ public function serve(Request $request, $path) * * @param string $path * @param string|null $name + * @param array $headers * @return \Symfony\Component\HttpFoundation\StreamedResponse */ public function download($path, $name = null, array $headers = []) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 8b2bf3f00e7e..e89fa8fb9932 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -84,13 +84,23 @@ protected function registerFileServing() Route::get($path.'/{file}', function (Request $request, $file) use ($disk, $config) { if (($config['visibility'] ?? 'private') === 'private' && ! $request->hasValidRelativeSignature()) { - abort(403); + abort($this->app->isProduction() ? 404 : 403); } try { - return Storage::disk($disk)->exists($file) - ? Storage::disk($disk)->serve($request, $file) - : abort(404); + abort_unless(Storage::disk($disk)->exists($file), 404); + + $headers = [ + 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; sandbox", + ]; + + $response = Storage::disk($disk)->serve($request, $file, headers: $headers); + + if (! $response->headers->has('Content-Security-Policy')) { + $response->headers->replace($headers); + } + + return $response; } catch (PathTraversalDetected $e) { abort(404); } From 3de33316b6ba73042b46d579426cd4a1573a3eaf Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 24 Jul 2024 22:26:14 -0500 Subject: [PATCH 27/48] route caching --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index e89fa8fb9932..d8dc9094999e 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -77,6 +77,10 @@ protected function registerFileServing() } $this->app->booted(function () use ($disk, $config) { + if ($this->app->routesAreCached()) { + return; + } + $path = isset($config['url']) ? rtrim(parse_url($config['url'])['path'], '/') : '/storage'; From b6656e3d821c69874b04f2fd974a0ba4f1c14f7b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 24 Jul 2024 22:28:31 -0500 Subject: [PATCH 28/48] move route caching check --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index d8dc9094999e..938ca0151c0c 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -71,16 +71,16 @@ protected function registerManager() */ protected function registerFileServing() { + if ($this->app->routesAreCached()) { + return; + } + foreach ($this->app['config']['filesystems.disks'] ?? [] as $disk => $config) { if ($config['driver'] !== 'local' || ! ($config['serve'] ?? false)) { continue; } $this->app->booted(function () use ($disk, $config) { - if ($this->app->routesAreCached()) { - return; - } - $path = isset($config['url']) ? rtrim(parse_url($config['url'])['path'], '/') : '/storage'; From 9913bbc53efe476481bc12ed336f8bba22e58a0d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 25 Jul 2024 10:47:42 -0500 Subject: [PATCH 29/48] extract file --- .../Filesystem/FilesystemServiceProvider.php | 44 +++++--------- .../Filesystem/LocalFilesystemAdapter.php | 2 +- src/Illuminate/Filesystem/ServeFile.php | 59 +++++++++++++++++++ 3 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 src/Illuminate/Filesystem/ServeFile.php diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 938ca0151c0c..d4c480bcbb66 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -4,9 +4,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; -use League\Flysystem\PathTraversalDetected; class FilesystemServiceProvider extends ServiceProvider { @@ -76,43 +74,33 @@ protected function registerFileServing() } foreach ($this->app['config']['filesystems.disks'] ?? [] as $disk => $config) { - if ($config['driver'] !== 'local' || ! ($config['serve'] ?? false)) { + if (! $this->serveable($config)) { continue; } $this->app->booted(function () use ($disk, $config) { - $path = isset($config['url']) + $uri = isset($config['url']) ? rtrim(parse_url($config['url'])['path'], '/') : '/storage'; - Route::get($path.'/{file}', function (Request $request, $file) use ($disk, $config) { - if (($config['visibility'] ?? 'private') === 'private' && - ! $request->hasValidRelativeSignature()) { - abort($this->app->isProduction() ? 404 : 403); - } - - try { - abort_unless(Storage::disk($disk)->exists($file), 404); - - $headers = [ - 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; sandbox", - ]; - - $response = Storage::disk($disk)->serve($request, $file, headers: $headers); - - if (! $response->headers->has('Content-Security-Policy')) { - $response->headers->replace($headers); - } - - return $response; - } catch (PathTraversalDetected $e) { - abort(404); - } - })->where('file', '.*')->name('storage.'.$disk); + Route::get($uri.'/{path}', function (Request $request, $path) use ($disk, $config) { + return (new ServeFile($disk, $config, $this->app->isProduction()))($request, $path); + })->where('path', '.*')->name('storage.'.$disk); }); } } + /** + * Determine if the disk is serveable. + * + * @param array $config + * @return bool + */ + protected function serveable(array $config) + { + return $config['driver'] === 'local' && ($config['serve'] ?? false); + } + /** * Get the default file driver. * diff --git a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php index 6801ffc19ee0..2c85ce1081e0 100644 --- a/src/Illuminate/Filesystem/LocalFilesystemAdapter.php +++ b/src/Illuminate/Filesystem/LocalFilesystemAdapter.php @@ -69,7 +69,7 @@ public function temporaryUrl($path, $expiration, array $options = []) return $url->to($url->temporarySignedRoute( 'storage.'.$this->disk, $expiration, - ['file' => $path], + ['path' => $path], absolute: false )); } diff --git a/src/Illuminate/Filesystem/ServeFile.php b/src/Illuminate/Filesystem/ServeFile.php new file mode 100644 index 000000000000..47a3fa6d187b --- /dev/null +++ b/src/Illuminate/Filesystem/ServeFile.php @@ -0,0 +1,59 @@ +hasValidSignature($request), + $this->isProduction ? 404 : 403 + ); + + try { + abort_unless(Storage::disk($this->disk)->exists($path), 404); + + $headers = [ + 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; sandbox", + ]; + + return tap( + Storage::disk($this->disk)->serve($request, $path, headers: $headers), + function ($response) use ($headers) { + if (! $response->headers->has('Content-Security-Policy')) { + $response->headers->replace($headers); + } + } + ); + } catch (PathTraversalDetected $e) { + abort(404); + } + } + + /** + * Determine if the request has a valid signature if applicable. + */ + protected function hasValidSignature(Request $request): bool + { + return ($this->config['visibility'] ?? 'private') === 'public' || + $request->hasValidRelativeSignature(); + } +} From 318218cab7c03ff235804f3634886abadd5e42a0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 25 Jul 2024 10:49:28 -0500 Subject: [PATCH 30/48] formatting --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index d4c480bcbb66..41015bf9e8da 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -74,7 +74,7 @@ protected function registerFileServing() } foreach ($this->app['config']['filesystems.disks'] ?? [] as $disk => $config) { - if (! $this->serveable($config)) { + if (! $this->shouldServeFiles($config)) { continue; } @@ -96,7 +96,7 @@ protected function registerFileServing() * @param array $config * @return bool */ - protected function serveable(array $config) + protected function shouldServeFiles(array $config) { return $config['driver'] === 'local' && ($config['serve'] ?? false); } From 14a9ae23e34474a0d40b7f616eddda34e634a6d0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 25 Jul 2024 10:49:59 -0500 Subject: [PATCH 31/48] formatting --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 41015bf9e8da..100841527286 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -83,7 +83,7 @@ protected function registerFileServing() ? rtrim(parse_url($config['url'])['path'], '/') : '/storage'; - Route::get($uri.'/{path}', function (Request $request, $path) use ($disk, $config) { + Route::get($uri.'/{path}', function (Request $request, string $path) use ($disk, $config) { return (new ServeFile($disk, $config, $this->app->isProduction()))($request, $path); })->where('path', '.*')->name('storage.'.$disk); }); From 984c4a2dd904763408daade8ba86ba96e3d09cc2 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 25 Jul 2024 10:50:20 -0500 Subject: [PATCH 32/48] formatting --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 100841527286..98788d052cdd 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -84,7 +84,11 @@ protected function registerFileServing() : '/storage'; Route::get($uri.'/{path}', function (Request $request, string $path) use ($disk, $config) { - return (new ServeFile($disk, $config, $this->app->isProduction()))($request, $path); + return (new ServeFile( + $disk, + $config, + $this->app->isProduction() + ))($request, $path); })->where('path', '.*')->name('storage.'.$disk); }); } From 8ac56182e4cd611e84fedf690e2bb26c0b710cda Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 31 Jul 2024 19:22:59 +0300 Subject: [PATCH 33/48] add test --- .../Filesystem/FilesystemServiceProvider.php | 11 +++- .../Integration/Filesystem/ServeFileTest.php | 58 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Filesystem/ServeFileTest.php diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 98788d052cdd..096592e80597 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -8,6 +8,16 @@ class FilesystemServiceProvider extends ServiceProvider { + /** + * Bootstrap the filesystem. + * + * @return void + */ + public function boot() + { + $this->registerFileServing(); + } + /** * Register the service provider. * @@ -17,7 +27,6 @@ public function register() { $this->registerNativeFilesystem(); $this->registerFlysystem(); - $this->registerFileServing(); } /** diff --git a/tests/Integration/Filesystem/ServeFileTest.php b/tests/Integration/Filesystem/ServeFileTest.php new file mode 100644 index 000000000000..c5219761ab10 --- /dev/null +++ b/tests/Integration/Filesystem/ServeFileTest.php @@ -0,0 +1,58 @@ +afterApplicationCreated(function () { + Storage::put('serve-file-test.txt', 'Hello World'); + }); + + $this->beforeApplicationDestroyed(function () { + Storage::delete('serve-file-test.txt'); + }); + + parent::setUp(); + } + + public function testItCanServeAnExistingFile() + { + $url = Storage::temporaryUrl('serve-file-test.txt', now()->addMinutes(1)); + + $response = $this->get($url); + + $this->assertEquals('Hello World', $response->streamedContent()); + } + + public function testItWill404OnMissingFile() + { + $url = Storage::temporaryUrl('serve-missing-test.txt', now()->addMinutes(1)); + + $response = $this->get($url); + + $response->assertNotFound(); + } + + public function testItWill403OnWrongSignature() + { + $url = Storage::temporaryUrl('serve-file-test.txt', now()->addMinutes(1)); + + $url = $url.'c'; + + $response = $this->get($url); + + $response->assertForbidden(); + } + + protected function getEnvironmentSetup($app) + { + tap($app['config'], function ($config) { + $config->set('filesystems.disks.local.serve', true); + }); + } +} From 308f1424edb160c77062cf84b1e55ed758e24734 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 1 Aug 2024 16:27:41 -0500 Subject: [PATCH 34/48] formatting --- src/Illuminate/Filesystem/FilesystemServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemServiceProvider.php b/src/Illuminate/Filesystem/FilesystemServiceProvider.php index 096592e80597..a6ad8e1a7b34 100644 --- a/src/Illuminate/Filesystem/FilesystemServiceProvider.php +++ b/src/Illuminate/Filesystem/FilesystemServiceProvider.php @@ -15,7 +15,7 @@ class FilesystemServiceProvider extends ServiceProvider */ public function boot() { - $this->registerFileServing(); + $this->serveFiles(); } /** @@ -76,7 +76,7 @@ protected function registerManager() * * @return void */ - protected function registerFileServing() + protected function serveFiles() { if ($this->app->routesAreCached()) { return; From 1dd890aef92ca003122b8f087fd0e7bb2b4ac0bd Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 9 Aug 2024 06:56:14 -0500 Subject: [PATCH 35/48] add cache control --- src/Illuminate/Filesystem/ServeFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/ServeFile.php b/src/Illuminate/Filesystem/ServeFile.php index 47a3fa6d187b..4ab2bd01df2d 100644 --- a/src/Illuminate/Filesystem/ServeFile.php +++ b/src/Illuminate/Filesystem/ServeFile.php @@ -27,11 +27,11 @@ public function __invoke(Request $request, string $path) $this->hasValidSignature($request), $this->isProduction ? 404 : 403 ); - try { abort_unless(Storage::disk($this->disk)->exists($path), 404); $headers = [ + 'Cache-Control' => "no-store, no-cache, must-revalidate, max-age=0", 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; sandbox", ]; From cc64665861408932ff91d9bf0c4d20f6f5d6e115 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 9 Aug 2024 13:28:17 -0500 Subject: [PATCH 36/48] use defer in concurrency --- src/Illuminate/Concurrency/ForkDriver.php | 7 ++++--- src/Illuminate/Concurrency/ProcessDriver.php | 17 ++++++++++------- src/Illuminate/Concurrency/SyncDriver.php | 7 ++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Concurrency/ForkDriver.php b/src/Illuminate/Concurrency/ForkDriver.php index 73cc6b0c92c4..ad1e62f156a5 100644 --- a/src/Illuminate/Concurrency/ForkDriver.php +++ b/src/Illuminate/Concurrency/ForkDriver.php @@ -3,6 +3,7 @@ namespace Illuminate\Concurrency; use Closure; +use Illuminate\Foundation\Defer\DeferredCallback; use Illuminate\Support\Arr; use Spatie\Fork\Fork; @@ -17,10 +18,10 @@ public function run(Closure|array $tasks): array } /** - * Start the given tasks in the background. + * Start the given tasks in the background after the current task has finished. */ - public function background(Closure|array $tasks): void + public function defer(Closure|array $tasks): DeferredCallback { - $this->run($tasks); + return defer(fn () => $this->run($tasks)); } } diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index ce79964ddee9..4f660b89a4fe 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -3,6 +3,7 @@ namespace Illuminate\Concurrency; use Closure; +use Illuminate\Foundation\Defer\DeferredCallback; use Illuminate\Process\Factory as ProcessFactory; use Illuminate\Process\Pool; use Illuminate\Support\Arr; @@ -45,14 +46,16 @@ public function run(Closure|array $tasks): array } /** - * Start the given tasks in the background. + * Start the given tasks in the background after the current task has finished. */ - public function background(Closure|array $tasks): void + public function defer(Closure|array $tasks): DeferredCallback { - foreach (Arr::wrap($tasks) as $task) { - $this->processFactory->path(base_path())->env([ - 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), - ])->run('php artisan invoke-serialized-closure 2>&1 &'); - } + return defer(function () use ($tasks) { + foreach (Arr::wrap($tasks) as $task) { + $this->processFactory->path(base_path())->env([ + 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), + ])->run('php artisan invoke-serialized-closure 2>&1 &'); + } + }); } } diff --git a/src/Illuminate/Concurrency/SyncDriver.php b/src/Illuminate/Concurrency/SyncDriver.php index cb3e9eda814b..10580124f3bd 100644 --- a/src/Illuminate/Concurrency/SyncDriver.php +++ b/src/Illuminate/Concurrency/SyncDriver.php @@ -3,6 +3,7 @@ namespace Illuminate\Concurrency; use Closure; +use Illuminate\Foundation\Defer\DeferredCallback; use Illuminate\Support\Arr; class SyncDriver @@ -18,10 +19,10 @@ public function run(Closure|array $tasks): array } /** - * Start the given tasks in the background. + * Start the given tasks in the background after the current task has finished. */ - public function background(Closure|array $tasks): void + public function defer(Closure|array $tasks): DeferredCallback { - collect(Arr::wrap($tasks))->each(fn ($task) => $task()); + return defer(fn () => collect(Arr::wrap($tasks))->each(fn ($task) => $task())); } } From 14dc1adc0862d1b861b026b035749b69182e5e12 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 9 Aug 2024 14:30:17 -0500 Subject: [PATCH 37/48] allow return from sleep --- src/Illuminate/Support/Sleep.php | 23 +++++++++++++++++++++++ tests/Support/SleepTest.php | 13 +++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index ebb8f4215b03..3b289abc6bf8 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -247,12 +247,35 @@ public function and($duration) return $this; } + /** + * Specify a callback that should be executed after sleeping. + * + * @param callable $then + * @return mixed + */ + public function then(callable $then) + { + $this->goodnight(); + + return $then(); + } + /** * Handle the object's destruction. * * @return void */ public function __destruct() + { + $this->goodnight(); + } + + /** + * Handle the object's destruction. + * + * @return void + */ + protected function goodnight() { if (! $this->shouldSleep) { return; diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index bf6b9123ce50..d2251d1bc000 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -25,11 +25,16 @@ protected function tearDown(): void public function testItSleepsForSeconds() { - $start = microtime(true); - Sleep::for(1)->seconds(); - $end = microtime(true); + $start = microtime(true); + Sleep::for(1)->seconds(); + $end = microtime(true); - $this->assertEqualsWithDelta(1, $end - $start, 0.03); + $this->assertEqualsWithDelta(1, $end - $start, 0.03); + } + + public function testCallbacksMayBeExecutedUsingThen() + { + $this->assertEquals(123, Sleep::for(1)->milliseconds()->then(fn () => 123)); } public function testItSleepsForSecondsWithMilliseconds() From 03d198b663f333a045563a117df59c1cbe53c157 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 9 Aug 2024 14:34:19 -0500 Subject: [PATCH 38/48] adjust sleep behavior --- src/Illuminate/Support/Sleep.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index 3b289abc6bf8..7fe1b7055720 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -61,6 +61,13 @@ class Sleep */ protected $shouldSleep = true; + /** + * Indicates if the instance already slept via `then()`. + * + * @var bool + */ + protected $alreadySlept = false; + /** * Create a new class instance. * @@ -257,6 +264,8 @@ public function then(callable $then) { $this->goodnight(); + $this->alreadySlept = true; + return $then(); } @@ -277,7 +286,7 @@ public function __destruct() */ protected function goodnight() { - if (! $this->shouldSleep) { + if ($this->alreadySlept || ! $this->shouldSleep) { return; } From 7726a92c0fdc20a95e97752607ff9d5551075a24 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 20 Aug 2024 16:44:21 -0500 Subject: [PATCH 39/48] add config file --- config/concurrency.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 config/concurrency.php diff --git a/config/concurrency.php b/config/concurrency.php new file mode 100644 index 000000000000..1d66b701f823 --- /dev/null +++ b/config/concurrency.php @@ -0,0 +1,20 @@ + env('CONCURRENCY_DRIVER', 'process'), + +]; From 1a8853ee82c9951a2e4ef2366f5f793c193c58bb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 21 Aug 2024 14:23:59 -0500 Subject: [PATCH 40/48] testing --- composer.json | 2 +- .../Concurrency/ConcurrencyTest.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Concurrency/ConcurrencyTest.php diff --git a/composer.json b/composer.json index 3dbaa9700751..cf9977afece5 100644 --- a/composer.json +++ b/composer.json @@ -106,7 +106,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.1.5", + "orchestra/testbench-core": "^9.3.0", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php new file mode 100644 index 000000000000..c2f2495d48be --- /dev/null +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -0,0 +1,22 @@ +markTestSkipped('Todo...'); + + [$first, $second] = Concurrency::run([ + fn () => 1 + 1, + fn () => 2 + 2, + ]); + + $this->assertEquals(2, $first); + $this->assertEquals(4, $second); + } +} From d9da8aea9e78ad5f476041df966265ddfc68b573 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 22 Aug 2024 10:22:06 -0500 Subject: [PATCH 41/48] add while to sleep --- src/Illuminate/Support/Sleep.php | 43 ++++++++++++++++++++++++++------ tests/Support/SleepTest.php | 16 ++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index 7fe1b7055720..f65141e12f77 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -3,6 +3,7 @@ namespace Illuminate\Support; use Carbon\CarbonInterval; +use Closure; use DateInterval; use Illuminate\Support\Traits\Macroable; use PHPUnit\Framework\Assert as PHPUnit; @@ -33,6 +34,13 @@ class Sleep */ public $duration; + /** + * The callback that determines if sleeping should continue. + * + * @var \Closure + */ + public $while; + /** * The pending duration to sleep. * @@ -254,6 +262,19 @@ public function and($duration) return $this; } + /** + * Sleep while a given callback returns "true". + * + * @param \Closure $callback + * @return $this + */ + public function while(Closure $callback) + { + $this->while = $callback; + + return $this; + } + /** * Specify a callback that should be executed after sleeping. * @@ -312,16 +333,24 @@ protected function goodnight() $seconds = (int) $remaining->totalSeconds; - if ($seconds > 0) { - sleep($seconds); + $while = $this->while ?: function () { + static $return = [true, false]; - $remaining = $remaining->subSeconds($seconds); - } + return array_shift($return); + }; - $microseconds = (int) $remaining->totalMicroseconds; + while ($while()) { + if ($seconds > 0) { + sleep($seconds); - if ($microseconds > 0) { - usleep($microseconds); + $remaining = $remaining->subSeconds($seconds); + } + + $microseconds = (int) $remaining->totalMicroseconds; + + if ($microseconds > 0) { + usleep($microseconds); + } } } diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index d2251d1bc000..d71f269a559f 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -37,6 +37,22 @@ public function testCallbacksMayBeExecutedUsingThen() $this->assertEquals(123, Sleep::for(1)->milliseconds()->then(fn () => 123)); } + public function testSleepRespectsWhile() + { + $_SERVER['__sleep.while'] = 0; + + $result = Sleep::for(10)->milliseconds()->while(function () { + static $results = [true, true, false]; + $_SERVER['__sleep.while']++; + return array_shift($results); + })->then(fn () => 100); + + $this->assertEquals(3, $_SERVER['__sleep.while']); + $this->assertEquals(100, $result); + + unset($_SERVER['__sleep.while']); + } + public function testItSleepsForSecondsWithMilliseconds() { $start = microtime(true); From eb207ac8b3f1371e86e5177055b9f03dc009c13e Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 24 Aug 2024 20:06:43 -0500 Subject: [PATCH 42/48] add autoload --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index cf9977afece5..75485f2e75e0 100644 --- a/composer.json +++ b/composer.json @@ -131,6 +131,7 @@ "src/Illuminate/Events/functions.php", "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { From 322928505db61115eb0afa22bc41c6a87baaee87 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 24 Aug 2024 20:06:51 -0500 Subject: [PATCH 43/48] add function --- src/Illuminate/Log/functions.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/Illuminate/Log/functions.php diff --git a/src/Illuminate/Log/functions.php b/src/Illuminate/Log/functions.php new file mode 100644 index 000000000000..8cbf3ef65593 --- /dev/null +++ b/src/Illuminate/Log/functions.php @@ -0,0 +1,17 @@ + Date: Mon, 9 Sep 2024 14:17:57 +0000 Subject: [PATCH 44/48] Apply fixes from StyleCI --- src/Illuminate/Cache/Repository.php | 2 +- .../Concurrency/ConcurrencyServiceProvider.php | 1 - .../Console/InvokeSerializedClosureCommand.php | 2 +- src/Illuminate/Filesystem/ServeFile.php | 2 +- .../Foundation/Providers/FoundationServiceProvider.php | 6 ++---- tests/Integration/Cache/RepositoryTest.php | 1 - tests/Support/SleepTest.php | 9 +++++---- 7 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index a0e45fcd3fdf..77fbb52a2e5f 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -505,7 +505,7 @@ public function flexible($key, $ttl, $callback, $lock = null) "illuminate:cache:refresh:lock:{$key}", $lock['seconds'] ?? 0, $lock['owner'] ?? null, - )->get(function () use ($key, $callback, $created, $ttl, $lock) { + )->get(function () use ($key, $callback, $created, $ttl) { if ($created !== $this->get("{$key}:created")) { return; } diff --git a/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php b/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php index 698ccc395598..2afd9b6312e0 100644 --- a/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php +++ b/src/Illuminate/Concurrency/ConcurrencyServiceProvider.php @@ -2,7 +2,6 @@ namespace Illuminate\Concurrency; -use Illuminate\Concurrency\ConcurrencyManager; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider; diff --git a/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php b/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php index a7ad6c5b58c7..faaa4d29c279 100644 --- a/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php +++ b/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php @@ -46,7 +46,7 @@ public function handle() ! is_null($this->argument('code')) => unserialize($this->argument('code')), isset($_SERVER['LARAVEL_INVOKABLE_CLOSURE']) => unserialize($_SERVER['LARAVEL_INVOKABLE_CLOSURE']), default => fn () => null, - })) + })), ])); } catch (Throwable $e) { report($e); diff --git a/src/Illuminate/Filesystem/ServeFile.php b/src/Illuminate/Filesystem/ServeFile.php index 4ab2bd01df2d..0fd577e91db2 100644 --- a/src/Illuminate/Filesystem/ServeFile.php +++ b/src/Illuminate/Filesystem/ServeFile.php @@ -31,7 +31,7 @@ public function __invoke(Request $request, string $path) abort_unless(Storage::disk($this->disk)->exists($path), 404); $headers = [ - 'Cache-Control' => "no-store, no-cache, must-revalidate, max-age=0", + 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; sandbox", ]; diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 11cef04e3e98..6711dd2d770f 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -198,14 +198,12 @@ protected function registerDeferHandler() $this->app->scoped(DeferredCallbackCollection::class); $this->app['events']->listen(function (CommandFinished $event) { - app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => - app()->runningInConsole() && ($event->exitCode === 0 || $callback->always) + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => app()->runningInConsole() && ($event->exitCode === 0 || $callback->always) ); }); $this->app['events']->listen(function (JobAttempted $event) { - app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => - $event->connectionName !== 'sync' && ($event->successful() || $callback->always) + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => $event->connectionName !== 'sync' && ($event->successful() || $callback->always) ); }); } diff --git a/tests/Integration/Cache/RepositoryTest.php b/tests/Integration/Cache/RepositoryTest.php index 1d453a66f269..77fd8c95302c 100644 --- a/tests/Integration/Cache/RepositoryTest.php +++ b/tests/Integration/Cache/RepositoryTest.php @@ -84,7 +84,6 @@ public function testStaleWhileRevalidate(): void $this->assertSame(3, $cache->get('foo')); $this->assertSame(946684832, $cache->get('foo:created')); - // Now lets see what happens when another request, job, or command is // also trying to refresh the same key at the same time. Will push past // the "fresh" TTL and register a deferred callback. diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index d71f269a559f..809ba41b8629 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -25,11 +25,11 @@ protected function tearDown(): void public function testItSleepsForSeconds() { - $start = microtime(true); - Sleep::for(1)->seconds(); - $end = microtime(true); + $start = microtime(true); + Sleep::for(1)->seconds(); + $end = microtime(true); - $this->assertEqualsWithDelta(1, $end - $start, 0.03); + $this->assertEqualsWithDelta(1, $end - $start, 0.03); } public function testCallbacksMayBeExecutedUsingThen() @@ -44,6 +44,7 @@ public function testSleepRespectsWhile() $result = Sleep::for(10)->milliseconds()->while(function () { static $results = [true, true, false]; $_SERVER['__sleep.while']++; + return array_shift($results); })->then(fn () => 100); From d71f4ee66a9bce66ca36a8e74671f8f0366e50c2 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 10 Sep 2024 00:21:16 +0800 Subject: [PATCH 45/48] Concurrency Improvements (#52713) * Concurrency Improvements 1. Add a working tests for Concurrency 2. Update composer.json to include replace `illuminate/concurrency` 3. Update PHP versions 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 --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- composer.json | 3 +- src/Illuminate/Concurrency/ForkDriver.php | 1 + src/Illuminate/Concurrency/composer.json | 2 +- .../Concurrency/ConcurrencyTest.php | 29 +++++++++++++++---- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 75485f2e75e0..8a78ce9b10cf 100644 --- a/composer.json +++ b/composer.json @@ -64,6 +64,7 @@ "illuminate/bus": "self.version", "illuminate/cache": "self.version", "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", "illuminate/conditionable": "self.version", "illuminate/config": "self.version", "illuminate/console": "self.version", @@ -106,7 +107,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.3.0", + "orchestra/testbench-core": "^9.4.0", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", diff --git a/src/Illuminate/Concurrency/ForkDriver.php b/src/Illuminate/Concurrency/ForkDriver.php index ad1e62f156a5..beca07d7e44b 100644 --- a/src/Illuminate/Concurrency/ForkDriver.php +++ b/src/Illuminate/Concurrency/ForkDriver.php @@ -14,6 +14,7 @@ class ForkDriver */ public function run(Closure|array $tasks): array { + /** @phpstan-ignore class.notFound */ return Fork::new()->run(...Arr::wrap($tasks)); } diff --git a/src/Illuminate/Concurrency/composer.json b/src/Illuminate/Concurrency/composer.json index d0ac214062e0..6476f73aafde 100644 --- a/src/Illuminate/Concurrency/composer.json +++ b/src/Illuminate/Concurrency/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^8.0.2", + "php": "^8.2", "illuminate/process": "^11.0", "laravel/serializable-closure": "^1.2.2" }, diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index c2f2495d48be..76012712c375 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -2,19 +2,36 @@ namespace Illuminate\Tests\Integration\Console; -use Illuminate\Support\Facades\Concurrency; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +#[RequiresOperatingSystem('Linux|DAR')] class ConcurrencyTest extends TestCase { + protected function setUp(): void + { + $this->defineCacheRoutes(<< 1 + 1, + fn () => 2 + 2, + ]); +}); +PHP); + + parent::setUp(); + } + public function testWorkCanBeDistributed() { - $this->markTestSkipped('Todo...'); + $response = $this->get('concurrency') + ->assertOk(); - [$first, $second] = Concurrency::run([ - fn () => 1 + 1, - fn () => 2 + 2, - ]); + [$first, $second] = $response->original; $this->assertEquals(2, $first); $this->assertEquals(4, $second); From 419893cdd555f4c41a6684c5686cb1365d532837 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 11 Sep 2024 14:57:02 -0500 Subject: [PATCH 46/48] adjust php binary --- src/Illuminate/Concurrency/ProcessDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index 4f660b89a4fe..80c8c896d483 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -28,7 +28,7 @@ public function run(Closure|array $tasks): array foreach (Arr::wrap($tasks) as $task) { $pool->path(base_path())->env([ 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), - ])->command('php artisan invoke-serialized-closure'); + ])->command(PHP_BINARY.' artisan invoke-serialized-closure'); } })->start()->wait(); From 16b85a3f3dad0ac9abd070d56da81cfc213c39b5 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 11 Sep 2024 15:01:17 -0500 Subject: [PATCH 47/48] php executable finder --- src/Illuminate/Concurrency/ProcessDriver.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index 80c8c896d483..2619dea3b7c8 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -8,6 +8,7 @@ use Illuminate\Process\Pool; use Illuminate\Support\Arr; use Laravel\SerializableClosure\SerializableClosure; +use Symfony\Component\Process\PhpExecutableFinder; class ProcessDriver { @@ -24,11 +25,13 @@ public function __construct(protected ProcessFactory $processFactory) */ public function run(Closure|array $tasks): array { - $results = $this->processFactory->pool(function (Pool $pool) use ($tasks) { + $php = (new PhpExecutableFinder)->find(); + + $results = $this->processFactory->pool(function (Pool $pool) use ($tasks, $php) { foreach (Arr::wrap($tasks) as $task) { $pool->path(base_path())->env([ 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), - ])->command(PHP_BINARY.' artisan invoke-serialized-closure'); + ])->command($php.' artisan invoke-serialized-closure'); } })->start()->wait(); From 1de6373012b2e4ddf3a8776ab048722434a63b1c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 11 Sep 2024 15:01:59 -0500 Subject: [PATCH 48/48] false arg --- src/Illuminate/Concurrency/ProcessDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index 2619dea3b7c8..0cd0add93ff1 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -25,7 +25,7 @@ public function __construct(protected ProcessFactory $processFactory) */ public function run(Closure|array $tasks): array { - $php = (new PhpExecutableFinder)->find(); + $php = (new PhpExecutableFinder)->find(false); $results = $this->processFactory->pool(function (Pool $pool) use ($tasks, $php) { foreach (Arr::wrap($tasks) as $task) {