From 05adecd519765dd61c8186b471972a5dfaeb5fd5 Mon Sep 17 00:00:00 2001 From: Jacek Kobus Date: Wed, 28 Jan 2026 19:27:58 +0100 Subject: [PATCH 1/4] feat(logger): Implement custom logger injection for Typesense client --- examples/custom_logger.php | 73 +++++++++++ src/Lib/Configuration.php | 11 +- tests/Feature/ConfigurationTest.php | 195 ++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 examples/custom_logger.php create mode 100644 tests/Feature/ConfigurationTest.php diff --git a/examples/custom_logger.php b/examples/custom_logger.php new file mode 100644 index 00000000..3ae61aba --- /dev/null +++ b/examples/custom_logger.php @@ -0,0 +1,73 @@ +'; + + // Create your custom logger instance + $customLogger = new Logger('my-custom-logger'); + $customLogger->pushHandler(new StreamHandler('/tmp/typesense-custom.log', Logger::DEBUG)); + + // You can also use any other PSR-3 compatible logger + // For example: Symfony's Logger, Laravel's Logger, etc. + + echo "--------Example 1: Client with Custom Logger-------\n"; + // Initialize Typesense client with custom logger + $client = new Client( + [ + 'api_key' => 'xyz', + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + ], + ], + 'client' => new HttplugClient(), + 'logger' => $customLogger, // Inject your custom logger here + ] + ); + + // Use the client - all logs will now use your custom logger + $health = $client->health->retrieve(); + print_r($health); + echo "✓ Using custom logger - logs written to /tmp/typesense-custom.log\n"; + + echo "\n--------Example 2: Client with Default Logger-------\n"; + // Example without custom logger (uses default): + $clientWithDefaultLogger = new Client( + [ + 'api_key' => 'xyz', + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + ], + ], + 'client' => new HttplugClient(), + 'log_level' => Logger::INFO, // You can customize the log level + ] + ); + + $health2 = $clientWithDefaultLogger->health->retrieve(); + print_r($health2); + echo "✓ Using default logger - logs written to stdout\n"; + +} catch (Exception $e) { + echo $e->getMessage(); +} + diff --git a/src/Lib/Configuration.php b/src/Lib/Configuration.php index e0767ca2..f82b7f71 100644 --- a/src/Lib/Configuration.php +++ b/src/Lib/Configuration.php @@ -110,9 +110,14 @@ public function __construct(array $config) $this->numRetries = (float)($config['num_retries'] ?? 3); $this->retryIntervalSeconds = (float)($config['retry_interval_seconds'] ?? 1.0); - $this->logLevel = $config['log_level'] ?? Logger::WARNING; - $this->logger = new Logger('typesense'); - $this->logger->pushHandler(new StreamHandler('php://stdout', $this->logLevel)); + // Allow custom logger injection + if (isset($config['logger']) && $config['logger'] instanceof LoggerInterface) { + $this->logger = $config['logger']; + } else { + $this->logLevel = $config['log_level'] ?? Logger::WARNING; + $this->logger = new Logger('typesense'); + $this->logger->pushHandler(new StreamHandler('php://stdout', $this->logLevel)); + } if (isset($config['client'])) { if ($config['client'] instanceof HttpMethodsClient || $config['client'] instanceof ClientInterface) { diff --git a/tests/Feature/ConfigurationTest.php b/tests/Feature/ConfigurationTest.php new file mode 100644 index 00000000..991c70f7 --- /dev/null +++ b/tests/Feature/ConfigurationTest.php @@ -0,0 +1,195 @@ +baseConfig = [ + 'api_key' => 'test_api_key', + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + ], + ], + ]; + } + + public function testConfigurationWithDefaultLogger(): void + { + $config = new Configuration($this->baseConfig); + + $logger = $config->getLogger(); + + $this->assertInstanceOf(LoggerInterface::class, $logger); + $this->assertInstanceOf(Logger::class, $logger); + } + + public function testConfigurationWithCustomLogger(): void + { + // Create a custom logger + $customLogger = new Logger('custom-test-logger'); + $customLogger->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG)); + + // Add custom logger to config + $configWithCustomLogger = array_merge($this->baseConfig, [ + 'logger' => $customLogger, + ]); + + $config = new Configuration($configWithCustomLogger); + + $logger = $config->getLogger(); + + // Assert that the logger is the same instance we passed + $this->assertInstanceOf(LoggerInterface::class, $logger); + $this->assertSame($customLogger, $logger); + $this->assertEquals('custom-test-logger', $logger->getName()); + } + + public function testConfigurationWithCustomLogLevel(): void + { + // Add custom log level to config + $configWithLogLevel = array_merge($this->baseConfig, [ + 'log_level' => Logger::DEBUG, + ]); + + $config = new Configuration($configWithLogLevel); + + $logger = $config->getLogger(); + + $this->assertInstanceOf(LoggerInterface::class, $logger); + $this->assertInstanceOf(Logger::class, $logger); + } + + public function testConfigurationWithCustomLoggerOverridesLogLevel(): void + { + // Create a custom logger + $customLogger = new Logger('custom-logger-with-level'); + $customLogger->pushHandler(new StreamHandler('php://stdout', Logger::ERROR)); + + // Add both custom logger and log level to config (logger should win) + $configWithBoth = array_merge($this->baseConfig, [ + 'logger' => $customLogger, + 'log_level' => Logger::DEBUG, // This should be ignored + ]); + + $config = new Configuration($configWithBoth); + + $logger = $config->getLogger(); + + // Assert that the custom logger is used, not a new one with log_level + $this->assertSame($customLogger, $logger); + $this->assertEquals('custom-logger-with-level', $logger->getName()); + } + + public function testConfigurationWithMockLogger(): void + { + // Create a mock logger for testing + $mockLogger = $this->createMock(LoggerInterface::class); + + // Add mock logger to config + $configWithMockLogger = array_merge($this->baseConfig, [ + 'logger' => $mockLogger, + ]); + + $config = new Configuration($configWithMockLogger); + + $logger = $config->getLogger(); + + // Assert that the logger is the same instance we passed + $this->assertInstanceOf(LoggerInterface::class, $logger); + $this->assertSame($mockLogger, $logger); + } + + public function testConfigurationWithInvalidLoggerIgnored(): void + { + // Try to pass a non-logger object (should fall back to default) + $configWithInvalidLogger = array_merge($this->baseConfig, [ + 'logger' => 'not-a-logger-instance', + ]); + + $config = new Configuration($configWithInvalidLogger); + + $logger = $config->getLogger(); + + // Should fall back to default logger + $this->assertInstanceOf(LoggerInterface::class, $logger); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertEquals('typesense', $logger->getName()); + } + + public function testConfigurationThrowsErrorWhenNodesAreMissing(): void + { + $this->expectException(ConfigError::class); + $this->expectExceptionMessage('`nodes` is not defined.'); + + new Configuration([ + 'api_key' => 'test_api_key', + ]); + } + + public function testConfigurationThrowsErrorWhenApiKeyIsMissing(): void + { + $this->expectException(ConfigError::class); + $this->expectExceptionMessage('`api_key` is not defined.'); + + new Configuration([ + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + ], + ], + ]); + } + + public function testConfigurationWithAllOptions(): void + { + $customLogger = new Logger('full-config-logger'); + $customLogger->pushHandler(new StreamHandler('php://stdout', Logger::INFO)); + + $fullConfig = [ + 'api_key' => 'test_api_key', + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + 'path' => '/api', + ], + ], + 'nearest_node' => [ + 'host' => 'nearest.example.com', + 'port' => '443', + 'protocol' => 'https', + ], + 'logger' => $customLogger, + 'num_retries' => 5, + 'retry_interval_seconds' => 2.0, + 'healthcheck_interval_seconds' => 30, + 'randomize_nodes' => false, + ]; + + $config = new Configuration($fullConfig); + + $this->assertSame($customLogger, $config->getLogger()); + $this->assertEquals(5, $config->getNumRetries()); + $this->assertEquals(2.0, $config->getRetryIntervalSeconds()); + $this->assertEquals(30, $config->getHealthCheckIntervalSeconds()); + $this->assertNotNull($config->getNearestNode()); + } +} + From 23d674bd7961c621d7dd0b02c61d3347fe2bcb24 Mon Sep 17 00:00:00 2001 From: Jacek Kobus Date: Sat, 31 Jan 2026 15:13:38 +0100 Subject: [PATCH 2/4] feat(logger): Implement custom logger injection for Typesense client --- src/Lib/Configuration.php | 10 +++++- tests/Feature/ConfigurationTest.php | 55 ++++++----------------------- 2 files changed, 20 insertions(+), 45 deletions(-) diff --git a/src/Lib/Configuration.php b/src/Lib/Configuration.php index f82b7f71..68ed68d9 100644 --- a/src/Lib/Configuration.php +++ b/src/Lib/Configuration.php @@ -111,7 +111,15 @@ public function __construct(array $config) $this->retryIntervalSeconds = (float)($config['retry_interval_seconds'] ?? 1.0); // Allow custom logger injection - if (isset($config['logger']) && $config['logger'] instanceof LoggerInterface) { + if (isset($config['logger'])) { + if (!$config['logger'] instanceof LoggerInterface) { + throw new \InvalidArgumentException('Logger must implement Psr\Log\LoggerInterface'); + } + + if (isset($config['log_level'])) { + throw new \InvalidArgumentException('Setting log_level is not allowed when a custom logger is provided.'); + } + $this->logger = $config['logger']; } else { $this->logLevel = $config['log_level'] ?? Logger::WARNING; diff --git a/tests/Feature/ConfigurationTest.php b/tests/Feature/ConfigurationTest.php index 991c70f7..3e13b335 100644 --- a/tests/Feature/ConfigurationTest.php +++ b/tests/Feature/ConfigurationTest.php @@ -5,7 +5,6 @@ use Monolog\Handler\StreamHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; use Typesense\Exceptions\ConfigError; use Typesense\Lib\Configuration; @@ -33,7 +32,6 @@ public function testConfigurationWithDefaultLogger(): void $logger = $config->getLogger(); - $this->assertInstanceOf(LoggerInterface::class, $logger); $this->assertInstanceOf(Logger::class, $logger); } @@ -53,7 +51,6 @@ public function testConfigurationWithCustomLogger(): void $logger = $config->getLogger(); // Assert that the logger is the same instance we passed - $this->assertInstanceOf(LoggerInterface::class, $logger); $this->assertSame($customLogger, $logger); $this->assertEquals('custom-test-logger', $logger->getName()); } @@ -68,66 +65,36 @@ public function testConfigurationWithCustomLogLevel(): void $config = new Configuration($configWithLogLevel); $logger = $config->getLogger(); - - $this->assertInstanceOf(LoggerInterface::class, $logger); $this->assertInstanceOf(Logger::class, $logger); } - public function testConfigurationWithCustomLoggerOverridesLogLevel(): void + public function testConfigurationWithCustomLoggerThrowsExceptionWhenLogLevelIsAlsoProvided(): void { - // Create a custom logger + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Setting log_level is not allowed when a custom logger is provided.'); + $customLogger = new Logger('custom-logger-with-level'); $customLogger->pushHandler(new StreamHandler('php://stdout', Logger::ERROR)); - // Add both custom logger and log level to config (logger should win) $configWithBoth = array_merge($this->baseConfig, [ 'logger' => $customLogger, - 'log_level' => Logger::DEBUG, // This should be ignored + 'log_level' => Logger::DEBUG, ]); - $config = new Configuration($configWithBoth); - - $logger = $config->getLogger(); - - // Assert that the custom logger is used, not a new one with log_level - $this->assertSame($customLogger, $logger); - $this->assertEquals('custom-logger-with-level', $logger->getName()); + new Configuration($configWithBoth); } - public function testConfigurationWithMockLogger(): void + public function testConfigurationWithInvalidLoggerThrowsException(): void { - // Create a mock logger for testing - $mockLogger = $this->createMock(LoggerInterface::class); - - // Add mock logger to config - $configWithMockLogger = array_merge($this->baseConfig, [ - 'logger' => $mockLogger, - ]); - - $config = new Configuration($configWithMockLogger); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Logger must implement Psr\Log\LoggerInterface'); - $logger = $config->getLogger(); - - // Assert that the logger is the same instance we passed - $this->assertInstanceOf(LoggerInterface::class, $logger); - $this->assertSame($mockLogger, $logger); - } - - public function testConfigurationWithInvalidLoggerIgnored(): void - { - // Try to pass a non-logger object (should fall back to default) + // Try to pass a non-logger object (should throw exception) $configWithInvalidLogger = array_merge($this->baseConfig, [ 'logger' => 'not-a-logger-instance', ]); - $config = new Configuration($configWithInvalidLogger); - - $logger = $config->getLogger(); - - // Should fall back to default logger - $this->assertInstanceOf(LoggerInterface::class, $logger); - $this->assertInstanceOf(Logger::class, $logger); - $this->assertEquals('typesense', $logger->getName()); + new Configuration($configWithInvalidLogger); } public function testConfigurationThrowsErrorWhenNodesAreMissing(): void From 73007edf7eb1724bffdf1c12ddcdc5253b776f34 Mon Sep 17 00:00:00 2001 From: Jacek Kobus Date: Sat, 31 Jan 2026 15:19:22 +0100 Subject: [PATCH 3/4] feat(logger): Implement custom logger injection for Typesense client --- tests/Feature/ConfigurationTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/ConfigurationTest.php b/tests/Feature/ConfigurationTest.php index 3e13b335..4295afa3 100644 --- a/tests/Feature/ConfigurationTest.php +++ b/tests/Feature/ConfigurationTest.php @@ -52,7 +52,6 @@ public function testConfigurationWithCustomLogger(): void // Assert that the logger is the same instance we passed $this->assertSame($customLogger, $logger); - $this->assertEquals('custom-test-logger', $logger->getName()); } public function testConfigurationWithCustomLogLevel(): void From 510c07dd9724831deed97c0308d8be1efd9e7642 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 23:13:40 +0100 Subject: [PATCH 4/4] Update src/Lib/Configuration.php Co-authored-by: Markus Poerschke --- src/Lib/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lib/Configuration.php b/src/Lib/Configuration.php index 68ed68d9..b2c788c9 100644 --- a/src/Lib/Configuration.php +++ b/src/Lib/Configuration.php @@ -113,7 +113,7 @@ public function __construct(array $config) // Allow custom logger injection if (isset($config['logger'])) { if (!$config['logger'] instanceof LoggerInterface) { - throw new \InvalidArgumentException('Logger must implement Psr\Log\LoggerInterface'); + throw new ConfigError('Logger must implement Psr\Log\LoggerInterface'); } if (isset($config['log_level'])) {