Skip to content

Commit 60e0066

Browse files
committed
refactor: extract environment config loading to EnvConfigLoader class
- Move database and cache configuration loading logic from PdoDb::fromEnv() to EnvConfigLoader - Add EnvConfigLoader class to centralize environment variable parsing - Fix CacheManager reflection issues with typed properties (isInitialized checks) - Fix unreachable code in CacheCommand after showError() call - Improve cache config loading in BaseCliCommand to handle putenv() variables - Add test script for cache statistics testing
1 parent b08bbf8 commit 60e0066

File tree

7 files changed

+665
-161
lines changed

7 files changed

+665
-161
lines changed

scripts/test-cache-stats.php

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
/**
7+
* Cache Stats Test Script
8+
*
9+
* Generates cacheable queries to test pdodb ui cache stats pane.
10+
* Uses .env file from project root for database configuration.
11+
*
12+
* Usage:
13+
* php scripts/test-cache-stats.php [N] [delay]
14+
*
15+
* Arguments:
16+
* N - Number of queries to execute (default: 50)
17+
* delay - Delay between queries in seconds (default: 1)
18+
*
19+
* Environment:
20+
* Loads database configuration from .env file in project root
21+
* Requires PDODB_DRIVER and other PDODB_* variables
22+
*/
23+
24+
require_once __DIR__ . '/../vendor/autoload.php';
25+
26+
use tommyknocker\pdodb\PdoDb;
27+
use tommyknocker\pdodb\helpers\Db;
28+
29+
// Get arguments
30+
$numQueries = isset($argv[1]) && is_numeric($argv[1]) ? (int)$argv[1] : 50;
31+
$delay = isset($argv[2]) && is_numeric($argv[2]) ? (float)$argv[2] : 1.0;
32+
33+
echo "=== Cache Stats Test Script ===\n\n";
34+
echo "Queries to execute: {$numQueries}\n";
35+
echo "Delay between queries: {$delay}s\n\n";
36+
37+
// Load .env from project root
38+
$envPath = __DIR__ . '/../.env';
39+
if (!file_exists($envPath)) {
40+
echo "Error: .env file not found at: {$envPath}\n";
41+
echo "Please create .env file in project root with PDODB_* variables.\n";
42+
exit(1);
43+
}
44+
45+
echo "Loading configuration from: {$envPath}\n";
46+
47+
// Create database connection from .env (cache will be auto-created from .env if PDODB_CACHE_ENABLED=true)
48+
try {
49+
$db = PdoDb::fromEnv($envPath);
50+
51+
// Display cache info if available
52+
$cacheManager = $db->getCacheManager();
53+
if ($cacheManager !== null) {
54+
$config = $cacheManager->getConfig();
55+
echo "✓ Database connection established with cache enabled\n";
56+
echo " Cache enabled: " . ($config->isEnabled() ? 'Yes' : 'No') . "\n";
57+
58+
// Try to detect cache type
59+
$cache = $cacheManager->getCache();
60+
$cacheTypeName = 'Unknown';
61+
if ($cache instanceof \Symfony\Component\Cache\Psr16Cache) {
62+
// Use reflection to get the adapter
63+
$reflection = new \ReflectionClass($cache);
64+
if ($reflection->hasProperty('pool')) {
65+
$property = $reflection->getProperty('pool');
66+
$property->setAccessible(true);
67+
$adapter = $property->getValue($cache);
68+
if ($adapter instanceof \Symfony\Component\Cache\Adapter\RedisAdapter) {
69+
$cacheTypeName = 'Redis';
70+
} elseif ($adapter instanceof \Symfony\Component\Cache\Adapter\FilesystemAdapter) {
71+
$cacheTypeName = 'Filesystem';
72+
} elseif ($adapter instanceof \Symfony\Component\Cache\Adapter\ApcuAdapter) {
73+
$cacheTypeName = 'APCu';
74+
} else {
75+
$cacheTypeName = get_class($adapter);
76+
}
77+
}
78+
} else {
79+
$cacheTypeName = get_class($cache);
80+
}
81+
echo " Cache type: {$cacheTypeName}\n";
82+
83+
// Show cache config
84+
$config = $cacheManager->getConfig();
85+
echo " Cache prefix: " . $config->getPrefix() . "\n";
86+
echo " Cache default TTL: " . $config->getDefaultTtl() . "s\n";
87+
echo " Cache enabled: " . ($config->isEnabled() ? 'Yes' : 'No') . "\n";
88+
89+
// Show initial stats
90+
$stats = $cacheManager->getStats();
91+
echo " Initial stats - Hits: " . ($stats['hits'] ?? 0) . ", Misses: " . ($stats['misses'] ?? 0) . ", Sets: " . ($stats['sets'] ?? 0) . "\n\n";
92+
} else {
93+
echo "✓ Database connection established (cache disabled)\n";
94+
echo " Warning: CacheManager is null. Check PDODB_CACHE_ENABLED in .env\n\n";
95+
}
96+
} catch (\Exception $e) {
97+
echo "Error: Failed to connect to database: " . $e->getMessage() . "\n";
98+
exit(1);
99+
}
100+
101+
// Create test table if it doesn't exist
102+
$tableName = 'cache_test_users';
103+
$driver = $db->connection->getDriverName();
104+
105+
echo "Creating test table: {$tableName}\n";
106+
try {
107+
// Use DDL Query Builder for cross-dialect compatibility
108+
$schema = $db->schema();
109+
110+
// Check if table exists
111+
if (!$schema->tableExists($tableName)) {
112+
$schema->createTable($tableName, [
113+
'id' => $schema->primaryKey(),
114+
'name' => $schema->string(100)->notNull(),
115+
'email' => $schema->string(100)->notNull(),
116+
'age' => $schema->integer(),
117+
'status' => $schema->string(20)->defaultValue('active'),
118+
'category' => $schema->string(50),
119+
'created_at' => $schema->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
120+
]);
121+
122+
// Create indexes
123+
$schema->createIndex("idx_{$tableName}_status", $tableName, ['status']);
124+
$schema->createIndex("idx_{$tableName}_category", $tableName, ['category']);
125+
$schema->createIndex("idx_{$tableName}_age", $tableName, ['age']);
126+
127+
echo "✓ Test table created\n";
128+
} else {
129+
echo "✓ Test table already exists\n";
130+
}
131+
} catch (\Exception $e) {
132+
echo "Warning: Could not create table: " . $e->getMessage() . "\n";
133+
echo "Continuing with existing table...\n";
134+
}
135+
136+
// Insert some test data if table is empty
137+
$count = $db->find()->from($tableName)->select('COUNT(*)')->getValue();
138+
if ($count === 0 || $count === null) {
139+
echo "Inserting test data...\n";
140+
$categories = ['Electronics', 'Clothing', 'Food', 'Books', 'Sports'];
141+
$statuses = ['active', 'inactive', 'pending'];
142+
143+
$data = [];
144+
for ($i = 1; $i <= 100; $i++) {
145+
$data[] = [
146+
'name' => "User {$i}",
147+
'email' => "user{$i}@example.com",
148+
'age' => 20 + ($i % 50),
149+
'status' => $statuses[$i % 3],
150+
'category' => $categories[$i % 5],
151+
];
152+
}
153+
154+
try {
155+
$db->find()->table($tableName)->insertMulti($data);
156+
echo "✓ Inserted 100 test records\n";
157+
} catch (\Exception $e) {
158+
echo "Warning: Could not insert test data: " . $e->getMessage() . "\n";
159+
}
160+
} else {
161+
echo "✓ Test data already exists ({$count} records)\n";
162+
}
163+
164+
echo "\nStarting query execution...\n";
165+
echo "Press Ctrl+C to stop\n\n";
166+
167+
// Define different query patterns for cache testing
168+
// Some patterns use fixed values to generate cache hits, others use variable values
169+
$queryPatterns = [
170+
// Pattern 1: Simple WHERE with fixed values (will generate cache hits)
171+
function ($i) use ($db, $tableName) {
172+
// Use fixed values for first 3 queries, then repeat
173+
$statuses = ['active', 'inactive', 'pending'];
174+
$statusIndex = ($i < 3) ? $i : ($i % 3);
175+
$status = $statuses[$statusIndex];
176+
return $db->find()
177+
->from($tableName)
178+
->where('status', $status)
179+
->cache(3600)
180+
->get();
181+
},
182+
183+
// Pattern 2: WHERE with fixed age range (will generate cache hits)
184+
function ($i) use ($db, $tableName) {
185+
// Use fixed age ranges that repeat
186+
$ranges = [[20, 30], [30, 40], [40, 50]];
187+
$rangeIndex = ($i < 3) ? $i : ($i % 3);
188+
[$minAge, $maxAge] = $ranges[$rangeIndex];
189+
return $db->find()
190+
->from($tableName)
191+
->where('age', $minAge, '>=')
192+
->andWhere('age', $maxAge, '<=')
193+
->cache(3600)
194+
->get();
195+
},
196+
197+
// Pattern 3: Category filter with fixed values (will generate cache hits)
198+
function ($i) use ($db, $tableName) {
199+
$categories = ['Electronics', 'Clothing', 'Food', 'Books', 'Sports'];
200+
$categoryIndex = ($i < 5) ? $i : ($i % 5);
201+
$category = $categories[$categoryIndex];
202+
return $db->find()
203+
->from($tableName)
204+
->where('category', $category)
205+
->cache(3600)
206+
->get();
207+
},
208+
209+
// Pattern 4: Aggregation with GROUP BY (always same query - perfect for cache)
210+
function ($i) use ($db, $tableName) {
211+
return $db->find()
212+
->from($tableName)
213+
->select(['status', 'count' => Db::count('*')])
214+
->groupBy('status')
215+
->cache(3600)
216+
->get();
217+
},
218+
219+
// Pattern 5: Aggregation by category (always same query - perfect for cache)
220+
function ($i) use ($db, $tableName) {
221+
return $db->find()
222+
->from($tableName)
223+
->select(['category', 'avg_age' => Db::avg('age'), 'total' => Db::count('*')])
224+
->groupBy('category')
225+
->cache(3600)
226+
->get();
227+
},
228+
229+
// Pattern 6: Complex WHERE with fixed conditions (will generate cache hits)
230+
function ($i) use ($db, $tableName) {
231+
$conditions = [
232+
['active', 25],
233+
['inactive', 30],
234+
['active', 35],
235+
];
236+
$condIndex = ($i < 3) ? $i : ($i % 3);
237+
[$status, $minAge] = $conditions[$condIndex];
238+
return $db->find()
239+
->from($tableName)
240+
->where('status', $status)
241+
->andWhere('age', $minAge, '>=')
242+
->orderBy('age', 'ASC')
243+
->limit(10)
244+
->cache(3600)
245+
->get();
246+
},
247+
248+
// Pattern 7: COUNT query with fixed values (will generate cache hits)
249+
function ($i) use ($db, $tableName) {
250+
$statuses = ['active', 'inactive', 'pending'];
251+
$statusIndex = ($i < 3) ? $i : ($i % 3);
252+
$status = $statuses[$statusIndex];
253+
return $db->find()
254+
->from($tableName)
255+
->where('status', $status)
256+
->select('COUNT(*)')
257+
->cache(3600)
258+
->getValue();
259+
},
260+
261+
// Pattern 8: ORDER BY with fixed LIMIT (will generate cache hits)
262+
function ($i) use ($db, $tableName) {
263+
$limits = [5, 10, 15];
264+
$limitIndex = ($i < 3) ? $i : ($i % 3);
265+
$limit = $limits[$limitIndex];
266+
return $db->find()
267+
->from($tableName)
268+
->orderBy('age', 'DESC')
269+
->limit($limit)
270+
->cache(3600)
271+
->get();
272+
},
273+
];
274+
275+
$patternCount = count($queryPatterns);
276+
$startTime = microtime(true);
277+
$queryCount = 0;
278+
$errorCount = 0;
279+
280+
// Execute queries in loop
281+
// Use modulo to repeat same queries for cache hits demonstration
282+
for ($i = 0; $i < $numQueries; $i++) {
283+
// Repeat patterns to generate cache hits
284+
// First half: unique queries (cache misses)
285+
// Second half: repeat same queries (cache hits)
286+
$patternIndex = ($i < $numQueries / 2)
287+
? ($i % $patternCount)
288+
: (($i - (int)($numQueries / 2)) % $patternCount);
289+
$queryNum = $i + 1;
290+
291+
try {
292+
$queryStart = microtime(true);
293+
$result = $queryPatterns[$patternIndex]($i);
294+
$queryTime = (microtime(true) - $queryStart) * 1000;
295+
296+
$resultCount = is_array($result) ? count($result) : 1;
297+
echo sprintf(
298+
"[%d/%d] Query pattern %d: %d rows, %.2fms\n",
299+
$queryNum,
300+
$numQueries,
301+
$patternIndex + 1,
302+
$resultCount,
303+
$queryTime
304+
);
305+
306+
$queryCount++;
307+
308+
// Delay between queries
309+
if ($i < $numQueries - 1 && $delay > 0) {
310+
usleep((int)($delay * 1000000));
311+
}
312+
} catch (\Exception $e) {
313+
$errorCount++;
314+
echo sprintf(
315+
"[%d/%d] Error in query #%d: %s\n",
316+
$queryNum,
317+
$numQueries,
318+
$patternIndex + 1,
319+
$e->getMessage()
320+
);
321+
}
322+
}
323+
324+
$totalTime = microtime(true) - $startTime;
325+
326+
echo "\n=== Summary ===\n";
327+
echo "Total queries executed: {$queryCount}\n";
328+
echo "Errors: {$errorCount}\n";
329+
echo "Total time: " . number_format($totalTime, 2) . "s\n";
330+
echo "Average time per query: " . number_format(($totalTime / max($queryCount, 1)) * 1000, 2) . "ms\n";
331+
echo "\n";
332+
333+
// Show cache statistics if available
334+
if (method_exists($db, 'getCacheManager')) {
335+
$cacheManager = $db->getCacheManager();
336+
if ($cacheManager !== null) {
337+
$stats = $cacheManager->getStats();
338+
echo "=== Cache Statistics ===\n";
339+
echo "Enabled: " . ($stats['enabled'] ?? false ? 'Yes' : 'No') . "\n";
340+
echo "Hits: " . ($stats['hits'] ?? 0) . "\n";
341+
echo "Misses: " . ($stats['misses'] ?? 0) . "\n";
342+
echo "Sets: " . ($stats['sets'] ?? 0) . "\n";
343+
echo "Deletes: " . ($stats['deletes'] ?? 0) . "\n";
344+
$total = ($stats['hits'] ?? 0) + ($stats['misses'] ?? 0);
345+
if ($total > 0) {
346+
$hitRate = (($stats['hits'] ?? 0) / $total) * 100;
347+
echo "Hit rate: " . number_format($hitRate, 2) . "%\n";
348+
} else {
349+
echo "Warning: No cache operations detected. Check if cache is enabled and queries use cache() method.\n";
350+
}
351+
} else {
352+
echo "=== Cache Statistics ===\n";
353+
echo "CacheManager is null - cache is not initialized.\n";
354+
echo "Check PDODB_CACHE_ENABLED in .env file.\n";
355+
}
356+
}
357+
358+
echo "\nNow run 'vendor/bin/pdodb ui' to view cache statistics in the dashboard!\n";
359+
echo "Navigate to Cache Statistics pane (key 3) to see real-time cache stats.\n";
360+

0 commit comments

Comments
 (0)