diff --git a/.run/Main.run.xml b/.run/Main.run.xml
index 99253aa..b79a586 100644
--- a/.run/Main.run.xml
+++ b/.run/Main.run.xml
@@ -5,7 +5,7 @@
-
+
\ No newline at end of file
diff --git a/composer.json b/composer.json
index c741b15..dd00d9a 100644
--- a/composer.json
+++ b/composer.json
@@ -21,12 +21,14 @@
"lcobucci/jwt": "^5.6",
"adhocore/cli": "^1.9",
"robinvdvleuten/ulid": "^5.0",
- "monolog/monolog": "^3.9"
+ "monolog/monolog": "^3.9",
+ "react/promise": "^3",
+ "react/async": "^4"
},
"require-dev": {
"phpunit/phpunit": "^12.4",
"mockery/mockery": "^1.6",
- "squizlabs/php_codesniffer": "^3.12",
+ "squizlabs/php_codesniffer": "^4.0",
"lendable/composer-license-checker": "^1.2",
"phpstan/phpstan": "^2.1.31",
"kwn/php-rdkafka-stubs": "^2.2"
diff --git a/composer.lock b/composer.lock
index 2667d82..736ee65 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7c2d40400d6f4d0469324dc1645eba3c",
+ "content-hash": "856fdd307835b635e6e912a2d5028515",
"packages": [
{
"name": "adhocore/cli",
@@ -300,16 +300,16 @@
},
{
"name": "google/protobuf",
- "version": "v4.33.0",
+ "version": "v4.33.1",
"source": {
"type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git",
- "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d"
+ "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/b50269e23204e5ae859a326ec3d90f09efe3047d",
- "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d",
+ "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/0cd73ccf0cd26c3e72299cce1ea6144091a57e12",
+ "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12",
"shasum": ""
},
"require": {
@@ -338,13 +338,13 @@
"proto"
],
"support": {
- "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.0"
+ "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.1"
},
- "time": "2025-10-15T20:10:28+00:00"
+ "time": "2025-11-12T21:58:05+00:00"
},
{
"name": "illuminate/collections",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
@@ -403,7 +403,7 @@
},
{
"name": "illuminate/conditionable",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@@ -449,7 +449,7 @@
},
{
"name": "illuminate/container",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
@@ -510,7 +510,7 @@
},
{
"name": "illuminate/contracts",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -558,7 +558,7 @@
},
{
"name": "illuminate/database",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
@@ -629,7 +629,7 @@
},
{
"name": "illuminate/macroable",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@@ -675,7 +675,7 @@
},
{
"name": "illuminate/support",
- "version": "v12.38.0",
+ "version": "v12.38.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
@@ -1798,6 +1798,226 @@
},
"time": "2021-10-29T13:26:27+00:00"
},
+ {
+ "name": "react/async",
+ "version": "v4.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/async.git",
+ "reference": "635d50e30844a484495713e8cb8d9e079c0008a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5",
+ "reference": "635d50e30844a484495713e8cb8d9e079c0008a5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.2 || ^2.8 || ^1.2.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "1.10.39",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "React\\Async\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async utilities and fibers for ReactPHP",
+ "keywords": [
+ "async",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/async/issues",
+ "source": "https://github.com/reactphp/async/tree/v4.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-06-04T14:40:02+00:00"
+ },
+ {
+ "name": "react/event-loop",
+ "version": "v1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/event-loop.git",
+ "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "suggest": {
+ "ext-pcntl": "For signal handling support when using the StreamSelectLoop"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\EventLoop\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
+ "keywords": [
+ "asynchronous",
+ "event-loop"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/event-loop/issues",
+ "source": "https://github.com/reactphp/event-loop/tree/v1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-11-13T13:48:05+00:00"
+ },
+ {
+ "name": "react/promise",
+ "version": "v3.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise.git",
+ "reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
+ "reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "1.12.28 || 1.4.10",
+ "phpunit/phpunit": "^9.6 || ^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "React\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+ "keywords": [
+ "promise",
+ "promises"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise/issues",
+ "source": "https://github.com/reactphp/promise/tree/v3.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-08-19T18:57:03+00:00"
+ },
{
"name": "roadrunner-php/app-logger",
"version": "1.2.0",
@@ -2150,16 +2370,16 @@
},
{
"name": "spiral/roadrunner",
- "version": "v2025.1.4",
+ "version": "v2025.1.5",
"source": {
"type": "git",
"url": "https://github.com/roadrunner-server/roadrunner.git",
- "reference": "ff25363b72dd6ab2bd642d741db13b30d52d254f"
+ "reference": "d68bee29eb689c5310f8ede935c95a13bd7cc153"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/ff25363b72dd6ab2bd642d741db13b30d52d254f",
- "reference": "ff25363b72dd6ab2bd642d741db13b30d52d254f",
+ "url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/d68bee29eb689c5310f8ede935c95a13bd7cc153",
+ "reference": "d68bee29eb689c5310f8ede935c95a13bd7cc153",
"shasum": ""
},
"type": "metapackage",
@@ -2185,10 +2405,9 @@
"homepage": "https://roadrunner.dev/",
"support": {
"chat": "https://discord.gg/V6EK4he",
- "docs": "https://roadrunner.dev/docs",
- "forum": "https://forum.roadrunner.dev/",
+ "docs": "https://docs.roadrunner.dev/",
"issues": "https://github.com/roadrunner-server/roadrunner/issues",
- "source": "https://github.com/roadrunner-server/roadrunner/tree/v2025.1.4"
+ "source": "https://github.com/roadrunner-server/roadrunner/tree/v2025.1.5"
},
"funding": [
{
@@ -2196,7 +2415,7 @@
"type": "github"
}
],
- "time": "2025-10-02T16:43:06+00:00"
+ "time": "2025-11-13T17:24:29+00:00"
},
{
"name": "spiral/roadrunner-http",
@@ -3952,16 +4171,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.4.2",
+ "version": "12.4.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea"
+ "reference": "d8f644d8d9bb904867f7a0aeb1bd306e0d966949"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a94ea4d26d865875803b23aaf78c3c2c670ea2ea",
- "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d8f644d8d9bb904867f7a0aeb1bd306e0d966949",
+ "reference": "d8f644d8d9bb904867f7a0aeb1bd306e0d966949",
"shasum": ""
},
"require": {
@@ -4029,7 +4248,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.2"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.3"
},
"funding": [
{
@@ -4053,7 +4272,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-30T08:41:39+00:00"
+ "time": "2025-11-13T07:20:26+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -4954,26 +5173,26 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.13.5",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4"
+ "reference": "0525c73950de35ded110cffafb9892946d7771b5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
- "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5",
+ "reference": "0525c73950de35ded110cffafb9892946d7771b5",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
- "php": ">=5.4.0"
+ "php": ">=7.2.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31"
},
"bin": [
"bin/phpcbf",
@@ -4998,7 +5217,7 @@
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
}
],
- "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"keywords": [
"phpcs",
@@ -5029,7 +5248,7 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-04T16:30:35+00:00"
+ "time": "2025-11-10T16:43:36+00:00"
},
{
"name": "staabm/side-effects-detector",
@@ -5675,16 +5894,16 @@
},
{
"name": "theseer/tokenizer",
- "version": "1.2.3",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb",
+ "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb",
"shasum": ""
},
"require": {
@@ -5713,7 +5932,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.0"
},
"funding": [
{
@@ -5721,7 +5940,7 @@
"type": "github"
}
],
- "time": "2024-03-03T12:36:25+00:00"
+ "time": "2025-11-13T13:44:09+00:00"
}
],
"aliases": [],
@@ -5733,5 +5952,5 @@
"php": "^8.4"
},
"platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}
diff --git a/config.php b/config.php
index 81ef275..46129d9 100644
--- a/config.php
+++ b/config.php
@@ -4,6 +4,10 @@ use Siteworxpro\App\Helpers\Env;
return [
+ 'app' => [
+ 'log_level' => Env::get('LOG_LEVEL', 'debug'),
+ ],
+
/**
* The server configuration.
*/
diff --git a/src/Events/Dispatcher.php b/src/Events/Dispatcher.php
index e5b561c..2de241e 100644
--- a/src/Events/Dispatcher.php
+++ b/src/Events/Dispatcher.php
@@ -9,6 +9,9 @@ use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Siteworxpro\App\Attributes\Events\ListensFor;
+use function React\Async\await;
+use function React\Async\coroutine;
+
/**
* Class Dispatcher
*
@@ -29,6 +32,8 @@ class Dispatcher implements DispatcherContract, Arrayable
*/
private Collection $pushed;
+ private array $subscribers = [];
+
/**
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
*/
@@ -99,7 +104,7 @@ class Dispatcher implements DispatcherContract, Arrayable
*/
public function subscribe($subscriber): void
{
- $this->listeners = array_merge($this->listeners, (array) $subscriber);
+ $this->subscribers[] = $subscriber;
}
/**
@@ -108,6 +113,7 @@ class Dispatcher implements DispatcherContract, Arrayable
* @param $event
* @param array $payload
* @return array|null
+ * @throws \Throwable
*/
public function until($event, $payload = []): array|null
{
@@ -121,6 +127,7 @@ class Dispatcher implements DispatcherContract, Arrayable
* @param array $payload
* @param bool $halt
* @return array|null
+ * @throws \Throwable
*/
public function dispatch($event, $payload = [], $halt = false): array|null
{
@@ -130,23 +137,46 @@ class Dispatcher implements DispatcherContract, Arrayable
$eventClass = $event;
}
+ // Handle subscribers as a coroutine
+ $promise = coroutine(function () use ($event, $payload, $halt, $eventClass, &$responses) {
+ foreach ($this->subscribers as $subscriber) {
+ if (method_exists($subscriber, 'handle')) {
+ $response = $subscriber->handle($event, $payload);
+ $responses[$eventClass] = $response;
+
+ if ($halt && $response !== null) {
+ return $responses;
+ }
+ }
+ }
+
+ return null;
+ });
+
$listeners = $this->listeners[$eventClass] ?? null;
+ // If no listeners, just await the subscriber promise
if ($listeners === null) {
- return null;
+ return await($promise);
}
$responses = [];
-
foreach ($listeners as $listener) {
$response = $listener($event, $payload);
- $responses[] = $response;
+ $responses[$eventClass] = $response;
if ($halt && $response !== null) {
return $response;
}
}
+ // Await the subscriber promise and merge responses
+ $promiseResponses = await($promise);
+
+ if (is_array($promiseResponses)) {
+ $responses = array_merge($responses, $promiseResponses);
+ }
+
return $responses;
}
@@ -167,6 +197,7 @@ class Dispatcher implements DispatcherContract, Arrayable
*
* @param $event
* @return void
+ * @throws \Throwable
*/
public function flush($event): void
{
diff --git a/src/Events/Listeners/Database/Connected.php b/src/Events/Listeners/Database/Connected.php
index 94915cc..f7844e9 100644
--- a/src/Events/Listeners/Database/Connected.php
+++ b/src/Events/Listeners/Database/Connected.php
@@ -18,12 +18,15 @@ use Siteworxpro\App\Services\Facades\Logger;
class Connected extends Listener
{
/**
- * @param ConnectionEvent $event
+ * @param mixed $event
* @param array $payload
* @return null
*/
- public function __invoke($event, array $payload = []): null
+ public function __invoke(mixed $event, array $payload = []): null
{
+ if (!($event instanceof ConnectionEvent)) {
+ throw new \TypeError("Invalid event type passed to listener " . static::class);
+ }
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
diff --git a/src/Services/ServiceProviders/LoggerServiceProvider.php b/src/Services/ServiceProviders/LoggerServiceProvider.php
index 512757a..1745bdb 100644
--- a/src/Services/ServiceProviders/LoggerServiceProvider.php
+++ b/src/Services/ServiceProviders/LoggerServiceProvider.php
@@ -6,6 +6,7 @@ namespace Siteworxpro\App\Services\ServiceProviders;
use Illuminate\Support\ServiceProvider;
use Siteworxpro\App\Log\Logger;
+use Siteworxpro\App\Services\Facades\Config;
/**
* Class LoggerServiceProvider
@@ -17,7 +18,7 @@ class LoggerServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->singleton(Logger::class, function () {
- return new Logger();
+ return new Logger(Config::get('app.log_level'));
});
}
}
diff --git a/tests/Attributes/HandlesMessageTest.php b/tests/Attributes/HandlesMessageTest.php
index 7bc8c08..42823d9 100644
--- a/tests/Attributes/HandlesMessageTest.php
+++ b/tests/Attributes/HandlesMessageTest.php
@@ -11,7 +11,10 @@ class HandlesMessageTest extends Unit
{
public function testGetsClass(): void
{
- $reflection = new \ReflectionClass(TestClass::class);
+ $class = new #[HandlesMessage('Siteworxpro\Tests\Attributes\TestClass')] class {
+ };
+
+ $reflection = new \ReflectionClass($class);
$attributes = $reflection->getAttributes(HandlesMessage::class);
$this->assertCount(1, $attributes);
@@ -20,8 +23,3 @@ class HandlesMessageTest extends Unit
$this->assertEquals('Siteworxpro\Tests\Attributes\TestClass', $instance->getMessageClass());
}
}
-
-#[HandlesMessage('Siteworxpro\Tests\Attributes\TestClass')]
-class TestClass // @codingStandardsIgnoreLine
-{
-}
diff --git a/tests/Controllers/IndexControllerTest.php b/tests/Controllers/IndexControllerTest.php
index ff28901..2639a6d 100644
--- a/tests/Controllers/IndexControllerTest.php
+++ b/tests/Controllers/IndexControllerTest.php
@@ -22,4 +22,19 @@ class IndexControllerTest extends AbstractController
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
}
+
+ /**
+ * @throws \JsonException
+ */
+ public function testPost(): void
+ {
+ $this->assertTrue(true);
+
+ $controller = new IndexController();
+
+ $response = $controller->post($this->getMockRequest());
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
+ }
}
diff --git a/tests/Events/Listeners/ConnectedTest.php b/tests/Events/Listeners/ConnectedTest.php
new file mode 100644
index 0000000..7c5a3a2
--- /dev/null
+++ b/tests/Events/Listeners/ConnectedTest.php
@@ -0,0 +1,47 @@
+bind(Logger::class, fn() => $logger);
+ }
+
+ public function testHandlesEvent()
+ {
+ $this->expectNotToPerformAssertions();
+
+ $connectedEvent = $this->createMock(ConnectionEstablished::class);
+ $listener = new Connected();
+
+ $listener->__invoke($connectedEvent);
+ }
+
+ public function testThrowsException()
+ {
+ $this->expectException(\TypeError::class);
+ $listener = new Connected();
+ $listener->__invoke(new \stdClass());
+ }
+}
diff --git a/tests/Http/Middleware/CorsMiddlewareTest.php b/tests/Http/Middleware/CorsMiddlewareTest.php
index 3163ec4..4eecdd5 100644
--- a/tests/Http/Middleware/CorsMiddlewareTest.php
+++ b/tests/Http/Middleware/CorsMiddlewareTest.php
@@ -11,7 +11,7 @@ use Siteworxpro\App\Http\Middleware\CorsMiddleware;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\Tests\Unit;
-class CorsMiddlewareTest extends Unit
+class CorsMiddlewareTest extends Middleware
{
public function testAllowsConfiguredOrigin(): void
{
@@ -80,22 +80,4 @@ class CorsMiddlewareTest extends Unit
$this->assertEquals('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
}
-
- private function mockHandler(Response $response): RequestHandlerInterface
- {
- return new class ($response) implements RequestHandlerInterface {
- private Response $response;
-
- public function __construct(Response $response)
- {
- $this->response = $response;
- }
-
- public function handle(
- \Psr\Http\Message\ServerRequestInterface $request
- ): \Psr\Http\Message\ResponseInterface {
- return $this->response;
- }
- };
- }
}
diff --git a/tests/Http/Middleware/JwtMiddlewareTest.php b/tests/Http/Middleware/JwtMiddlewareTest.php
new file mode 100644
index 0000000..353748f
--- /dev/null
+++ b/tests/Http/Middleware/JwtMiddlewareTest.php
@@ -0,0 +1,230 @@
+shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $handler
+ ->shouldReceive('handle')
+ ->once()
+ ->andReturn(new Response(200));
+
+ $request = new ServerRequest('GET', '/');
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
+ }
+
+ /**
+ * @throws \JsonException
+ */
+ public function testIgnoresJwtAttributeButNoToken()
+ {
+ $class = $this->getClass();
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $request = new ServerRequest('GET', '/');
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
+ }
+
+ /**
+ * @throws \JsonException
+ */
+ public function testInvalidToken()
+ {
+ $class = $this->getClass();
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $request = new ServerRequest('GET', '/');
+ $request = $request->withHeader('Authorization', 'Bearer ' . 'invalid_token_string');
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
+ $this->assertStringContainsString(
+ 'Unauthorized: Invalid token',
+ $response->getBody()->getContents()
+ );
+ }
+
+ /**
+ * @throws \JsonException
+ */
+ public function testJwtAttributeWithTokenButWrongAud()
+ {
+ $class = $this->getClass();
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $request = new ServerRequest('GET', '/');
+ $request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
+ $this->assertStringContainsString(
+ 'The token is not allowed to be used by this audience',
+ $response->getBody()->getContents()
+ );
+ }
+
+ /**
+ * @throws \JsonException
+ */
+ public function testJwtAttributeWithTokenButWrongIss()
+ {
+ Config::set('jwt.audience', 'https://client-app.io');
+
+ $class = $this->getClass();
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $request = new ServerRequest('GET', '/');
+ $request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
+ $this->assertStringContainsString(
+ 'The token was not issued by the given issuers',
+ $response->getBody()->getContents()
+ );
+ }
+
+ /**
+ * @throws \JsonException
+ */
+ public function testJwtAttributeWithTokenWithDiffIssuer()
+ {
+ Config::set('jwt.audience', 'https://client-app.io');
+ Config::set('jwt.issuer', 'https://different-issuer.io');
+
+ $class = $this->getClass();
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $request = new ServerRequest('GET', '/');
+ $request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
+ $this->assertStringContainsString(
+ 'The token was not issued by the given issuers',
+ $response->getBody()->getContents()
+ );
+ }
+
+ public function testJwtAttributeWithToken()
+ {
+ Config::set('jwt.audience', 'https://client-app.io');
+ Config::set('jwt.issuer', 'https://api.my-awesome-app.io');
+
+ $class = $this->getClass();
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $handler
+ ->shouldReceive('handle')
+ ->once()
+ ->andReturn(new Response(200));
+
+ $request = new ServerRequest('GET', '/');
+ $request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
+ $middleware = new JwtMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
+ }
+
+ private function getJwt(): string
+ {
+ $key = InMemory::plainText(self::TEST_SIGNING_KEY);
+ $signer = new Sha256();
+
+ $token = new JwtFacade()->issue(
+ $signer,
+ $key,
+ static fn (
+ Builder $builder,
+ DateTimeImmutable $issuedAt
+ ): Builder => $builder
+ ->issuedBy('https://api.my-awesome-app.io')
+ ->permittedFor('https://client-app.io')
+ ->expiresAt($issuedAt->modify('+10 minutes'))
+ );
+
+ return $token->toString();
+ }
+}
diff --git a/tests/Http/Middleware/Middleware.php b/tests/Http/Middleware/Middleware.php
new file mode 100644
index 0000000..cfb69b5
--- /dev/null
+++ b/tests/Http/Middleware/Middleware.php
@@ -0,0 +1,32 @@
+response = $response;
+ }
+
+ public function handle(
+ ServerRequestInterface $request
+ ): ResponseInterface {
+ return $this->response;
+ }
+ };
+ }
+}
diff --git a/tests/Http/Middleware/ScopeMiddlewareTest.php b/tests/Http/Middleware/ScopeMiddlewareTest.php
new file mode 100644
index 0000000..57de8d9
--- /dev/null
+++ b/tests/Http/Middleware/ScopeMiddlewareTest.php
@@ -0,0 +1,111 @@
+shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $handler
+ ->shouldReceive('handle')
+ ->once()
+ ->andReturn(new Response(200));
+
+ $request = new ServerRequest('GET', '/');
+ $middleware = new ScopeMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ /**
+ * @throws \ReflectionException
+ * @throws \JsonException
+ */
+ public function testAllowsWithScope()
+ {
+ $class = new class {
+ public function getCallable(): array
+ {
+ return [ $this, 'index' ];
+ }
+
+ #[Scope(['admin'])]
+ public function index()
+ {
+ // Dummy method for testing
+ }
+ };
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $handler
+ ->shouldReceive('handle')
+ ->once()
+ ->andReturn(new Response(200));
+
+ $request = new ServerRequest('GET', '/')->withAttribute('scopes', ['admin', 'user']);
+ $middleware = new ScopeMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
+ }
+
+ /**
+ * @throws \ReflectionException
+ * @throws \JsonException
+ */
+ public function testDisallowsWithScope()
+ {
+ $class = new class {
+ public function getCallable(): array
+ {
+ return [ $this, 'index' ];
+ }
+
+ #[Scope(['admin'])]
+ public function index()
+ {
+ // Dummy method for testing
+ }
+ };
+
+ $handler = \Mockery::mock(Dispatcher::class);
+ $handler->shouldReceive('getMiddlewareStack')
+ ->andReturn([$class]);
+
+ $request = new ServerRequest('GET', '/');
+ $middleware = new ScopeMiddleware();
+ $response = $middleware->process($request, $handler);
+ $this->assertEquals(CodesEnum::FORBIDDEN->value, $response->getStatusCode());
+ }
+}
diff --git a/tests/Log/LoggerRpcTest.php b/tests/Log/LoggerRpcTest.php
index 329c96b..775fb00 100644
--- a/tests/Log/LoggerRpcTest.php
+++ b/tests/Log/LoggerRpcTest.php
@@ -34,7 +34,7 @@ class LoggerRpcTest extends Unit
$mock = Mockery::mock(LoggerInterface::class);
$mock->expects('debug')
->with('message', ['key' => 'value'])
- ->once();
+ ->times(1);
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
@@ -46,8 +46,6 @@ class LoggerRpcTest extends Unit
$logger->debug('message', ['key' => 'value']);
$mock->shouldHaveReceived('debug');
-
- Mockery::close();
}
/**
@@ -76,7 +74,6 @@ class LoggerRpcTest extends Unit
$logger->notice('message', ['key' => 'value']);
$mock->shouldHaveReceived('info')->times(2);
- Mockery::close();
}
/**
@@ -104,7 +101,6 @@ class LoggerRpcTest extends Unit
$logger->warning('message', ['key' => 'value']);
$mock->shouldHaveReceived('warning');
- Mockery::close();
}
/**
@@ -135,7 +131,6 @@ class LoggerRpcTest extends Unit
$logger->emergency('message', ['key' => 'value']);
$mock->shouldHaveReceived('error')->times(4);
- Mockery::close();
}
/**
@@ -162,6 +157,5 @@ class LoggerRpcTest extends Unit
$logger->log('notaloglevel', 'message', ['key' => 'value']);
$mock->shouldHaveReceived('log')->times(1);
- Mockery::close();
}
}
diff --git a/tests/Log/LoggerTest.php b/tests/Log/LoggerTest.php
index 4de291b..e618d67 100644
--- a/tests/Log/LoggerTest.php
+++ b/tests/Log/LoggerTest.php
@@ -4,12 +4,18 @@ declare(strict_types=1);
namespace Siteworxpro\Tests\Log;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LogLevel;
use Siteworxpro\App\Log\Logger;
use Siteworxpro\Tests\Unit;
class LoggerTest extends Unit
{
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
private function getLoggerWithBuffer(string $logLevel): array
{
$inputBuffer = fopen('php://memory', 'r+');
@@ -21,6 +27,10 @@ class LoggerTest extends Unit
return stream_get_contents($inputBuffer, -1, 0);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
private function testLogLevel(string $level): void
{
[$logger, $inputBuffer] = $this->getLoggerWithBuffer($level);
@@ -33,6 +43,10 @@ class LoggerTest extends Unit
$this->assertEquals('value', $decoded['context']['key']);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
private function testLogLevelEmpty(string $configLevel, string $logLevel): void
{
[$logger, $inputBuffer] = $this->getLoggerWithBuffer($configLevel);
@@ -42,57 +56,101 @@ class LoggerTest extends Unit
$this->assertEmpty($output);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsDebugMessageWhenLevelIsDebug(): void
{
$this->testLogLevel(LogLevel::DEBUG);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsInfoMessageWhenLevelIsInfo(): void
{
$this->testLogLevel(LogLevel::INFO);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsWarningMessageWhenLevelIsWarning(): void
{
$this->testLogLevel(LogLevel::WARNING);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsErrorMessageWhenLevelIsError(): void
{
$this->testLogLevel(LogLevel::ERROR);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsCriticalMessageWhenLevelIsCritical(): void
{
$this->testLogLevel(LogLevel::CRITICAL);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsAlertMessageWhenLevelIsAlert(): void
{
$this->testLogLevel(LogLevel::ALERT);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsEmergencyMessageWhenLevelIsEmergency(): void
{
$this->testLogLevel(LogLevel::EMERGENCY);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsNoticeMessageWhenLevelIsNotice(): void
{
$this->testLogLevel(LogLevel::NOTICE);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testDoesNotLogWhenMinimumLevelIsInfo(): void
{
$this->testLogLevelEmpty(LogLevel::INFO, LogLevel::DEBUG);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testDoesNotLogWhenMinimumLevelIsWarning(): void
{
$this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::INFO);
$this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::DEBUG);
}
+ /**
+ * @throws NotFoundExceptionInterface
+ * @throws ContainerExceptionInterface
+ */
public function testDoesNotLogWhenMinimumLevelIsError(): void
{
$this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::DEBUG);
@@ -100,12 +158,20 @@ class LoggerTest extends Unit
$this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::WARNING);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testDoesNotLogWhenMinimumLevelIsNotice(): void
{
$this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::DEBUG);
$this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::INFO);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsMessageWithEmptyContext(): void
{
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
@@ -118,6 +184,10 @@ class LoggerTest extends Unit
$this->assertEquals('Message without context', $decoded['message']);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsMessageWithComplexContext(): void
{
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
@@ -135,6 +205,10 @@ class LoggerTest extends Unit
$this->assertEquals('value', $decoded['context']['nested']['key']);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
public function testLogsStringableMessage(): void
{
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
diff --git a/tests/Unit.php b/tests/Unit.php
index b655657..f84d715 100644
--- a/tests/Unit.php
+++ b/tests/Unit.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Siteworxpro\Tests;
use Illuminate\Container\Container;
+use Mockery;
use PHPUnit\Framework\TestCase;
use Siteworx\Config\Config as SWConfig;
use Siteworxpro\App\Services\Facade;
@@ -29,5 +30,6 @@ abstract class Unit extends TestCase
{
Config::clearResolvedInstances();
Facade::setFacadeContainer(null);
+ Mockery::close();
}
}