From ed2005b99f73c7e70b8923444588ff27b1d98a04 Mon Sep 17 00:00:00 2001 From: David Galet Date: Sat, 24 Oct 2026 09:13:39 +0200 Subject: [PATCH] feat: add query logging functionality with enable/disable control --- README.md | 12 ++++++++ index.php | 2 ++ src/Database/QueryCollector.php | 25 +++++++++++++++++ tests/CliTest.php | 41 +++++++++++++++++++++++++++ tests/QueryCollectorTest.php | 49 +++++++++++++++++++++++++++++++++ tests/TinkerTest.php | 1 + 6 files changed, 130 insertions(+) diff --git a/README.md b/README.md index 17fe8b5..528bf8e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,18 @@ php client.phar execute php client.phar execute-stream ``` +Available options: + +- `--query-log=`: enable database query collection for `execute` and + `execute-stream`. Accepts `true`/`false`, `1`/`0`, `on`/`off`, and + `yes`/`no`. Query logging is disabled by default: no query listeners are + attached to the project's database, and every `queries` field in the output + is an empty array. + +```bash +php client.phar /path/to/project execute "$code" --query-log=true +``` + ## Check Project Info `info` reports the detected project type, project version, and PHP version: diff --git a/index.php b/index.php index ce85691..9648a80 100644 --- a/index.php +++ b/index.php @@ -65,6 +65,8 @@ $customLoader = Cli::getArgument('loader'); try { + QueryCollector::enable(filter_var(Cli::getArgument('query-log'), FILTER_VALIDATE_BOOL)); + $loader = Loader::load($arguments[1], $customLoader ?: null); if ($loader === null) { diff --git a/src/Database/QueryCollector.php b/src/Database/QueryCollector.php index 703529a..8cd0c39 100644 --- a/src/Database/QueryCollector.php +++ b/src/Database/QueryCollector.php @@ -4,6 +4,8 @@ class QueryCollector { + protected static bool $enabled = false; + /** @var QueryProviderInterface[] */ protected static array $providers = []; @@ -13,13 +15,28 @@ class QueryCollector protected static array $errors = []; + public static function enable(bool $enabled = true): void + { + self::$enabled = $enabled; + } + + public static function isEnabled(): bool + { + return self::$enabled; + } + public static function register(QueryProviderInterface $provider): void { + if (! self::$enabled) { + return; + } + self::$providers[] = $provider; } public static function reset(): void { + self::$enabled = false; self::$providers = []; self::$queries = []; self::$allCapturedQueries = []; @@ -28,6 +45,10 @@ public static function reset(): void public static function start(): void { + if (! self::$enabled) { + return; + } + self::$queries = []; self::$errors = []; @@ -42,6 +63,10 @@ public static function start(): void public static function stop(): array { + if (! self::$enabled) { + return []; + } + self::$queries = []; foreach (self::$providers as $provider) { diff --git a/tests/CliTest.php b/tests/CliTest.php index d2edc80..d87ba5e 100644 --- a/tests/CliTest.php +++ b/tests/CliTest.php @@ -65,4 +65,45 @@ public function test_execute_reports_runtime_errors_with_queries_array(): void $this->assertArrayHasKey('queries', $data); $this->assertIsArray($data['queries']); } + + /** @dataProvider provideQueryLogArguments */ + public function test_execute_keeps_queries_key_with_query_log_argument(string $argument): void + { + $code = base64_encode('return 1;'); + $command = sprintf( + '%s %s %s %s %s', + escapeshellarg(PHP_BINARY), + escapeshellarg(__DIR__.'/../index.php'), + escapeshellarg(__DIR__.'/..'), + 'execute', + escapeshellarg($code) + ); + + if ($argument !== '') { + $command .= ' '.escapeshellarg($argument); + } + + exec($command, $output, $exitCode); + + $this->assertSame(0, $exitCode); + $rawOutput = implode(PHP_EOL, $output); + $this->assertStringStartsWith('TWEAKPHP_RESULT:', $rawOutput); + + $data = json_decode(substr($rawOutput, strlen('TWEAKPHP_RESULT:')), true); + + $this->assertIsArray($data); + $this->assertArrayHasKey('queries', $data); + $this->assertSame([], $data['queries']); + $this->assertArrayHasKey('queries', $data['output'][0]); + $this->assertSame([], $data['output'][0]['queries']); + } + + public function provideQueryLogArguments(): array + { + return [ + 'no argument' => [''], + 'query log disabled' => ['--query-log=false'], + 'query log enabled' => ['--query-log=true'], + ]; + } } diff --git a/tests/QueryCollectorTest.php b/tests/QueryCollectorTest.php index 8010676..7bd5bf6 100644 --- a/tests/QueryCollectorTest.php +++ b/tests/QueryCollectorTest.php @@ -69,6 +69,7 @@ class QueryCollectorTest extends TestCase protected function setUp(): void { QueryCollector::reset(); + QueryCollector::enable(true); } public function test_query_collector_with_laravel_db() @@ -283,5 +284,53 @@ public function test_query_collector_get_captured_queries_accumulates_across_sta $this->assertSame('SELECT * FROM users WHERE id = 1', $allQueries[0]['sql']); $this->assertSame('SELECT * FROM users WHERE id = 999', $allQueries[1]['sql']); } + + public function test_query_collector_is_disabled_by_default(): void + { + QueryCollector::reset(); + + $this->assertFalse(QueryCollector::isEnabled()); + } + + public function test_query_collector_ignores_providers_when_disabled(): void + { + QueryCollector::enable(false); + + $provider = $this->createMock(QueryProviderInterface::class); + $provider->expects($this->never())->method('start'); + $provider->expects($this->never())->method('stop'); + + QueryCollector::register($provider); + QueryCollector::start(); + + $this->assertSame([], QueryCollector::stop()); + $this->assertSame([], QueryCollector::errors()); + $this->assertSame([], QueryCollector::getCapturedQueries()); + } + + public function test_query_collector_ignores_registration_from_before_enable(): void + { + QueryCollector::enable(false); + + $ignoredProvider = $this->createMock(QueryProviderInterface::class); + $ignoredProvider->expects($this->never())->method('start'); + $ignoredProvider->expects($this->never())->method('stop'); + QueryCollector::register($ignoredProvider); + + QueryCollector::enable(true); + + $provider = $this->createMock(QueryProviderInterface::class); + $provider->expects($this->once())->method('start'); + $provider->expects($this->once())->method('stop')->willReturn([ + ['sql' => 'SELECT 1'], + ]); + QueryCollector::register($provider); + + QueryCollector::start(); + + $this->assertSame([ + ['sql' => 'SELECT 1'], + ], QueryCollector::stop()); + } } } diff --git a/tests/TinkerTest.php b/tests/TinkerTest.php index a77a418..4c0d0b7 100644 --- a/tests/TinkerTest.php +++ b/tests/TinkerTest.php @@ -238,6 +238,7 @@ protected function doExecuteStreaming(string $code, int $index, callable $onEven }; $provider = $this->createMock(QueryProviderInterface::class); $provider->method('stop')->willThrowException(new \RuntimeException('query collection failed')); + QueryCollector::enable(true); QueryCollector::register($provider); $events = [];