diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 84219d82..871c602f 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -3,8 +3,9 @@ name: PHPStan on: workflow_dispatch: push: - branches-ignore: - - 'dependabot/npm_and_yarn/*' + branches: [main] + pull_request: + branches: [main] jobs: phpstan: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a866e6b..7c33f8d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to `nativephp-laravel` will be documented in this file. +## 0.7.0 - 2024-12-19 + +### What's Changed + +* Fix Settings facade DocBloc by @SRWieZ in https://github.com/NativePHP/laravel/pull/419 +* Fake test double for WindowManager::Class by @XbNz in https://github.com/NativePHP/laravel/pull/422 +* Child process test double by @XbNz in https://github.com/NativePHP/laravel/pull/430 +* fix: Notification facade docbloc by @SRWieZ in https://github.com/NativePHP/laravel/pull/428 +* Improvements to window test doubles by @XbNz in https://github.com/NativePHP/laravel/pull/426 +* fix: child process cmd: option except iterable array by @SRWieZ in https://github.com/NativePHP/laravel/pull/429 +* feat: improve Settings by @SRWieZ in https://github.com/NativePHP/laravel/pull/432 +* Dock goodies by @simonhamp in https://github.com/NativePHP/laravel/pull/421 +* MenuBars continued by @simonhamp in https://github.com/NativePHP/laravel/pull/420 +* Global shortcut test double by @XbNz in https://github.com/NativePHP/laravel/pull/436 +* Menu improvements by @simonhamp in https://github.com/NativePHP/laravel/pull/423 +* fix: database migration on first launch by @SRWieZ in https://github.com/NativePHP/laravel/pull/439 +* Fixes and improvements to powerMonitor by @SRWieZ in https://github.com/NativePHP/laravel/pull/445 +* feat: phpstan level 5 by @SRWieZ in https://github.com/NativePHP/laravel/pull/446 + +### New Contributors + +* @SRWieZ made their first contribution in https://github.com/NativePHP/laravel/pull/419 +* @XbNz made their first contribution in https://github.com/NativePHP/laravel/pull/422 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.4...0.7.0 + ## 0.6.4 - 2024-11-17 ### What's Changed diff --git a/README.md b/README.md index 4de29bb3..45e26d39 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ Thanks to the following sponsors for funding NativePHP development. Please consi - [Laradevs](https://laradevs.com/?ref=nativephp-docs) - Connecting the best Laravel Developers with the best Laravel Teams. - [RedGalaxy](https://www.redgalaxy.co.uk) - A web application development studio based in Cambridgeshire, building solutions to help businesses improve efficiency and profitability. - [Sevalla](https://sevalla.com/?utm_source=nativephp&utm_medium=Referral&utm_campaign=homepage) - Host and manage your applications, databases, and static sites in a single, intuitive platform. -- [ServerAuth](https://serverauth.com) - Website Deployment & Server Management, made simple! - [KaasHosting](https://www.kaashosting.nl/?lang=en) - Minecraft Server and VPS hosting from The Netherlands. +- [Borah Digital Labs](https://borah.digital/) - An MVP building studio from the sunny Canary Islands focusing on AI, SaaS and online platforms. ## Changelog diff --git a/config/nativephp.php b/config/nativephp.php index b24afec0..5c6e7a8f 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -114,4 +114,12 @@ ], ], ], + + 'queue_workers' => [ + 'default' => [ + 'queues' => ['default'], + 'memory_limit' => 128, + 'timeout' => 60, + ], + ], ]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8b18acdc..266bd0e1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,36 +1,35 @@ - - - - tests - - - - - ./src - - - - - - - - - - + + + + tests + + + + + + + + + + + + + + + ./src + + diff --git a/src/Contracts/QueueWorker.php b/src/Contracts/QueueWorker.php new file mode 100644 index 00000000..a2c4cf9c --- /dev/null +++ b/src/Contracts/QueueWorker.php @@ -0,0 +1,12 @@ + $queuesToConsume + */ + public function __construct( + public readonly string $alias, + public readonly array $queuesToConsume, + public readonly int $memoryLimit, + public readonly int $timeout, + ) {} + + /** + * @return array + */ + public static function fromConfigArray(array $config): array + { + return array_map( + function (array|string $worker, string $alias) { + return new self( + $alias, + $worker['queues'] ?? ['default'], + $worker['memory_limit'] ?? 128, + $worker['timeout'] ?? 60, + ); + }, + $config, + array_keys($config), + ); + } +} diff --git a/src/Events/ChildProcess/ErrorReceived.php b/src/Events/ChildProcess/ErrorReceived.php index 65db9c66..334e9ccd 100644 --- a/src/Events/ChildProcess/ErrorReceived.php +++ b/src/Events/ChildProcess/ErrorReceived.php @@ -3,11 +3,11 @@ namespace Native\Laravel\Events\ChildProcess; use Illuminate\Broadcasting\Channel; -use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class ErrorReceived implements ShouldBroadcast +class ErrorReceived implements ShouldBroadcastNow { use Dispatchable, SerializesModels; diff --git a/src/Events/ChildProcess/MessageReceived.php b/src/Events/ChildProcess/MessageReceived.php index 5f7a432c..04a51c2f 100644 --- a/src/Events/ChildProcess/MessageReceived.php +++ b/src/Events/ChildProcess/MessageReceived.php @@ -3,11 +3,11 @@ namespace Native\Laravel\Events\ChildProcess; use Illuminate\Broadcasting\Channel; -use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class MessageReceived implements ShouldBroadcast +class MessageReceived implements ShouldBroadcastNow { use Dispatchable, SerializesModels; diff --git a/src/Events/ChildProcess/ProcessExited.php b/src/Events/ChildProcess/ProcessExited.php index bf570d84..0dcd5891 100644 --- a/src/Events/ChildProcess/ProcessExited.php +++ b/src/Events/ChildProcess/ProcessExited.php @@ -3,11 +3,11 @@ namespace Native\Laravel\Events\ChildProcess; use Illuminate\Broadcasting\Channel; -use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class ProcessExited implements ShouldBroadcast +class ProcessExited implements ShouldBroadcastNow { use Dispatchable, SerializesModels; diff --git a/src/Events/ChildProcess/ProcessSpawned.php b/src/Events/ChildProcess/ProcessSpawned.php index 91fc9170..a49b31bd 100644 --- a/src/Events/ChildProcess/ProcessSpawned.php +++ b/src/Events/ChildProcess/ProcessSpawned.php @@ -3,11 +3,11 @@ namespace Native\Laravel\Events\ChildProcess; use Illuminate\Broadcasting\Channel; -use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class ProcessSpawned implements ShouldBroadcast +class ProcessSpawned implements ShouldBroadcastNow { use Dispatchable, SerializesModels; diff --git a/src/Events/MenuBar/MenuBarCreated.php b/src/Events/MenuBar/MenuBarCreated.php new file mode 100644 index 00000000..07891a18 --- /dev/null +++ b/src/Events/MenuBar/MenuBarCreated.php @@ -0,0 +1,21 @@ +make(QueueWorkerFake::class), function ($fake) { + static::swap($fake); + }); + } + + protected static function getFacadeAccessor(): string + { + self::clearResolvedInstance(QueueWorkerContract::class); + + return QueueWorkerContract::class; + } +} diff --git a/src/Fakes/PowerMonitorFake.php b/src/Fakes/PowerMonitorFake.php index 2af99167..caaf2823 100644 --- a/src/Fakes/PowerMonitorFake.php +++ b/src/Fakes/PowerMonitorFake.php @@ -64,7 +64,7 @@ public function assertGetSystemIdleState(int|Closure $key): void $hit = empty( array_filter( $this->getSystemIdleStateCalls, - fn (string $keyIteration) => $key($keyIteration) === true + fn (int $keyIteration) => $key($keyIteration) === true ) ) === false; diff --git a/src/Fakes/QueueWorkerFake.php b/src/Fakes/QueueWorkerFake.php new file mode 100644 index 00000000..6482dd9f --- /dev/null +++ b/src/Fakes/QueueWorkerFake.php @@ -0,0 +1,61 @@ + + */ + public array $ups = []; + + /** + * @var array + */ + public array $downs = []; + + public function up(QueueConfig $config): void + { + $this->ups[] = $config; + } + + public function down(string $alias): void + { + $this->downs[] = $alias; + } + + public function assertUp(Closure $callback): void + { + $hit = empty( + array_filter( + $this->ups, + fn (QueueConfig $up) => $callback($up) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertDown(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->downs); + + return; + } + + $hit = empty( + array_filter( + $this->downs, + fn (string $down) => $alias($down) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } +} diff --git a/src/Fakes/WindowManagerFake.php b/src/Fakes/WindowManagerFake.php index 8604224f..64112b34 100644 --- a/src/Fakes/WindowManagerFake.php +++ b/src/Fakes/WindowManagerFake.php @@ -18,6 +18,8 @@ class WindowManagerFake implements WindowManagerContract public array $hidden = []; + public array $shown = []; + public array $forcedWindowReturnValues = []; public function __construct( @@ -64,6 +66,11 @@ public function hide($id = null) $this->hidden[] = $id; } + public function show($id = null) + { + $this->shown[] = $id; + } + public function current(): Window { $this->ensureForceReturnWindowsProvided(); @@ -156,6 +163,27 @@ public function assertHidden(string|Closure $id): void PHPUnit::assertTrue($hit); } + /** + * @param string|Closure(string): bool $id + */ + public function assertShown(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->shown); + + return; + } + + $hit = empty( + array_filter( + $this->shown, + fn (mixed $shownId) => $id($shownId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + public function assertOpenedCount(int $expected): void { PHPUnit::assertCount($expected, $this->opened); @@ -171,6 +199,11 @@ public function assertHiddenCount(int $expected): void PHPUnit::assertCount($expected, $this->hidden); } + public function assertShownCount(int $expected): void + { + PHPUnit::assertCount($expected, $this->shown); + } + private function ensureForceReturnWindowsProvided(): void { Assert::notEmpty($this->forcedWindowReturnValues, 'No windows were provided to return'); diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 22e39913..6ca8d626 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -17,7 +17,9 @@ use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; use Native\Laravel\Contracts\PowerMonitor as PowerMonitorContract; +use Native\Laravel\Contracts\QueueWorker as QueueWorkerContract; use Native\Laravel\Contracts\WindowManager as WindowManagerContract; +use Native\Laravel\DTOs\QueueConfig; use Native\Laravel\Events\EventWatcher; use Native\Laravel\Exceptions\Handler; use Native\Laravel\GlobalShortcut as GlobalShortcutImplementation; @@ -73,6 +75,10 @@ public function packageRegistered() return $app->make(PowerMonitorImplementation::class); }); + $this->app->bind(QueueWorkerContract::class, function (Foundation $app) { + return $app->make(QueueWorker::class); + }); + if (config('nativephp-internal.running')) { $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, @@ -112,6 +118,11 @@ protected function configureApp() config(['session.driver' => 'file']); config(['queue.default' => 'database']); + + // XXX: This logic may need to change when we ditch the internal web server + if (! $this->app->runningInConsole()) { + $this->fireUpQueueWorkers(); + } } protected function rewriteStoragePath() @@ -210,4 +221,13 @@ protected function configureDisks(): void ]); } } + + protected function fireUpQueueWorkers(): void + { + $queueConfigs = QueueConfig::fromConfigArray(config('nativephp.queue_workers')); + + foreach ($queueConfigs as $queueConfig) { + $this->app->make(QueueWorkerContract::class)->up($queueConfig); + } + } } diff --git a/src/Notification.php b/src/Notification.php index 82d13d82..8bde4d59 100644 --- a/src/Notification.php +++ b/src/Notification.php @@ -12,7 +12,10 @@ class Notification protected string $event = ''; - final public function __construct(protected Client $client) {} + final public function __construct(protected Client $client) + { + $this->title = config('app.name'); + } public static function new() { diff --git a/src/QueueWorker.php b/src/QueueWorker.php new file mode 100644 index 00000000..1eb0c000 --- /dev/null +++ b/src/QueueWorker.php @@ -0,0 +1,47 @@ +has("nativephp.queue_workers.{$config}")) { + $config = QueueConfig::fromConfigArray([ + $config => config("nativephp.queue_workers.{$config}"), + ])[0]; + } + + if (! $config instanceof QueueConfig) { + throw new \InvalidArgumentException("Invalid queue configuration alias [$config]"); + } + + $this->childProcess->php( + [ + '-d', + "memory_limit={$config->memoryLimit}M", + 'artisan', + 'queue:work', + "--name={$config->alias}", + '--queue='.implode(',', $config->queuesToConsume), + "--memory={$config->memoryLimit}", + "--timeout={$config->timeout}", + ], + $config->alias, + persistent: true, + ); + } + + public function down(string $alias): void + { + $this->childProcess->stop($alias); + } +} diff --git a/src/Screen.php b/src/Screen.php index c15d4e1e..9f34c101 100644 --- a/src/Screen.php +++ b/src/Screen.php @@ -18,12 +18,12 @@ public function displays(): array return $this->client->get('screen/displays')->json('displays'); } - public function primary(): object + public function primary(): array { return $this->client->get('screen/primary-display')->json('primaryDisplay'); } - public function active(): object + public function active(): array { return $this->client->get('screen/active')->json(); } diff --git a/src/Windows/WindowManager.php b/src/Windows/WindowManager.php index 64046869..bdd7f90d 100644 --- a/src/Windows/WindowManager.php +++ b/src/Windows/WindowManager.php @@ -31,6 +31,13 @@ public function hide($id = null) ]); } + public function show($id = null) + { + $this->client->post('window/show', [ + 'id' => $id ?? $this->detectId(), + ]); + } + public function current(): Window { $window = (object) $this->client->get('window/current')->json(); diff --git a/tests/ChildProcess/ChildProcessTest.php b/tests/ChildProcess/ChildProcessTest.php index 7bd13b79..c78db8ce 100644 --- a/tests/ChildProcess/ChildProcessTest.php +++ b/tests/ChildProcess/ChildProcessTest.php @@ -2,7 +2,6 @@ use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Http; -use Mockery; use Native\Laravel\ChildProcess as ChildProcessImplement; use Native\Laravel\Client\Client; use Native\Laravel\Facades\ChildProcess; diff --git a/tests/DTOs/QueueWorkerTest.php b/tests/DTOs/QueueWorkerTest.php new file mode 100644 index 00000000..bc1b764f --- /dev/null +++ b/tests/DTOs/QueueWorkerTest.php @@ -0,0 +1,66 @@ +toBeArray(); + expect($configObject)->toHaveCount(count($config)); + + foreach ($config as $alias => $worker) { + if (is_string($worker)) { + expect( + Arr::first( + array_filter($configObject, fn (QueueConfig $config) => $config->alias === $worker)) + )->queuesToConsume->toBe(['default'] + ); + + expect(Arr::first(array_filter($configObject, fn (QueueConfig $config) => $config->alias === $worker)))->memoryLimit->toBe(128); + expect(Arr::first(array_filter($configObject, fn (QueueConfig $config) => $config->alias === $worker)))->timeout->toBe(60); + + continue; + } + + expect( + Arr::first( + array_filter($configObject, fn (QueueConfig $config) => $config->alias === $alias)) + )->queuesToConsume->toBe($worker['queues'] ?? ['default'] + ); + + expect(Arr::first(array_filter($configObject, fn (QueueConfig $config) => $config->alias === $alias)))->memoryLimit->toBe($worker['memory_limit'] ?? 128); + expect(Arr::first(array_filter($configObject, fn (QueueConfig $config) => $config->alias === $alias)))->timeout->toBe($worker['timeout'] ?? 60); + } +})->with([ + [ + 'queue_workers' => [ + 'some_worker' => [ + 'queues' => ['default'], + 'memory_limit' => 64, + 'timeout' => 60, + ], + ], + ], + [ + 'queue_workers' => [ + 'some_worker' => [], + 'another_worker' => [], + ], + ], + [ + 'queue_workers' => [ + 'some_worker' => [ + ], + 'another_worker' => [ + 'queues' => ['default', 'another'], + ], + 'yet_another_worker' => [ + 'memory_limit' => 256, + ], + 'one_more_worker' => [ + 'timeout' => 120, + ], + ], + ], +]); diff --git a/tests/Fakes/FakeQueueWorkerTest.php b/tests/Fakes/FakeQueueWorkerTest.php new file mode 100644 index 00000000..4b22f34d --- /dev/null +++ b/tests/Fakes/FakeQueueWorkerTest.php @@ -0,0 +1,69 @@ +toBeInstanceOf(QueueWorkerFake::class); +}); + +it('asserts up using callable', function () { + swap(QueueWorkerContract::class, $fake = app(QueueWorkerFake::class)); + + $fake->up(new QueueConfig('testA', ['default'], 123, 123)); + $fake->up(new QueueConfig('testB', ['default'], 123, 123)); + + $fake->assertUp(fn (QueueConfig $up) => $up->alias === 'testA'); + $fake->assertUp(fn (QueueConfig $up) => $up->alias === 'testB'); + + try { + $fake->assertUp(fn (QueueConfig $up) => $up->alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts down using string', function () { + swap(QueueWorkerContract::class, $fake = app(QueueWorkerFake::class)); + + $fake->down('testA'); + $fake->down('testB'); + + $fake->assertDown('testA'); + $fake->assertDown('testB'); + + try { + $fake->assertDown('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts down using callable', function () { + swap(QueueWorkerContract::class, $fake = app(QueueWorkerFake::class)); + + $fake->down('testA'); + $fake->down('testB'); + + $fake->assertDown(fn (string $alias) => $alias === 'testA'); + $fake->assertDown(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertDown(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); diff --git a/tests/Fakes/FakeWindowManagerTest.php b/tests/Fakes/FakeWindowManagerTest.php index b2f4af25..f6c4804c 100644 --- a/tests/Fakes/FakeWindowManagerTest.php +++ b/tests/Fakes/FakeWindowManagerTest.php @@ -136,6 +136,24 @@ $this->fail('Expected assertion to fail'); }); +it('asserts that a window was shown', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->show('main'); + app(WindowManagerContract::class)->show('secondary'); + + $fake->assertShown('main'); + $fake->assertShown('secondary'); + + try { + $fake->assertShown('tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + it('asserts opened count', function () { Http::fake(['*' => Http::response(status: 200)]); @@ -196,6 +214,24 @@ $this->fail('Expected assertion to fail'); }); +it('asserts shown count', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->show('main'); + app(WindowManagerContract::class)->show(); + app(WindowManagerContract::class)->show(); + + $fake->assertShownCount(3); + + try { + $fake->assertShownCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + it('forces the return value of current window', function () { swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); diff --git a/tests/QueueWorker/QueueWorkerTest.php b/tests/QueueWorker/QueueWorkerTest.php new file mode 100644 index 00000000..a3fbd576 --- /dev/null +++ b/tests/QueueWorker/QueueWorkerTest.php @@ -0,0 +1,39 @@ +toBe([ + '-d', + 'memory_limit=128M', + 'artisan', + 'queue:work', + "--name={$alias}", + '--queue=default', + '--memory=128', + '--timeout=61', + ]); + + expect($alias)->toBe('some_worker'); + expect($env)->toBeNull(); + expect($persistent)->toBeTrue(); + + return true; + }); +}); + +it('hits the child process with relevant alias spin down a queue worker', function () { + ChildProcess::fake(); + + QueueWorker::down('some_worker'); + + ChildProcess::assertStop('some_worker'); +});