diff --git a/src/AppBuilder.php b/src/AppBuilder.php index e7b6817a..c67310ed 100644 --- a/src/AppBuilder.php +++ b/src/AppBuilder.php @@ -13,7 +13,7 @@ public static function build(string|null $path = null, string|null $env = null): { $app = new App($path ?? dirname(__DIR__)); - Environment::load($env); + Environment::load('.env', $env); putenv('PHENIX_BASE_PATH=' . base_path()); $_ENV['PHENIX_BASE_PATH'] = base_path(); diff --git a/src/Console/Commands/ViewCache.php b/src/Console/Commands/ViewCache.php index 1b258de2..e912784e 100644 --- a/src/Console/Commands/ViewCache.php +++ b/src/Console/Commands/ViewCache.php @@ -45,7 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->compile(Config::get('view.path')); - WorkerPool::batch($this->tasks); + WorkerPool::awaitAll($this->tasks); $output->writeln('All views were compiled successfully!.'); diff --git a/src/Crypto/Crypto.php b/src/Crypto/Crypto.php index e198e916..2a550c80 100644 --- a/src/Crypto/Crypto.php +++ b/src/Crypto/Crypto.php @@ -11,7 +11,6 @@ use Phenix\Crypto\Tasks\Decrypt; use Phenix\Crypto\Tasks\Encrypt; use Phenix\Tasks\Result; -use Phenix\Tasks\Worker; use SensitiveParameter; class Crypto implements CipherContract, StringCipher @@ -26,14 +25,14 @@ public function __construct( public function encrypt(#[SensitiveParameter] object|array|string $value, bool $serialize = true): string { + $task = new Encrypt( + key: $this->key, + value: $value, + serialize: $serialize + ); + /** @var Result $result */ - [$result] = Worker::batch([ - new Encrypt( - key: $this->key, - value: $value, - serialize: $serialize - ), - ]); + $result = $task->output(); if ($result->isFailure()) { throw new EncryptException($result->message()); @@ -49,14 +48,14 @@ public function encryptString(#[SensitiveParameter] string $value): string public function decrypt(string $payload, bool $unserialize = true): object|array|string { + $task = new Decrypt( + key: $this->key, + value: $payload, + unserialize: $unserialize + ); + /** @var Result $result */ - [$result] = Worker::batch([ - new Decrypt( - key: $this->key, - value: $payload, - unserialize: $unserialize - ), - ]); + $result = $task->output(); if ($result->isFailure()) { throw new DecryptException($result->message()); diff --git a/src/Crypto/Hash.php b/src/Crypto/Hash.php index d7c17aaf..d8c03cdf 100644 --- a/src/Crypto/Hash.php +++ b/src/Crypto/Hash.php @@ -9,37 +9,36 @@ use Phenix\Crypto\Tasks\GeneratePasswordHash; use Phenix\Crypto\Tasks\VerifyPasswordHash; use Phenix\Tasks\Result; -use Phenix\Tasks\Worker; use SensitiveParameter; class Hash implements HasherContract { public function make(#[SensitiveParameter] string $password): string { + $task = new GeneratePasswordHash($password); + /** @var Result $result */ - [$result] = Worker::batch([ - new GeneratePasswordHash($password), - ]); + $result = $task->output(); return $result->output(); } public function verify(string $hash, #[SensitiveParameter] string $password): bool { + $task = new VerifyPasswordHash($hash, $password); + /** @var Result $result */ - [$result] = Worker::batch([ - new VerifyPasswordHash($hash, $password), - ]); + $result = $task->output(); return $result->output(); } public function needsRehash(string $hash): bool { + $task = new CheckNeedsRehash($hash); + /** @var Result $result */ - [$result] = Worker::batch([ - new CheckNeedsRehash($hash), - ]); + $result = $task->output(); return $result->output(); } diff --git a/src/Facades/Mail.php b/src/Facades/Mail.php index 7387ee98..d486ee61 100644 --- a/src/Facades/Mail.php +++ b/src/Facades/Mail.php @@ -4,6 +4,7 @@ namespace Phenix\Facades; +use Amp\Future; use Phenix\Mail\Constants\MailerType; use Phenix\Mail\Contracts\Mailable as MailableContract; use Phenix\Mail\Contracts\Mailer; @@ -15,7 +16,7 @@ * @method static Mailer mailer(MailerType|null $mailerType = null) * @method static Mailer using(MailerType $mailerType) * @method static Mailer to(array|string $to) - * @method static void send(MailableContract $mailable) + * @method static Future send(MailableContract $mailable) * @method static Mailer fake(MailerType|null $mailerType = null) * @method static array getSendingLog(MailerType|null $mailerType = null) * @method static void resetSendingLog(MailerType|null $mailerType = null) diff --git a/src/Facades/Worker.php b/src/Facades/Worker.php new file mode 100644 index 00000000..84b61d04 --- /dev/null +++ b/src/Facades/Worker.php @@ -0,0 +1,35 @@ +mailer()->to($to); } - public function send(Mailable $mailable): void + public function send(Mailable $mailable): Future { - $this->mailer()->send($mailable); + return $this->mailer()->send($mailable); } public function fake(MailerType|null $mailerType = null): void diff --git a/src/Mail/Mailer.php b/src/Mail/Mailer.php index 0a0ffcf2..0bd1bfd4 100644 --- a/src/Mail/Mailer.php +++ b/src/Mail/Mailer.php @@ -4,11 +4,11 @@ namespace Phenix\Mail; +use Amp\Future; use Phenix\Mail\Contracts\Mailable; use Phenix\Mail\Contracts\Mailer as MailerContract; use Phenix\Mail\Tasks\SendEmail; -use Phenix\Tasks\Result; -use Phenix\Tasks\Worker; +use Phenix\Tasks\WorkerPool; use SensitiveParameter; use Symfony\Component\Mime\Address; @@ -57,7 +57,7 @@ public function bcc(array|string $bcc): self return $this; } - public function send(Mailable $mailable): void + public function send(Mailable $mailable): Future { $mailable->from($this->from) ->to($this->to) @@ -67,22 +67,22 @@ public function send(Mailable $mailable): void $email = $mailable->toMail(); - /** @var Result $result */ - [$result] = Worker::batch([ + $future = WorkerPool::submit( new SendEmail( $email, $this->config, $this->serviceConfig, - ), - ]); + ) + ); if ($this->config['transport'] === 'log') { $this->sendingLog[] = [ 'mailable' => $mailable::class, 'email' => $email, - 'success' => $result->isSuccess(), ]; } + + return $future; } public function getSendingLog(): array diff --git a/src/Mail/TransportFactory.php b/src/Mail/TransportFactory.php index 81eefb42..2fedfa6d 100644 --- a/src/Mail/TransportFactory.php +++ b/src/Mail/TransportFactory.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use Phenix\Mail\Constants\MailerType; use Phenix\Mail\Transports\LogTransport; +use Phenix\Util\Arr; use SensitiveParameter; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport; use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendApiTransport; @@ -34,7 +35,7 @@ private static function createSmtpTransport(#[SensitiveParameter] array $config) $scheme = 'smtp'; if (! empty($config['encryption']) && $config['encryption'] === 'tls') { - $scheme = ($config['port'] === 465) ? 'smtps' : 'smtp'; + $scheme = (Arr::get($config, 'port') === 465) ? 'smtps' : 'smtp'; } $dsn = new Dsn( @@ -42,7 +43,7 @@ private static function createSmtpTransport(#[SensitiveParameter] array $config) $config['host'], $config['username'] ?? null, $config['password'] ?? null, - $config['port'] ?? null, + Arr::has($config, 'port') ? (int) Arr::get($config, 'port') : null, $config ); diff --git a/src/Runtime/Environment.php b/src/Runtime/Environment.php index 798a207b..c51b8978 100644 --- a/src/Runtime/Environment.php +++ b/src/Runtime/Environment.php @@ -5,6 +5,7 @@ namespace Phenix\Runtime; use Dotenv\Dotenv; +use Phenix\Util\Str; class Environment { @@ -12,7 +13,7 @@ public static function load(string|null $fileName = null, string|null $environme { $fileName ??= '.env'; $fileName .= $environment ? ".{$environment}" : ''; - $fileNamePath = base_path() . DIRECTORY_SEPARATOR . $fileName; + $fileNamePath = Str::finish(base_path(), DIRECTORY_SEPARATOR) . $fileName; if (file_exists($fileNamePath)) { Dotenv::createImmutable(base_path(), $fileName)->load(); diff --git a/src/Tasks/AbstractWorker.php b/src/Tasks/AbstractWorker.php index 2886026b..3aeb0c1f 100644 --- a/src/Tasks/AbstractWorker.php +++ b/src/Tasks/AbstractWorker.php @@ -26,12 +26,12 @@ public function __construct() * @param array $tasks * @return array */ - public static function batch(array $tasks): array + public static function awaitAll(array $tasks): array { $pool = new static(); foreach ($tasks as $task) { - $pool->submit($task); + $pool->push($task); } $results = $pool->run(); @@ -41,9 +41,9 @@ public static function batch(array $tasks): array return $results; } - public function submit(Task $parallelTask): self + public function push(Task $parallelTask): self { - $this->tasks[] = $this->submitTask($parallelTask); + $this->tasks[] = $this->prepareTask($parallelTask); return $this; } @@ -56,7 +56,7 @@ public function run(): array )); } - abstract protected function submitTask(Task $parallelTask): Worker\Execution; + abstract protected function prepareTask(Task $parallelTask): Worker\Execution; protected function finalize(): void { diff --git a/src/Tasks/Contracts/Worker.php b/src/Tasks/Contracts/Worker.php index ed332ab2..30818ee6 100644 --- a/src/Tasks/Contracts/Worker.php +++ b/src/Tasks/Contracts/Worker.php @@ -6,7 +6,7 @@ interface Worker { - public function submit(Task $parallelTask): self; + public function push(Task $parallelTask): self; public function run(): array; @@ -14,5 +14,5 @@ public function run(): array; * @param Task[] $tasks * @return array */ - public static function batch(array $tasks): array; + public static function awaitAll(array $tasks): array; } diff --git a/src/Tasks/Task.php b/src/Tasks/Task.php index 2c0d1094..727607bf 100644 --- a/src/Tasks/Task.php +++ b/src/Tasks/Task.php @@ -53,12 +53,7 @@ public function run(Channel $channel, Cancellation $cancellation): mixed public function output(): Result { - /** @var Result $result */ - [$result] = Worker::batch([ - $this, - ]); - - return $result; + return WorkerPool::submit($this)->await(); } public function setTimeout(int $timeout): void diff --git a/src/Tasks/Worker.php b/src/Tasks/Worker.php index 9b2f32c7..1af6b8e9 100644 --- a/src/Tasks/Worker.php +++ b/src/Tasks/Worker.php @@ -19,7 +19,7 @@ public function __construct() $this->worker = Workers\createWorker(); } - protected function submitTask(Task $parallelTask): Workers\Execution + protected function prepareTask(Task $parallelTask): Workers\Execution { $timeout = new TimeoutCancellation($parallelTask->getTimeout()); diff --git a/src/Tasks/WorkerPool.php b/src/Tasks/WorkerPool.php index 3bbf6ff1..7eb6365c 100644 --- a/src/Tasks/WorkerPool.php +++ b/src/Tasks/WorkerPool.php @@ -4,21 +4,27 @@ namespace Phenix\Tasks; -use Amp\Parallel\Worker; -use Amp\Parallel\Worker\WorkerPool as Pool; +use Amp\Future; +use Amp\Parallel\Worker\Execution; use Amp\TimeoutCancellation; -use Phenix\App; +use Phenix\Facades\Worker; use Phenix\Tasks\Contracts\Task; class WorkerPool extends AbstractWorker { - protected function submitTask(Task $parallelTask): Worker\Execution + protected function prepareTask(Task $parallelTask): Execution { - /** @var Pool $pool */ - $pool = App::make(Pool::class); + $timeout = new TimeoutCancellation($parallelTask->getTimeout()); + + return Worker::submit($parallelTask, $timeout); + } + public static function submit(Task $parallelTask): Future + { $timeout = new TimeoutCancellation($parallelTask->getTimeout()); - return $pool->submit($parallelTask, $timeout); + $execution = Worker::submit($parallelTask, $timeout); + + return $execution->getFuture(); } } diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index b8f2a99f..547f67d0 100644 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -12,6 +12,7 @@ use Phenix\Facades\Event; use Phenix\Facades\Mail; use Phenix\Facades\Queue; +use Phenix\Facades\View; use Phenix\Testing\Concerns\InteractWithResponses; use Phenix\Testing\Concerns\RefreshDatabase; use Symfony\Component\Console\Tester\CommandTester; @@ -41,6 +42,8 @@ protected function setUp(): void if (in_array(RefreshDatabase::class, $uses, true) && method_exists($this, 'refreshDatabase')) { $this->refreshDatabase(); } + + View::clearCache(); } protected function tearDown(): void diff --git a/src/Testing/TestMail.php b/src/Testing/TestMail.php index d8cf98f5..e8f207d0 100644 --- a/src/Testing/TestMail.php +++ b/src/Testing/TestMail.php @@ -7,6 +7,7 @@ use Closure; use Phenix\Data\Collection; use Phenix\Mail\Contracts\Mailable; +use Phenix\Util\Arr; use PHPUnit\Framework\Assert; class TestMail @@ -32,7 +33,7 @@ public function toBeSent(Closure|null $closure = null): void $matches = $this->filterByMailable($this->mailable); if ($closure) { - Assert::assertTrue($closure($matches->first())); + Assert::assertTrue($closure($matches)); } else { Assert::assertNotEmpty($matches, "Failed asserting that mailable '{$this->mailable}' was sent at least once."); } @@ -43,10 +44,8 @@ public function toNotBeSent(Closure|null $closure = null): void $matches = $this->filterByMailable($this->mailable); if ($closure) { - Assert::assertFalse($closure($matches->first())); + Assert::assertTrue($closure($matches)); } else { - $matches = $matches->filter(fn (array $item): bool => $item['success'] === false); - Assert::assertEmpty($matches, "Failed asserting that mailable '{$this->mailable}' was NOT sent."); } } @@ -65,7 +64,7 @@ private function filterByMailable(string $mailable): Collection $filtered = []; foreach ($this->log as $record) { - if (($record['mailable'] ?? null) === $mailable) { + if (Arr::get($record, 'mailable') === $mailable) { $filtered[] = $record; } } diff --git a/tests/Unit/Mail/MailTest.php b/tests/Unit/Mail/MailTest.php index c1e65b4e..ff820ccd 100644 --- a/tests/Unit/Mail/MailTest.php +++ b/tests/Unit/Mail/MailTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Phenix\Data\Collection; use Phenix\Facades\Config; use Phenix\Facades\Mail; use Phenix\Mail\Constants\MailerType; @@ -13,6 +14,7 @@ use Phenix\Mail\TransportFactory; use Phenix\Mail\Transports\LogTransport; use Phenix\Tasks\Result; +use Phenix\Util\Arr; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport; use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendApiTransport; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; @@ -140,19 +142,27 @@ public function build(): self } }; - Mail::to($email)->send($mailable); + $missingMailable = new class () extends Mailable { + public function build(): self + { + return $this->view('emails.welcome') + ->subject('It will not be sent'); + } + }; - Mail::expect($mailable)->toBeSent(); + $future = Mail::to($email)->send($mailable); - Mail::expect($mailable)->toBeSent(function (array $matches): bool { - return $matches['success'] === true; - }); + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeTrue(); + Mail::expect($mailable)->toBeSent(); + Mail::expect($mailable)->toBeSent(fn (Collection $matches): bool => Arr::get($matches->first(), 'mailable') === $mailable::class); Mail::expect($mailable)->toBeSentTimes(1); - Mail::expect($mailable)->toNotBeSent(); - Mail::expect($mailable)->toNotBeSent(function (array $matches): bool { - return $matches['success'] === false; - }); + + Mail::expect($missingMailable)->toNotBeSent(); + Mail::expect($missingMailable)->toNotBeSent(fn (Collection $matches): bool => $matches->isEmpty()); Mail::resetSendingLog(); @@ -181,18 +191,16 @@ public function build(): self } }; - Mail::to($email)->send($mailable); + $future = Mail::to($email)->send($mailable); - Mail::expect($mailable)->toBeSent(); + /** @var Result $result */ + $result = $future->await(); - Mail::expect($mailable)->toBeSent(function (array $matches): bool { - return $matches['success'] === true; - }); + expect($result->isSuccess())->toBeTrue(); + Mail::expect($mailable)->toBeSent(); + Mail::expect($mailable)->toBeSent(fn (Collection $matches): bool => Arr::get($matches->first(), 'mailable') === $mailable::class); Mail::expect($mailable)->toBeSentTimes(1); - Mail::expect($mailable)->toNotBeSent(function (array $matches): bool { - return $matches['success'] === false; - }); }); it('send email successfully using smtp mailer with sender defined in mailable', function (): void { @@ -216,7 +224,12 @@ public function build(): self } }; - Mail::send($mailable); + $future = Mail::send($mailable); + + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeTrue(); Mail::expect($mailable)->toBeSent(); }); @@ -244,10 +257,16 @@ public function build(): self } }; - Mail::to($email)->send($mailable); + $future = Mail::to($email)->send($mailable); - Mail::expect($mailable)->toBeSent(function (array $matches): bool { - $email = $matches['email'] ?? null; + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeTrue(); + + Mail::expect($mailable)->toBeSent(function (Collection $matches): bool { + $firstMatch = $matches->first(); + $email = $firstMatch['email'] ?? null; if (! $email) { return false; @@ -283,12 +302,18 @@ public function build(): self } }; - Mail::to($to) + $future = Mail::to($to) ->cc($cc) ->send($mailable); - Mail::expect($mailable)->toBeSent(function (array $matches) use ($cc): bool { - $email = $matches['email'] ?? null; + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeTrue(); + + Mail::expect($mailable)->toBeSent(function (Collection $matches) use ($cc): bool { + $firstMatch = $matches->first(); + $email = $firstMatch['email'] ?? null; if (! $email) { return false; @@ -325,12 +350,18 @@ public function build(): self } }; - Mail::to($to) + $future = Mail::to($to) ->bcc($bcc) ->send($mailable); - Mail::expect($mailable)->toBeSent(function (array $matches) use ($bcc): bool { - $email = $matches['email'] ?? null; + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeTrue(); + + Mail::expect($mailable)->toBeSent(function (Collection $matches) use ($bcc): bool { + $firstMatch = $matches->first(); + $email = $firstMatch['email'] ?? null; if (! $email) { return false; @@ -367,11 +398,17 @@ public function build(): self } }; - Mail::to($to) + $future = Mail::to($to) ->send($mailable); - Mail::expect($mailable)->toBeSent(function (array $matches): bool { - $email = $matches['email'] ?? null; + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeTrue(); + + Mail::expect($mailable)->toBeSent(function (Collection $matches): bool { + $firstMatch = $matches->first(); + $email = $firstMatch['email'] ?? null; if (! $email) { return false; @@ -413,10 +450,16 @@ public function build(): self } }; - Mail::to($to)->send($mailable); + $future = Mail::to($to)->send($mailable); + + /** @var Result $result */ + $result = $future->await(); - Mail::expect($mailable)->toBeSent(function (array $matches): bool { - $email = $matches['email'] ?? null; + expect($result->isSuccess())->toBeTrue(); + + Mail::expect($mailable)->toBeSent(function (Collection $matches): bool { + $firstMatch = $matches->first(); + $email = $firstMatch['email'] ?? null; if (! $email) { return false; } @@ -459,7 +502,12 @@ public function build(): self } }; - Mail::to($to)->send($mailable); + $future = Mail::to($to)->send($mailable); + + /** @var Result $result */ + $result = $future->await(); + + expect($result->isSuccess())->toBeFalse(); Mail::expect($mailable)->toNotBeSent(); })->throws(InvalidArgumentException::class); @@ -535,8 +583,9 @@ public function build(): self Mail::to($to)->send($mailable); - Mail::expect($mailable)->toBeSent(function (array $matches): bool { - $email = $matches['email'] ?? null; + Mail::expect($mailable)->toBeSent(function (Collection $matches): bool { + $firstMatch = $matches->first(); + $email = $firstMatch['email'] ?? null; if (! $email) { return false; diff --git a/tests/Unit/Queue/ParallelQueueTest.php b/tests/Unit/Queue/ParallelQueueTest.php index 5f492347..b4f75bd1 100644 --- a/tests/Unit/Queue/ParallelQueueTest.php +++ b/tests/Unit/Queue/ParallelQueueTest.php @@ -251,9 +251,6 @@ // Processor should still be running expect($parallelQueue->isProcessing())->ToBeTrue(); - - $parallelQueue->clear(); - $parallelQueue->stop(); }); it('automatically disables processing when no tasks are available to reserve', function (): void { @@ -357,8 +354,6 @@ // All tasks should eventually be processed or re-enqueued appropriately $this->assertGreaterThanOrEqual(0, $parallelQueue->size()); - - $parallelQueue->clear(); }); it('handles concurrent task reservation attempts correctly', function (): void { diff --git a/tests/Unit/Tasks/WorkerTest.php b/tests/Unit/Tasks/WorkerTest.php new file mode 100644 index 00000000..0c4f91f2 --- /dev/null +++ b/tests/Unit/Tasks/WorkerTest.php @@ -0,0 +1,17 @@ +push($task)->run(); + + expect($result->isSuccess())->toBeTrue(); + expect($result->output())->toBe('Task completed successfully'); +});