diff --git a/composer.json b/composer.json index bf1832b32..b0abcc477 100644 --- a/composer.json +++ b/composer.json @@ -230,7 +230,10 @@ "audit": { "ignore": ["PKSA-z3gr-8qht-p93v"] }, - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": true + } }, "extra": { "hyperf": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 719dbc862..1554534ff 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -39,6 +39,9 @@ parameters: - '#Method Redis::eval\(\) invoked with [0-9] parameters, 1-3 required.#' - '#Access to an undefined property Hypervel\\Queue\\Jobs\\DatabaseJobRecord::\$.*#' - '#Access to an undefined property Hypervel\\Queue\\Contracts\\Job::\$.*#' + # NodeTrait methods - mixed in at app level, not on base Model class + - message: '#Call to an undefined method Hyperf\\Database\\Model\\Model::new(ScopedQuery|NestedSetQuery)\(\)#' + path: src/nested-set/* - '#Call to an undefined method Hyperf\\Database\\Query\\Builder::where[a-zA-Z0-9\\\\_]+#' - '#Call to an undefined method Hyperf\\Database\\Query\\Builder::firstOrFail\(\)#' - '#Access to an undefined property Hyperf\\Collection\\HigherOrderCollectionProxy#' diff --git a/src/bus/src/PendingChain.php b/src/bus/src/PendingChain.php index 8b86c5e30..f51355283 100644 --- a/src/bus/src/PendingChain.php +++ b/src/bus/src/PendingChain.php @@ -100,7 +100,7 @@ public function catch(callable $callback): static */ public function catchCallbacks(): array { - return $this->catchCallbacks ?? []; + return $this->catchCallbacks ?: []; } /** diff --git a/src/cache/src/Contracts/Lock.php b/src/cache/src/Contracts/Lock.php index 98a2669a3..c81301b6a 100644 --- a/src/cache/src/Contracts/Lock.php +++ b/src/cache/src/Contracts/Lock.php @@ -6,6 +6,11 @@ interface Lock { + /** + * Attempt to acquire the lock. + */ + public function acquire(): bool; + /** * Attempt to acquire the lock. */ @@ -16,6 +21,11 @@ public function get(?callable $callback = null): mixed; */ public function block(int $seconds, ?callable $callback = null): mixed; + /** + * Specify the number of milliseconds to sleep in between blocked lock acquisition attempts. + */ + public function betweenBlockedAttemptsSleepFor(int $milliseconds): static; + /** * Release the lock. */ diff --git a/src/console/src/Scheduling/CacheEventMutex.php b/src/console/src/Scheduling/CacheEventMutex.php index 8dda537ee..15aab12c2 100644 --- a/src/console/src/Scheduling/CacheEventMutex.php +++ b/src/console/src/Scheduling/CacheEventMutex.php @@ -32,9 +32,11 @@ public function __construct( */ public function create(Event $event): bool { - if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) { - /* @phpstan-ignore-next-line */ - return $this->cache->store($this->store)->getStore() + $store = $this->cache->store($this->store)->getStore(); + + if ($this->shouldUseLocks($store)) { + /** @var LockProvider&Store $store */ // @phpstan-ignore varTag.nativeType + return $store ->lock($event->mutexName(), $event->expiresAt * 60) ->acquire(); } @@ -51,9 +53,11 @@ public function create(Event $event): bool */ public function exists(Event $event): bool { - if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) { - /* @phpstan-ignore-next-line */ - return ! $this->cache->store($this->store)->getStore() + $store = $this->cache->store($this->store)->getStore(); + + if ($this->shouldUseLocks($store)) { + /** @var LockProvider&Store $store */ // @phpstan-ignore varTag.nativeType + return ! $store ->lock($event->mutexName(), $event->expiresAt * 60) ->get(fn () => true); } @@ -66,9 +70,11 @@ public function exists(Event $event): bool */ public function forget(Event $event): void { - if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) { - /* @phpstan-ignore-next-line */ - $this->cache->store($this->store)->getStore() + $store = $this->cache->store($this->store)->getStore(); + + if ($this->shouldUseLocks($store)) { + /** @var LockProvider&Store $store */ // @phpstan-ignore varTag.nativeType + $store ->lock($event->mutexName(), $event->expiresAt * 60) ->forceRelease(); diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php index 011773682..4bfc1e7da 100644 --- a/src/filesystem/src/FilesystemManager.php +++ b/src/filesystem/src/FilesystemManager.php @@ -79,7 +79,7 @@ public function drive(?string $name = null): Filesystem /** * Get a filesystem instance. */ - public function disk(?string $name = null): FileSystem + public function disk(?string $name = null): Filesystem { $name = $name ?: $this->getDefaultDriver(); @@ -100,7 +100,7 @@ public function cloud(): Cloud /** * Build an on-demand disk. */ - public function build(array|string $config): FileSystem + public function build(array|string $config): Filesystem { return $this->resolve('ondemand', is_array($config) ? $config : [ 'driver' => 'local', @@ -111,7 +111,7 @@ public function build(array|string $config): FileSystem /** * Attempt to get the disk from the local cache. */ - protected function get(string $name): FileSystem + protected function get(string $name): Filesystem { return $this->disks[$name] ?? $this->resolve($name); } @@ -121,7 +121,7 @@ protected function get(string $name): FileSystem * * @throws InvalidArgumentException */ - protected function resolve(string $name, ?array $config = null): FileSystem + protected function resolve(string $name, ?array $config = null): Filesystem { $config ??= $this->getConfig($name); @@ -163,7 +163,7 @@ protected function resolve(string $name, ?array $config = null): FileSystem /** * Call a custom driver creator. */ - protected function callCustomCreator(array $config): FileSystem + protected function callCustomCreator(array $config): Filesystem { return $this->customCreators[$config['driver']]($this->app, $config); } @@ -171,7 +171,7 @@ protected function callCustomCreator(array $config): FileSystem /** * Create an instance of the local driver. */ - public function createLocalDriver(array $config, string $name = 'local'): FileSystem + public function createLocalDriver(array $config, string $name = 'local'): Filesystem { $visibility = PortableVisibilityConverter::fromArray( $config['permissions'] ?? [], @@ -204,7 +204,7 @@ public function createLocalDriver(array $config, string $name = 'local'): FileSy /** * Create an instance of the ftp driver. */ - public function createFtpDriver(array $config): FileSystem + public function createFtpDriver(array $config): Filesystem { if (! isset($config['root'])) { $config['root'] = ''; @@ -219,7 +219,7 @@ public function createFtpDriver(array $config): FileSystem /** * Create an instance of the sftp driver. */ - public function createSftpDriver(array $config): FileSystem + public function createSftpDriver(array $config): Filesystem { /* @phpstan-ignore-next-line */ $provider = SftpConnectionProvider::fromArray($config); @@ -353,7 +353,7 @@ protected function createGcsClient(array $config): GcsClient /** * Create a scoped driver. */ - public function createScopedDriver(array $config): FileSystem + public function createScopedDriver(array $config): Filesystem { if (empty($config['disk'])) { throw new InvalidArgumentException('Scoped disk is missing "disk" configuration option.'); diff --git a/src/foundation/src/Support/Providers/RouteServiceProvider.php b/src/foundation/src/Support/Providers/RouteServiceProvider.php index 48c51cbf9..6ce60b15e 100644 --- a/src/foundation/src/Support/Providers/RouteServiceProvider.php +++ b/src/foundation/src/Support/Providers/RouteServiceProvider.php @@ -12,8 +12,7 @@ class RouteServiceProvider extends ServiceProvider /** * The route files for the application. */ - protected array $routes = [ - ]; + protected array $routes = []; public function boot(): void { diff --git a/src/horizon/src/EventMap.php b/src/horizon/src/EventMap.php index 02798ae92..353f7cccc 100644 --- a/src/horizon/src/EventMap.php +++ b/src/horizon/src/EventMap.php @@ -61,11 +61,9 @@ trait EventMap Listeners\MonitorWaitTimes::class, ], - Events\WorkerProcessRestarting::class => [ - ], + Events\WorkerProcessRestarting::class => [], - Events\SupervisorProcessRestarting::class => [ - ], + Events\SupervisorProcessRestarting::class => [], Events\LongWaitDetected::class => [ Listeners\SendNotification::class, diff --git a/src/horizon/src/ProcessPool.php b/src/horizon/src/ProcessPool.php index ff67fd57a..dd8bed92b 100644 --- a/src/horizon/src/ProcessPool.php +++ b/src/horizon/src/ProcessPool.php @@ -126,8 +126,6 @@ public function markForTermination(WorkerProcess $process): void protected function removeProcesses(int $count): void { array_splice($this->processes, 0, $count); - - $this->processes = array_values($this->processes); } /** diff --git a/src/http-client/src/PendingRequest.php b/src/http-client/src/PendingRequest.php index 1c06f3c97..a491eb028 100644 --- a/src/http-client/src/PendingRequest.php +++ b/src/http-client/src/PendingRequest.php @@ -728,7 +728,7 @@ public function send(string $method, string $url, array $options = []): PromiseI $shouldRetry = null; - return retry($this->tries ?? 1, function ($attempt) use ($method, $url, $options, &$shouldRetry) { + return retry($this->tries ?: 1, function ($attempt) use ($method, $url, $options, &$shouldRetry) { try { return tap( $this->newResponse($this->sendRequest($method, $url, $options)), @@ -780,7 +780,8 @@ function (Response $response) use ($attempt, &$shouldRetry) { throw $exception; } - }, $this->retryDelay ?? 100, function ($exception) use (&$shouldRetry) { + }, $this->retryDelay ?: 100, function ($exception) use (&$shouldRetry) { + // @phpstan-ignore-next-line nullCoalesce.variable $result = $shouldRetry ?? ($this->retryWhenCallback ? call_user_func( $this->retryWhenCallback, $exception, diff --git a/src/http-client/src/Request.php b/src/http-client/src/Request.php index 8bc3f0de8..3ea74e406 100644 --- a/src/http-client/src/Request.php +++ b/src/http-client/src/Request.php @@ -133,7 +133,7 @@ public function data(): array return $this->json(); } - return $this->data ?? []; + return $this->data ?: []; } /** diff --git a/src/prompts/src/Concerns/Fallback.php b/src/prompts/src/Concerns/Fallback.php index 8c0d68bca..3e2a2c2f3 100644 --- a/src/prompts/src/Concerns/Fallback.php +++ b/src/prompts/src/Concerns/Fallback.php @@ -17,7 +17,7 @@ trait Fallback /** * The fallback implementations. * - * @var array + * @var array */ protected static array $fallbacks = []; diff --git a/src/queue/src/Queue.php b/src/queue/src/Queue.php index 365e4c033..b41d69e6d 100644 --- a/src/queue/src/Queue.php +++ b/src/queue/src/Queue.php @@ -315,7 +315,7 @@ protected function shouldDispatchAfterCommit(object|string $job): bool return $job->afterCommit; } - return $this->dispatchAfterCommit ?? false; + return $this->dispatchAfterCommit; } /** diff --git a/src/queue/src/QueueManager.php b/src/queue/src/QueueManager.php index 780bc7358..34936b787 100644 --- a/src/queue/src/QueueManager.php +++ b/src/queue/src/QueueManager.php @@ -161,11 +161,10 @@ protected function resolve(string $name): Queue throw new InvalidArgumentException("The [{$name}] queue connection has not been configured."); } - /** @phpstan-ignore-next-line */ $resolver = fn () => $this->getConnector($config['driver']) ->connect($config) ->setConnectionName($name) - ->setContainer($this->app) + ->setContainer($this->app) // @phpstan-ignore method.notFound ->setConfig($config); if (in_array($config['driver'], $this->poolables)) { diff --git a/src/queue/src/QueueManagerFactory.php b/src/queue/src/QueueManagerFactory.php index ec954db5e..691849345 100644 --- a/src/queue/src/QueueManagerFactory.php +++ b/src/queue/src/QueueManagerFactory.php @@ -22,8 +22,8 @@ public function __invoke(ContainerInterface $container): QueueManager $reportHandler = fn (Throwable $e) => $container->get(ExceptionHandler::class)->report($e); foreach ($connectors as $connector) { try { - $manager->connection($connector) // @phpstan-ignore-line - ->setExceptionCallback($reportHandler); + $manager->connection($connector) + ->setExceptionCallback($reportHandler); // @phpstan-ignore method.notFound } catch (InvalidArgumentException) { // Ignore exception when the connector is not configured. } diff --git a/src/sentry/config/sentry.php b/src/sentry/config/sentry.php index b7756114e..a7f2bf7cd 100644 --- a/src/sentry/config/sentry.php +++ b/src/sentry/config/sentry.php @@ -95,8 +95,7 @@ ValidationException::class, ], - 'ignore_transactions' => [ - ], + 'ignore_transactions' => [], 'ignore_commands' => [ 'crontab:run', diff --git a/src/session/src/Middleware/StartSession.php b/src/session/src/Middleware/StartSession.php index af0d74ff5..81174b266 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -11,6 +11,7 @@ use Hyperf\HttpServer\Request; use Hyperf\HttpServer\Router\Dispatched; use Hypervel\Cache\Contracts\Factory as CacheFactoryContract; +use Hypervel\Cache\Contracts\LockProvider; use Hypervel\Cookie\Cookie; use Hypervel\Session\Contracts\Session; use Hypervel\Session\SessionManager; @@ -78,8 +79,9 @@ protected function handleRequestWhileBlocking(ServerRequestInterface $request, S $waitFor = ($blockingOptions['wait'] ?? $this->manager->defaultRouteBlockWaitSeconds()); - /* @phpstan-ignore-next-line */ - $lock = $this->cache->store($this->manager->blockDriver()) + /** @var \Hypervel\Cache\Contracts\Repository&LockProvider $store */ // @phpstan-ignore varTag.nativeType + $store = $this->cache->store($this->manager->blockDriver()); + $lock = $store ->lock('session:' . $session->getId(), (int) $lockFor) ->betweenBlockedAttemptsSleepFor(50); @@ -88,7 +90,7 @@ protected function handleRequestWhileBlocking(ServerRequestInterface $request, S return $this->handleStatefulRequest($request, $session, $handler); } finally { - $lock?->release(); + $lock->release(); } } diff --git a/src/support/src/FileinfoMimeTypeGuesser.php b/src/support/src/FileinfoMimeTypeGuesser.php index fa04a2b33..6835e52e9 100644 --- a/src/support/src/FileinfoMimeTypeGuesser.php +++ b/src/support/src/FileinfoMimeTypeGuesser.php @@ -15,6 +15,7 @@ use Exception; use finfo; +use Hypervel\Context\Context; use InvalidArgumentException; use LogicException; use RuntimeException; @@ -24,10 +25,7 @@ */ class FileinfoMimeTypeGuesser { - /** - * @var array - */ - private static $finfoCache = []; + private const FINFO_CACHE_KEY = '__support.finfo_mime_type_guesser.'; /** * @param null|string $magicFile A magic file to use with the finfo instance @@ -55,7 +53,10 @@ public function guessMimeType(string $path): ?string } try { - $finfo = self::$finfoCache[$this->magicFile] ??= new finfo(FILEINFO_MIME_TYPE, $this->magicFile); + $finfo = Context::getOrSet( + self::FINFO_CACHE_KEY . ($this->magicFile ?? ''), + fn () => new finfo(FILEINFO_MIME_TYPE, $this->magicFile) + ); } catch (Exception $e) { throw new RuntimeException($e->getMessage()); } diff --git a/src/support/src/Timebox.php b/src/support/src/Timebox.php new file mode 100644 index 000000000..16d89ea37 --- /dev/null +++ b/src/support/src/Timebox.php @@ -0,0 +1,65 @@ +earlyReturn && $remainder > 0) { + $this->usleep($remainder); + } + + if ($exception) { + throw $exception; + } + + return $result; + } + + public function returnEarly(): static + { + $this->earlyReturn = true; + + return $this; + } + + public function dontReturnEarly(): static + { + $this->earlyReturn = false; + + return $this; + } + + protected function usleep(int $microseconds) + { + Sleep::usleep($microseconds); + } +} diff --git a/src/telescope/config/telescope.php b/src/telescope/config/telescope.php index 18902050c..9a3530dce 100644 --- a/src/telescope/config/telescope.php +++ b/src/telescope/config/telescope.php @@ -127,11 +127,9 @@ // 'api/*' ], - 'ignore_paths' => [ - ], + 'ignore_paths' => [], - 'ignore_commands' => [ - ], + 'ignore_commands' => [], /* |-------------------------------------------------------------------------- diff --git a/src/telescope/src/Storage/DatabaseEntriesRepository.php b/src/telescope/src/Storage/DatabaseEntriesRepository.php index f83ac1261..93c395203 100644 --- a/src/telescope/src/Storage/DatabaseEntriesRepository.php +++ b/src/telescope/src/Storage/DatabaseEntriesRepository.php @@ -72,9 +72,8 @@ public function find(mixed $id): EntryResult */ public function get(?string $type, EntryQueryOptions $options): Collection { - /* @phpstan-ignore-next-line */ return EntryModel::on($this->connection) - ->withTelescopeOptions($type, $options) + ->withTelescopeOptions($type, $options) // @phpstan-ignore method.notFound ->take($options->limit) ->orderByDesc('sequence') ->get()->reject(function ($entry) { diff --git a/src/telescope/src/TelescopeApplicationServiceProvider.php b/src/telescope/src/TelescopeApplicationServiceProvider.php index 614af2d98..a65da14ea 100644 --- a/src/telescope/src/TelescopeApplicationServiceProvider.php +++ b/src/telescope/src/TelescopeApplicationServiceProvider.php @@ -39,8 +39,7 @@ protected function authorization(): void protected function gate(): void { Gate::define('viewTelescope', function ($user) { - return in_array($user->email, [ - ]); + return in_array($user->email, []); }); } diff --git a/tests/Support/FileinfoMimeTypeGuesserTest.php b/tests/Support/FileinfoMimeTypeGuesserTest.php new file mode 100644 index 000000000..5a59b72dd --- /dev/null +++ b/tests/Support/FileinfoMimeTypeGuesserTest.php @@ -0,0 +1,49 @@ +expectException(InvalidArgumentException::class); + + (new FileinfoMimeTypeGuesser()) + ->guessMimeType(__DIR__ . '/unknown'); + } + + public function testGuessMimeType(): void + { + $mimeType = (new FileinfoMimeTypeGuesser()) + ->guessMimeType(__DIR__ . '/fixtures/test.gif'); + + $this->assertEquals('image/gif', $mimeType); + } + + public function testGuessMimeTypeInCoroutines(): void + { + $guesser = (new FileinfoMimeTypeGuesser()); + for ($i = 0; $i < 5; ++$i) { + Coroutine::create(function () use ($guesser) { + $mimeType = $guesser->guessMimeType(__DIR__ . '/fixtures/test.gif'); + $this->assertEquals('image/gif', $mimeType); + }); + } + } +} diff --git a/tests/Support/TimeboxTest.php b/tests/Support/TimeboxTest.php new file mode 100644 index 000000000..991d6c448 --- /dev/null +++ b/tests/Support/TimeboxTest.php @@ -0,0 +1,92 @@ +assertTrue(true); + }; + + (new Timebox())->call($callback, 0); + } + + public function testMakeWaitsForMicroseconds(): void + { + $mock = Mockery::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial(); + $mock->shouldReceive('usleep')->once(); + + $mock->call(function () { + }, 10000); + + $mock->shouldHaveReceived('usleep')->once(); + } + + public function testMakeShouldNotSleepWhenEarlyReturnHasBeenFlagged(): void + { + $mock = Mockery::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial(); + $mock->call(function ($timebox) { + $timebox->returnEarly(); + }, 10000); + + $mock->shouldNotHaveReceived('usleep'); + } + + public function testMakeShouldSleepWhenDontEarlyReturnHasBeenFlagged(): void + { + $mock = Mockery::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial(); + $mock->shouldReceive('usleep')->once(); + + $mock->call(function ($timebox) { + $timebox->returnEarly(); + $timebox->dontReturnEarly(); + }, 10000); + + $mock->shouldHaveReceived('usleep')->once(); + } + + public function testMakeWaitsForMicrosecondsWhenExceptionIsThrown(): void + { + $mock = Mockery::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial(); + $mock->shouldReceive('usleep')->once(); + + try { + $this->expectExceptionMessage('Exception within Timebox callback.'); + + $mock->call(function () { + throw new Exception('Exception within Timebox callback.'); + }, 10000); + } finally { + $mock->shouldHaveReceived('usleep')->once(); + } + } + + public function testMakeShouldNotSleepWhenEarlyReturnHasBeenFlaggedAndExceptionIsThrown(): void + { + $mock = Mockery::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial(); + + try { + $this->expectExceptionMessage('Exception within Timebox callback.'); + + $mock->call(function ($timebox) { + $timebox->returnEarly(); + throw new Exception('Exception within Timebox callback.'); + }, 10000); + } finally { + $mock->shouldNotHaveReceived('usleep'); + } + } +} diff --git a/tests/Support/fixtures/test.gif b/tests/Support/fixtures/test.gif new file mode 100644 index 000000000..b506624ab Binary files /dev/null and b/tests/Support/fixtures/test.gif differ