diff --git a/.run/ Compose Deployment.run.xml b/.run/ Compose Deployment.run.xml
new file mode 100644
index 0000000..46b86bd
--- /dev/null
+++ b/.run/ Compose Deployment.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/All.run.xml b/.run/All.run.xml
new file mode 100644
index 0000000..77c7e80
--- /dev/null
+++ b/.run/All.run.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Lint_fix.run.xml b/.run/Lint_fix.run.xml
new file mode 100644
index 0000000..06b7491
--- /dev/null
+++ b/.run/Lint_fix.run.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Main.run.xml b/.run/Main.run.xml
new file mode 100644
index 0000000..99253aa
--- /dev/null
+++ b/.run/Main.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 587c7b9..2a5bbd0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -83,7 +83,7 @@ services:
postgres:
condition: service_healthy
environment:
- QUEUE_BROKER: kafka
+ QUEUE_BROKER: redis
PHP_IDE_CONFIG: serverName=localhost
WORKERS: 1
DEBUG: 1
diff --git a/src/Log/Logger.php b/src/Log/Logger.php
index 00c0a25..69127dc 100644
--- a/src/Log/Logger.php
+++ b/src/Log/Logger.php
@@ -6,10 +6,12 @@ namespace Siteworxpro\App\Log;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use RoadRunner\Logger\Logger as RRLogger;
-use Spiral\Goridge\RPC\RPC;
+use Siteworxpro\App\Services\Facades\RoadRunnerLogger;
/**
* Logger implementation that conforms to PSR-3 (`Psr\Log\LoggerInterface`).
@@ -30,9 +32,9 @@ class Logger implements LoggerInterface
/**
* RoadRunner RPC logger instance when running under RoadRunner.
*
- * @var RRLogger|null
+ * @var RRLogger | LoggerInterface | null
*/
- private ?RRLogger $rpcLogger = null;
+ private RRLogger | LoggerInterface | null $rpcLogger = null;
/**
* Monolog logger used as a fallback to write JSON-formatted logs to stdout.
@@ -66,21 +68,28 @@ class Logger implements LoggerInterface
* @param string $level Minimum level to log (PSR-3 level string). Messages with
* a higher numeric value in `$levels` will be ignored.
*
+ * @param resource | null $streamOutput Optional stream handler for Monolog.
+ *
* The default is `LogLevel::DEBUG` (log everything).
*
* If `$_SERVER['RR_RPC']` is set, an RPC connection will be attempted at
* $_SERVER['RR_RPC'] and a RoadRunner RPC logger will be used.
+ *
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
*/
- public function __construct(private readonly string $level = LogLevel::DEBUG)
- {
+ public function __construct(
+ private readonly string $level = LogLevel::DEBUG,
+ $streamOutput = null,
+ ) {
if (isset($_SERVER['RR_RPC'])) {
- $rpc = RPC::create($_SERVER['RR_RPC']);
- $this->rpcLogger = new RRLogger($rpc);
+ $this->rpcLogger = RoadRunnerLogger::getFacadeRoot();
}
$this->monologLogger = new \Monolog\Logger('app_logger');
$formatter = new JsonFormatter();
- $this->monologLogger->pushHandler(new StreamHandler('php://stdout')->setFormatter($formatter));
+ $stream = $streamOutput ?? 'php://stdout';
+ $this->monologLogger->pushHandler(new StreamHandler($stream)->setFormatter($formatter));
}
/**
@@ -192,7 +201,7 @@ class Logger implements LoggerInterface
*/
public function log($level, \Stringable|string $message, array $context = []): void
{
- if ($this->levels[$level] > $this->levels[$this->level]) {
+ if (isset($this->levels[$level]) && $this->levels[$level] > $this->levels[$this->level]) {
return;
}
@@ -215,7 +224,7 @@ class Logger implements LoggerInterface
$this->rpcLogger->error((string)$message, $context);
break;
default:
- $this->rpcLogger->log((string)$message, $context);
+ $this->rpcLogger->log($level, (string)$message, $context);
break;
}
diff --git a/src/Services/Facades/RoadRunnerLogger.php b/src/Services/Facades/RoadRunnerLogger.php
new file mode 100644
index 0000000..044f414
--- /dev/null
+++ b/src/Services/Facades/RoadRunnerLogger.php
@@ -0,0 +1,39 @@
+has(Logger::class) === false) {
+ $rpc = RPC::create($_SERVER['RR_RPC']);
+ $logger = new Logger($rpc);
+ $container->bind(static::getFacadeAccessor(), function () use ($logger) {
+ return $logger;
+ });
+
+ return $logger;
+ }
+
+ return $container->get(Logger::class);
+ }
+
+ protected static function getFacadeAccessor(): string
+ {
+ return Logger::class;
+ }
+}
diff --git a/tests/Facades/DispatcherTest.php b/tests/Facades/DispatcherTest.php
new file mode 100644
index 0000000..ade0fd9
--- /dev/null
+++ b/tests/Facades/DispatcherTest.php
@@ -0,0 +1,20 @@
+assertIsString($ulid);
+ $this->assertEquals(16, strlen($ulid));
+ }
+}
diff --git a/tests/Log/LoggerRpcTest.php b/tests/Log/LoggerRpcTest.php
new file mode 100644
index 0000000..329c96b
--- /dev/null
+++ b/tests/Log/LoggerRpcTest.php
@@ -0,0 +1,167 @@
+expectNotToPerformAssertions();
+
+ $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001';
+
+ $mock = Mockery::mock(LoggerInterface::class);
+ $mock->expects('debug')
+ ->with('message', ['key' => 'value'])
+ ->once();
+
+ \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
+ ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
+ return $mock;
+ });
+
+ $inputBuffer = fopen('php://memory', 'r+');
+ $logger = new Logger(LogLevel::DEBUG, $inputBuffer);
+ $logger->debug('message', ['key' => 'value']);
+
+ $mock->shouldHaveReceived('debug');
+
+ Mockery::close();
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testLogsDebugMessageWhenLevelIsInfoNotice(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001';
+
+ $mock = Mockery::mock(LoggerInterface::class);
+ $mock->expects('info')
+ ->with('message', ['key' => 'value'])
+ ->times(2);
+
+ \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
+ ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
+ return $mock;
+ });
+
+ $inputBuffer = fopen('php://memory', 'r+');
+ $logger = new Logger(LogLevel::DEBUG, $inputBuffer);
+ $logger->info('message', ['key' => 'value']);
+ $logger->notice('message', ['key' => 'value']);
+
+ $mock->shouldHaveReceived('info')->times(2);
+ Mockery::close();
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testLogsDebugMessageWhenLevelIsInfoWarning(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001';
+
+ $mock = Mockery::mock(LoggerInterface::class);
+ $mock->expects('warning')
+ ->with('message', ['key' => 'value'])
+ ->times(1);
+
+ \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
+ ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
+ return $mock;
+ });
+
+ $inputBuffer = fopen('php://memory', 'r+');
+ $logger = new Logger(LogLevel::DEBUG, $inputBuffer);
+ $logger->warning('message', ['key' => 'value']);
+
+ $mock->shouldHaveReceived('warning');
+ Mockery::close();
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testLogsDebugMessageWhenLevelIsInfoError(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001';
+
+ $mock = Mockery::mock(LoggerInterface::class);
+ $mock->expects('error')
+ ->with('message', ['key' => 'value'])
+ ->times(4);
+
+ \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
+ ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
+ return $mock;
+ });
+
+ $inputBuffer = fopen('php://memory', 'r+');
+ $logger = new Logger(LogLevel::DEBUG, $inputBuffer);
+ $logger->error('message', ['key' => 'value']);
+ $logger->critical('message', ['key' => 'value']);
+ $logger->alert('message', ['key' => 'value']);
+ $logger->emergency('message', ['key' => 'value']);
+
+ $mock->shouldHaveReceived('error')->times(4);
+ Mockery::close();
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testLogsLog(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $_SERVER['RR_RPC'] = 'tcp://127.0.0.1:6001';
+
+ $mock = Mockery::mock(LoggerInterface::class);
+ $mock->expects('log')
+ ->with('notaloglevel', 'message', ['key' => 'value']);
+
+ \Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
+ ->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
+ return $mock;
+ });
+
+ $inputBuffer = fopen('php://memory', 'r+');
+ $logger = new Logger(LogLevel::DEBUG, $inputBuffer);
+ $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
new file mode 100644
index 0000000..4de291b
--- /dev/null
+++ b/tests/Log/LoggerTest.php
@@ -0,0 +1,155 @@
+getLoggerWithBuffer($level);
+ $logger->$level('message', ['key' => 'value']);
+ $output = $this->getContents($inputBuffer);
+
+ $this->assertNotEmpty($output);
+ $decoded = json_decode($output, true);
+ $this->assertEquals('message', $decoded['message']);
+ $this->assertEquals('value', $decoded['context']['key']);
+ }
+
+ private function testLogLevelEmpty(string $configLevel, string $logLevel): void
+ {
+ [$logger, $inputBuffer] = $this->getLoggerWithBuffer($configLevel);
+ $logger->$logLevel('message', ['key' => 'value']);
+ $output = $this->getContents($inputBuffer);
+
+ $this->assertEmpty($output);
+ }
+
+ public function testLogsDebugMessageWhenLevelIsDebug(): void
+ {
+ $this->testLogLevel(LogLevel::DEBUG);
+ }
+
+ public function testLogsInfoMessageWhenLevelIsInfo(): void
+ {
+ $this->testLogLevel(LogLevel::INFO);
+ }
+
+ public function testLogsWarningMessageWhenLevelIsWarning(): void
+ {
+ $this->testLogLevel(LogLevel::WARNING);
+ }
+
+ public function testLogsErrorMessageWhenLevelIsError(): void
+ {
+ $this->testLogLevel(LogLevel::ERROR);
+ }
+
+ public function testLogsCriticalMessageWhenLevelIsCritical(): void
+ {
+ $this->testLogLevel(LogLevel::CRITICAL);
+ }
+
+ public function testLogsAlertMessageWhenLevelIsAlert(): void
+ {
+ $this->testLogLevel(LogLevel::ALERT);
+ }
+
+ public function testLogsEmergencyMessageWhenLevelIsEmergency(): void
+ {
+ $this->testLogLevel(LogLevel::EMERGENCY);
+ }
+
+ public function testLogsNoticeMessageWhenLevelIsNotice(): void
+ {
+ $this->testLogLevel(LogLevel::NOTICE);
+ }
+
+ public function testDoesNotLogWhenMinimumLevelIsInfo(): void
+ {
+ $this->testLogLevelEmpty(LogLevel::INFO, LogLevel::DEBUG);
+ }
+
+ public function testDoesNotLogWhenMinimumLevelIsWarning(): void
+ {
+ $this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::INFO);
+ $this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::DEBUG);
+ }
+
+ public function testDoesNotLogWhenMinimumLevelIsError(): void
+ {
+ $this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::DEBUG);
+ $this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::INFO);
+ $this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::WARNING);
+ }
+
+ public function testDoesNotLogWhenMinimumLevelIsNotice(): void
+ {
+ $this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::DEBUG);
+ $this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::INFO);
+ }
+
+ public function testLogsMessageWithEmptyContext(): void
+ {
+ [$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
+
+ $logger->info('Message without context');
+ $output = $this->getContents($buffer);
+
+ $this->assertNotEmpty($output);
+ $decoded = json_decode($output, true);
+ $this->assertEquals('Message without context', $decoded['message']);
+ }
+
+ public function testLogsMessageWithComplexContext(): void
+ {
+ [$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
+
+ $logger->info('Complex context', [
+ 'user_id' => 123,
+ 'nested' => ['key' => 'value'],
+ 'array' => [1, 2, 3]
+ ]);
+ $output = $this->getContents($buffer);
+
+ $this->assertNotEmpty($output);
+ $decoded = json_decode($output, true);
+ $this->assertEquals(123, $decoded['context']['user_id']);
+ $this->assertEquals('value', $decoded['context']['nested']['key']);
+ }
+
+ public function testLogsStringableMessage(): void
+ {
+ [$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
+
+ $stringable = new class implements \Stringable {
+ public function __toString(): string
+ {
+ return 'Stringable message';
+ }
+ };
+
+ $logger->info($stringable);
+ $output = $this->getContents($buffer);
+ $this->assertNotEmpty($output);
+ $decoded = json_decode($output, true);
+ $this->assertEquals('Stringable message', $decoded['message']);
+ }
+}