From 75a8921aad1edbcc1f4e22e79c723db917475dc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 14:33:09 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20audit=20&=20dry=20run=20=E2=80=94=20?= =?UTF-8?q?hapus=20semua=20dependensi=20CodeIgniter4,=20perbaiki=2017=20bu?= =?UTF-8?q?g=20kritis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug yang diperbaiki: - DebugToolbarMiddleware::inject() memanggil render() yang tidak ada → gunakan prepare() - Config::display() menerima 0 arg tapi dipanggil dengan arg → tambah param opsional - Config extends BaseCollector menyebabkan static method conflict → hapus extends - History: constructor tanpa param tapi dipanggil new History($config) → tambah constructor - History::setFiles() menggunakan WRITEPATH (CI4 constant) → gunakan historyPath dari config - Database.php: seluruh file masih CI4 (Query, config(), is_cli(), \Config\Database, Time) → tulis ulang menggunakan DatabaseAdapterInterface - Timers.php: service('timer') CI4 → static start()/stop() agnostik - Events.php: \CodeIgniter\Events\Events → static trigger() agnostik - Logs.php: service('logger') CI4 → static addLog() agnostik - Views.php: service('renderer') CI4 → static logView() agnostik - Files.php: clean_path() CI4 → normalizePath() + isCoreFile() native PHP - Routes.php: service('routes/router') + DefinedRouteCollector CI4 → RouterInterface - BaseCollector::cleanPath() memanggil clean_path() CI4 → str_replace native - toolbar.tpl.php: use CodeIgniter\..., site_url(), $CI_VERSION, $parser → dihapus/diganti - Semua .tpl: CI4 parser syntax ({key}, {loop}...{/loop}) → plain PHP foreach/if - phpunit.dist.xml: bootstrap CI4 system/Test/bootstrap.php → vendor/autoload.php - AdodbDatabaseAdapter::logQuery() tambah parameter $startTime untuk data timeline - Database::reset() dibuat static agar dapat dipanggil dari tearDown test Ditambahkan: 3 test file (42 tests, 2595 assertions — semua lulus) https://claude.ai/code/session_01QevTXiGqzvPkwfj8YeEmB8 --- phpunit.dist.xml | 50 +----- src/Adapters/AdodbDatabaseAdapter.php | 18 +- src/Collectors/BaseCollector.php | 6 +- src/Collectors/Config.php | 6 +- src/Collectors/Database.php | 199 +++++++--------------- src/Collectors/Events.php | 65 +++++-- src/Collectors/Files.php | 23 ++- src/Collectors/History.php | 33 +++- src/Collectors/Logs.php | 46 ++--- src/Collectors/Routes.php | 123 ++++++------- src/Collectors/Timers.php | 57 ++++++- src/Collectors/Views.php | 83 +++++---- src/Middleware/DebugToolbarMiddleware.php | 14 +- views/_config.tpl | 50 +++--- views/_database.tpl | 28 +-- views/_events.tpl | 13 +- views/_files.tpl | 22 ++- views/_history.tpl | 44 ++--- views/_logs.tpl | 21 ++- views/_routes.tpl | 47 +++-- views/toolbar.tpl.php | 153 ++++++----------- 21 files changed, 534 insertions(+), 567 deletions(-) diff --git a/phpunit.dist.xml b/phpunit.dist.xml index 1de961c..9d9f0f9 100644 --- a/phpunit.dist.xml +++ b/phpunit.dist.xml @@ -2,64 +2,22 @@ - - - - - - - - - - - - - - - - - tests/system + + tests - system + src - - system/Commands/Generators/Views - system/Debug/Toolbar/Views - system/Pager/Views - system/ThirdParty - system/Validation/Views - system/bootstrap.php - system/ComposerScripts.php - system/Config/Routes.php - system/Test/bootstrap.php - system/Test/ControllerTester.php - system/Test/FeatureTestCase.php - - - - - - - - - - - - diff --git a/src/Adapters/AdodbDatabaseAdapter.php b/src/Adapters/AdodbDatabaseAdapter.php index 9f9a635..a10b652 100644 --- a/src/Adapters/AdodbDatabaseAdapter.php +++ b/src/Adapters/AdodbDatabaseAdapter.php @@ -57,19 +57,21 @@ class AdodbDatabaseAdapter implements DatabaseAdapterInterface * Catat satu query ke dalam log. * Dipanggil dari kode aplikasi saat query dieksekusi. * - * @param string $sql Query SQL yang dieksekusi - * @param float $duration Durasi dalam milidetik - * @param array $params Bind parameter (opsional) + * @param string $sql Query SQL yang dieksekusi + * @param float $duration Durasi dalam milidetik + * @param array $params Bind parameter (opsional) + * @param float|null $startTime microtime(true) saat query dimulai (untuk timeline) */ - public static function logQuery(string $sql, float $duration, array $params = []): void + public static function logQuery(string $sql, float $duration, array $params = [], ?float $startTime = null): void { $sql = trim($sql); self::$queries[] = [ - 'sql' => $sql, - 'duration' => round($duration, 4), - 'params' => $params, - 'trace' => self::buildShortTrace(), + 'sql' => $sql, + 'duration' => round($duration, 4), + 'params' => $params, + 'trace' => self::buildShortTrace(), + 'startTime' => $startTime ?? (microtime(true) - $duration / 1000.0), ]; self::$totalTime += $duration; diff --git a/src/Collectors/BaseCollector.php b/src/Collectors/BaseCollector.php index 53cec33..ad7f117 100644 --- a/src/Collectors/BaseCollector.php +++ b/src/Collectors/BaseCollector.php @@ -186,13 +186,13 @@ public function display() } /** - * This makes nicer looking paths for the error output. + * Normalize a file path for display (forward slashes, no trailing sep). * - * @deprecated Use the dedicated `clean_path()` function. + * @deprecated No longer needed — paths are normalized per-collector. */ public function cleanPath(string $file): string { - return clean_path($file); + return str_replace('\\', '/', $file); } /** diff --git a/src/Collectors/Config.php b/src/Collectors/Config.php index 4592f2f..d1d7a6d 100644 --- a/src/Collectors/Config.php +++ b/src/Collectors/Config.php @@ -27,18 +27,20 @@ * Adapted from CodeIgniter 4 to be framework-agnostic. * Returns basic PHP and environment information. */ -class Config extends BaseCollector +class Config { /** * Return toolbar config values as an array. */ - public static function display(): array + public static function display(array $config = []): array { return [ 'phpVersion' => PHP_VERSION, 'phpSAPI' => PHP_SAPI, 'timezone' => date_default_timezone_get(), 'serverOS' => PHP_OS, + 'baseURL' => $config['baseURL'] ?? '', + 'environment' => $config['environment'] ?? '', ]; } } diff --git a/src/Collectors/Database.php b/src/Collectors/Database.php index 8e12135..438608e 100644 --- a/src/Collectors/Database.php +++ b/src/Collectors/Database.php @@ -27,7 +27,11 @@ * Collector for the Database tab of the Debug Toolbar. * * Adapted from CodeIgniter 4 to be framework-agnostic. - * Requires a DatabaseAdapterInterface implementation (e.g., AdodbDatabaseAdapter). + * Use setAdapter() to register a DatabaseAdapterInterface implementation + * (e.g., AdodbDatabaseAdapter) before the toolbar is rendered. + * + * Example: + * Database::setAdapter(new AdodbDatabaseAdapter()); */ class Database extends BaseCollector { @@ -45,13 +49,6 @@ class Database extends BaseCollector */ protected $hasTabContent = true; - /** - * Whether this collector has data for the Vars tab. - * - * @var bool - */ - protected $hasVarData = false; - /** * The name used to reference this collector in the toolbar. * @@ -60,89 +57,40 @@ class Database extends BaseCollector protected $title = 'Database'; /** - * Array of database connections. - * - * @var array - */ - protected $connections; - - /** - * The query instances that have been collected - * through the DBQuery Event. - * - * @var array - */ - protected static $queries = []; - - /** - * Constructor + * Registered database adapter, shared across all instances. */ - public function __construct() - { - $this->getConnections(); - } + private static ?DatabaseAdapterInterface $adapter = null; /** - * The static method used during Events to collect - * data. - * - * @internal - * - * @return void + * Register the database adapter. + * Call this once at bootstrap before the toolbar is rendered. */ - public static function collect(Query $query) + public static function setAdapter(DatabaseAdapterInterface $adapter): void { - $config = config(Toolbar::class); - - // Provide default in case it's not set - $max = $config->maxQueries ?: 100; - - if (count(static::$queries) < $max) { - $queryString = $query->getQuery(); - - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - - if (! is_cli()) { - // when called in the browser, the first two trace arrays - // are from the DB event trigger, which are unneeded - $backtrace = array_slice($backtrace, 2); - } - - static::$queries[] = [ - 'query' => $query, - 'string' => $queryString, - 'duplicate' => in_array($queryString, array_column(static::$queries, 'string'), true), - 'trace' => $backtrace, - ]; - } + self::$adapter = $adapter; } /** * Returns timeline data formatted for the toolbar. - * - * @return array The formatted data or an empty array. */ protected function formatTimelineData(): array { + if (self::$adapter === null) { + return []; + } + $data = []; - foreach ($this->connections as $alias => $connection) { - // Connection Time - $data[] = [ - 'name' => 'Connecting to Database: "' . $alias . '"', - 'component' => 'Database', - 'start' => $connection->getConnectStart(), - 'duration' => $connection->getConnectDuration(), - ]; - } + foreach (self::$adapter->getQueries() as $query) { + $startTime = (float) ($query['startTime'] ?? 0); + $duration = ((float) ($query['duration'] ?? 0)) / 1000; // ms → seconds - foreach (static::$queries as $query) { $data[] = [ 'name' => 'Query', 'component' => 'Database', - 'start' => $query['query']->getStartTime(true), - 'duration' => $query['query']->getDuration(), - 'query' => $query['query']->debugToolbarDisplay(), + 'start' => $startTime, + 'duration' => $duration, + 'query' => htmlspecialchars($query['sql'] ?? '', ENT_QUOTES, 'UTF-8'), ]; } @@ -150,58 +98,38 @@ protected function formatTimelineData(): array } /** - * Returns the data of this collector to be formatted in the toolbar + * Returns the data of this collector to be formatted in the toolbar. */ public function display(): array { - return ['queries' => array_map(static function (array $query): array { - $isDuplicate = $query['duplicate'] === true; - - $firstNonSystemLine = ''; - - foreach ($query['trace'] as $index => &$line) { - // simplify file and line - if (isset($line['file'])) { - $line['file'] = clean_path($line['file']) . ':' . $line['line']; - unset($line['line']); - } else { - $line['file'] = '[internal function]'; - } - - // find the first trace line that does not originate from `system/` - if ($firstNonSystemLine === '' && ! str_contains($line['file'], 'SYSTEMPATH')) { - $firstNonSystemLine = $line['file']; - } - - // simplify function call - if (isset($line['class'])) { - $line['function'] = $line['class'] . $line['type'] . $line['function']; - unset($line['class'], $line['type']); - } - - if (strrpos($line['function'], '{closure}') === false) { - $line['function'] .= '()'; - } + if (self::$adapter === null) { + return ['queries' => []]; + } - $line['function'] = str_repeat(chr(0xC2) . chr(0xA0), 8) . $line['function']; + $rawQueries = self::$adapter->getQueries(); + $sqlCounts = array_count_values(array_column($rawQueries, 'sql')); - // add index numbering padded with nonbreaking space - $indexPadded = str_pad(sprintf('%d', $index + 1), 3, ' ', STR_PAD_LEFT); - $indexPadded = preg_replace('/\s/', chr(0xC2) . chr(0xA0), $indexPadded); + $queries = []; + $idx = 0; - $line['index'] = $indexPadded . str_repeat(chr(0xC2) . chr(0xA0), 4); - } + foreach ($rawQueries as $query) { + $sql = $query['sql'] ?? ''; + $isDuplicate = ($sqlCounts[$sql] ?? 1) > 1; - return [ + $queries[] = [ 'hover' => $isDuplicate ? 'This query was called more than once.' : '', 'class' => $isDuplicate ? 'duplicate' : '', - 'duration' => ((float) $query['query']->getDuration(5) * 1000) . ' ms', - 'sql' => $query['query']->debugToolbarDisplay(), - 'trace' => $query['trace'], - 'trace-file' => $firstNonSystemLine, - 'qid' => md5($query['query'] . Time::now()->format('0.u00 U')), + 'duration' => number_format((float) ($query['duration'] ?? 0), 2) . ' ms', + 'sql' => htmlspecialchars($sql, ENT_QUOTES, 'UTF-8'), + 'trace' => $query['trace'] ?? '', + 'trace-file' => $query['trace'] ?? '', + 'qid' => md5($sql . $idx), ]; - }, static::$queries)]; + + $idx++; + } + + return ['queries' => $queries]; } /** @@ -209,30 +137,26 @@ public function display(): array */ public function getBadgeValue(): int { - return count(static::$queries); + return self::$adapter !== null ? self::$adapter->getQueryCount() : 0; } /** * Information to be displayed next to the title. - * - * @return string The number of queries (in parentheses) or an empty string. */ public function getTitleDetails(): string { - $this->getConnections(); + if (self::$adapter === null) { + return ''; + } - $queryCount = count(static::$queries); - $uniqueCount = count(array_filter(static::$queries, static fn ($query): bool => $query['duplicate'] === false)); - $connectionCount = count($this->connections); + $total = self::$adapter->getQueryCount(); + $duplicates = count(self::$adapter->getDuplicates()); return sprintf( - '(%d total Quer%s, %d %s unique across %d Connection%s)', - $queryCount, - $queryCount > 1 ? 'ies' : 'y', - $uniqueCount, - $uniqueCount > 1 ? 'of them' : '', - $connectionCount, - $connectionCount > 1 ? 's' : '', + '(%d total Quer%s, %d duplicate)', + $total, + $total === 1 ? 'y' : 'ies', + $duplicates, ); } @@ -241,7 +165,7 @@ public function getTitleDetails(): string */ public function isEmpty(): bool { - return static::$queries === []; + return self::$adapter === null || self::$adapter->getQueryCount() === 0; } /** @@ -255,19 +179,10 @@ public function icon(): string } /** - * Gets the connections from the database config - */ - private function getConnections(): void - { - $this->connections = \Config\Database::getConnections(); - } - - /** - * Reset collector state for worker mode. - * Clears collected queries between requests. + * Reset collector state. */ - public function reset(): void + public static function reset(): void { - static::$queries = []; + self::$adapter = null; } } diff --git a/src/Collectors/Events.php b/src/Collectors/Events.php index c683d2b..aa55a9c 100644 --- a/src/Collectors/Events.php +++ b/src/Collectors/Events.php @@ -25,6 +25,11 @@ * Events collector * * Adapted from CodeIgniter 4 to be framework-agnostic. + * + * Usage: + * $start = microtime(true); + * // ... event handler runs ... + * Events::trigger('my_event', $start, microtime(true)); */ class Events extends BaseCollector { @@ -60,6 +65,26 @@ class Events extends BaseCollector */ protected $title = 'Events'; + /** + * @var list + */ + private static array $logs = []; + + /** + * Log a triggered event. + * + * @param float $start microtime(true) before the event handler ran + * @param float $end microtime(true) after the event handler returned + */ + public static function trigger(string $event, float $start, float $end): void + { + self::$logs[] = [ + 'event' => $event, + 'start' => $start, + 'end' => $end, + ]; + } + /** * Child classes should implement this to return the timeline data * formatted for correct usage. @@ -68,9 +93,7 @@ protected function formatTimelineData(): array { $data = []; - $rows = \CodeIgniter\Events\Events::getPerformanceLogs(); - - foreach ($rows as $info) { + foreach (self::$logs as $info) { $data[] = [ 'name' => 'Event: ' . $info['event'], 'component' => 'Events', @@ -87,15 +110,13 @@ protected function formatTimelineData(): array */ public function display(): array { - $data = [ - 'events' => [], - ]; + $events = []; - foreach (\CodeIgniter\Events\Events::getPerformanceLogs() as $row) { + foreach (self::$logs as $row) { $key = $row['event']; - if (! array_key_exists($key, $data['events'])) { - $data['events'][$key] = [ + if (! array_key_exists($key, $events)) { + $events[$key] = [ 'event' => $key, 'duration' => ($row['end'] - $row['start']) * 1000, 'count' => 1, @@ -104,15 +125,15 @@ public function display(): array continue; } - $data['events'][$key]['duration'] += ($row['end'] - $row['start']) * 1000; - $data['events'][$key]['count']++; + $events[$key]['duration'] += ($row['end'] - $row['start']) * 1000; + $events[$key]['count']++; } - foreach ($data['events'] as &$row) { + foreach ($events as &$row) { $row['duration'] = number_format($row['duration'], 2); } - return $data; + return ['events' => array_values($events)]; } /** @@ -120,7 +141,15 @@ public function display(): array */ public function getBadgeValue(): int { - return count(\CodeIgniter\Events\Events::getPerformanceLogs()); + return count(self::$logs); + } + + /** + * Does this collector have any data collected? + */ + public function isEmpty(): bool + { + return self::$logs === []; } /** @@ -132,4 +161,12 @@ public function icon(): string { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEASURBVEhL7ZXNDcIwDIVTsRBH1uDQDdquUA6IM1xgCA6MwJUN2hk6AQzAz0vl0ETUxC5VT3zSU5w81/mRMGZysixbFEVR0jSKNt8geQU9aRpFmp/keX6AbjZ5oB74vsaN5lSzA4tLSjpBFxsjeSuRy4d2mDdQTWU7YLbXTNN05mKyovj5KL6B7q3hoy3KwdZxBlT+Ipz+jPHrBqOIynZgcZonoukb/0ckiTHqNvDXtXEAaygRbaB9FvUTjRUHsIYS0QaSp+Dw6wT4hiTmYHOcYZsdLQ2CbXa4ftuuYR4x9vYZgdb4vsFYUdmABMYeukK9/SUme3KMFQ77+Yfzh8eYF8+orDuDWU5LAAAAAElFTkSuQmCC'; } + + /** + * Reset all logged events. + */ + public static function reset(): void + { + self::$logs = []; + } } diff --git a/src/Collectors/Files.php b/src/Collectors/Files.php index 89e3df3..c2f2321 100644 --- a/src/Collectors/Files.php +++ b/src/Collectors/Files.php @@ -25,6 +25,8 @@ * Files collector * * Adapted from CodeIgniter 4 to be framework-agnostic. + * Lists all PHP files loaded during this request, separated into + * "vendor / core" files and "user application" files. */ class Files extends BaseCollector { @@ -70,9 +72,9 @@ public function display(): array $userFiles = []; foreach ($rawFiles as $file) { - $path = clean_path($file); + $path = $this->normalizePath($file); - if (str_contains($path, 'SYSTEMPATH')) { + if ($this->isCoreFile($path)) { $coreFiles[] = [ 'path' => $path, 'name' => basename($file), @@ -111,4 +113,21 @@ public function icon(): string { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVEhL7ZQ9S8NQGIVTBQUncfMfCO4uLgoKbuKQOWg+OkXERRE1IAXrIHbVDrqIDuLiJgj+gro7S3dnpfq88b1FMTE3VZx64HBzzvvZWxKnj15QCcPwCD5HUfSWR+JtzgmtsUcQBEva5IIm9SwSu+95CAWbUuy67qBa32ByZEDpIaZYZSZMjjQuPcQUq8yEyYEb8FSerYeQVGbAFzJkX1PyQWLhgCz0BxTCekC1Wp0hsa6yokzhed4oje6Iz6rlJEkyIKfUEFtITVtQdAibn5rMyaYsMS+a5wTv8qeXMhcU16QZbKgl3hbs+L4/pnpdc87MElZgq10p5DxGdq8I7xrvUWUKvG3NbSK7ubngYzdJwSsF7TiOh9VOgfcEz1UayNe3JUPM1RWC5GXYgTfc75B4NBmXJnAtTfpABX0iPvEd9ezALwkplCFXcr9styiNOKc1RRZpaPM9tcqBwlWzGY1qPL9wjqRBgF5BH6j8HWh2S7MHlX8PrmbK+k/8PzjOOzx1D3i1pKTTAAAAAElFTkSuQmCC'; } + + /** + * Convert backslashes to forward slashes for uniform display. + */ + private function normalizePath(string $file): string + { + return str_replace('\\', '/', $file); + } + + /** + * Heuristic: files inside a vendor/ directory are "core/library" files; + * everything else is considered a user application file. + */ + private function isCoreFile(string $normalizedPath): bool + { + return str_contains($normalizedPath, '/vendor/'); + } } diff --git a/src/Collectors/History.php b/src/Collectors/History.php index 83a5713..26aed14 100644 --- a/src/Collectors/History.php +++ b/src/Collectors/History.php @@ -65,6 +65,21 @@ class History extends BaseCollector */ protected $files = []; + /** + * Path to directory containing debugbar JSON history files. + * + * @var string + */ + private string $historyPath; + + public function __construct(array $config = []) + { + $this->historyPath = rtrim( + $config['historyPath'] ?? sys_get_temp_dir() . '/wizdam-debugbar/', + '/\\', + ) . DIRECTORY_SEPARATOR; + } + /** * Specify time limit & file count for debug history. * @@ -75,7 +90,7 @@ class History extends BaseCollector */ public function setFiles(string $current, int $limit = 20) { - $filenames = glob(WRITEPATH . 'debugbar/debugbar_*.json'); + $filenames = glob($this->historyPath . 'debugbar_*.json') ?: []; $files = []; $counter = 0; @@ -101,13 +116,15 @@ public function setFiles(string $current, int $limit = 20) // Debugbar files shown in History Collector $files[] = [ 'time' => $time, - 'datetime' => DateTime::createFromFormat('U.u', $time)->format('Y-m-d H:i:s.u'), + 'datetime' => \DateTime::createFromFormat('U.u', $time) !== false + ? \DateTime::createFromFormat('U.u', $time)->format('Y-m-d H:i:s.u') + : '', 'active' => $time === $current, - 'status' => $contents->vars->response->statusCode, - 'method' => $contents->method, - 'url' => $contents->url, - 'isAJAX' => $contents->isAJAX ? 'Yes' : 'No', - 'contentType' => $contents->vars->response->contentType, + 'status' => $contents->vars->response->statusCode ?? 0, + 'method' => $contents->method ?? '', + 'url' => $contents->url ?? '', + 'isAJAX' => !empty($contents->isAJAX) ? 'Yes' : 'No', + 'contentType' => $contents->vars->response->contentType ?? '', ]; } } @@ -146,6 +163,6 @@ public function isEmpty(): bool */ public function icon(): string { - return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJySURBVEhL3ZU7aJNhGIVTpV6i4qCIgkIHxcXLErS4FBwUFNwiCKGhuTYJGaIgnRoo4qRu6iCiiIuIXXTTIkIpuqoFwaGgonUQlC5KafU5ycmNP0lTdPLA4fu+8573/a4/f6hXpFKpwUwmc9fDfweKbk+n07fgEv33TLSbtt/hvwNFT1PsG/zdTE0Gp+GFfD6/2fbVIxqNrqPIRbjg4t/hY8aztcngfDabHXbKyiiXy2vcrcPH8oDCry2FKDrA+Ar6L01E/ypyXzXaARjDGGcoeNxSDZXE0dHRA5VRE5LJ5CFy5jzJuOX2wHRHRnjbklZ6isQ3tIctBaAd4vlK3jLtkOVWqABBXd47jGHLmjTmSScttQV5J+SjfcUweFQEbsjAas5aqoCLXutJl7vtQsAzpRowYqkBinyCC8Vicb2lOih8zoldd0F8RD7qTFiqAnGrAy8stUAvi/hbqDM+YzkAFrLPdR5ZqoLXsd+Bh5YCIH7JniVdquUWxOPxDfboHhrI5XJ7HHhiqQXox+APe/Qk64+gGYVCYZs8cMpSFQj9JOoFzVqqo7k4HIvFYpscCoAjOmLffUsNUGRaQUwDlmofUa34ecsdgXdcXo4wbakBgiUFafXJV8A4DJ/2UrxUKm3E95H8RbjLcgOJRGILhnmCP+FBy5XvwN2uIPcy1AJvWgqC4xm2aU4Xb3lF4I+Tpyf8hRe5w3J7YLymSeA8Z3nSclv4WLRyFdfOjzrUFX0klJUEtZtntCNc+F69cz/FiDzEPtjzmcUMOr83kDQEX6pAJxJfpL3OX22n01YN7SZCoQnaSdoZ+Jz+PZihH3wt/xlCoT9M6nEtmRSPCQAAAABJRU5ErkJggg=='; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJySURBVEhL3ZU7aJNhGIVTpV6i4qCIgkIHxcXLErS4FBwUFNwiCKGhuTYJGaIgnRoo4qRu6iCiiIuIXXTTIkIpuqoFwaGgonUQlC5KafU5ycmNP0lTdPLA4fs+c973va4/f6hXpFKpwUwmc9fDfweKbk+n07fgEv33TLSbtt/hvwNFT1PsG/zdTE0Gp+GFfD6/2fbVIxqNrqPIRbjg4t/hY8aztcngfDabHTrKyiiXy2vcrcPH8oDCry2FKDrA+Ar6L01E/ypyXzXaARjDGGcoeNxSDZXE0dHRA5VRE5LJ5CFy5jzJuOX2wHRHRnjbklZ6isQ3tIctBaAd4vlK3jLtkOVWqABBXd47jGHLmjTmSScttQV5J+SjfcUweFQEbsjAas5aqoCLXutJl7vtQsAzpRowYqkBinyCC8Vicb2lOih8zoldd0F8RD7qTFiqAnGrAy8stUAvi/hbqDM+YzkAFrLPdR5ZqoLXsd+Bh5YCIH7JniVdquUWxOPxDfboHhrI5XJ7HHhiqQXox+APe/Qk64+gGYVCYZs8cMpSFQj9JOoFzVqqo7k4HIvFYpscCoAjOmLffUsNUGRaQUwDlmofUa34ecsdgXdcXo4wbakBgiUFafXJV8A4DJ/2UrxUKm3E95H8RbjLcgOJRGILhnmCP+FBy5XvwN2uIPcy1AJvWgqC4xm2aU4Xb3lF4I+Tpyf8hRe5w3J7YLymSeA8Z3nSclv4WLRyFdfOjzrUFX0klJUEtZtntCNc+F69cz/FiDzEPtjzmcUMOr83kDQEX6pAJxJfpL3OX22n01YN7SZCoQnaSdoZ+Jz+PZihH3wt/xlCoT9M6nEtmRSPCQAAAABJRU5ErkJggg=='; } } diff --git a/src/Collectors/Logs.php b/src/Collectors/Logs.php index 4a89de7..f457add 100644 --- a/src/Collectors/Logs.php +++ b/src/Collectors/Logs.php @@ -25,6 +25,10 @@ * Logs collector * * Adapted from CodeIgniter 4 to be framework-agnostic. + * + * Usage: + * Logs::addLog('error', 'Something went wrong'); + * Logs::addLog('info', 'User logged in'); */ class Logs extends BaseCollector { @@ -53,11 +57,23 @@ class Logs extends BaseCollector protected $title = 'Logs'; /** - * Our collected data. - * * @var list */ - protected $data = []; + private static array $logCache = []; + + /** + * Add a log entry. + * + * @param string $level PSR-3 log level: emergency, alert, critical, error, warning, notice, info, debug + * @param string $msg Log message + */ + public static function addLog(string $level, string $msg): void + { + self::$logCache[] = [ + 'level' => $level, + 'msg' => $msg, + ]; + } /** * Returns the data of this collector to be formatted in the toolbar. @@ -66,9 +82,7 @@ class Logs extends BaseCollector */ public function display(): array { - return [ - 'logs' => $this->collectLogs(), - ]; + return ['logs' => self::$logCache]; } /** @@ -76,9 +90,7 @@ public function display(): array */ public function isEmpty(): bool { - $this->collectLogs(); - - return $this->data === []; + return self::$logCache === []; } /** @@ -92,20 +104,10 @@ public function icon(): string } /** - * Ensures the data has been collected. - * - * @return list + * Reset all log entries. */ - protected function collectLogs() + public static function reset(): void { - if ($this->data !== []) { - return $this->data; - } - - $cache = service('logger')->logCache; - - $this->data = $cache ?? []; - - return $this->data; + self::$logCache = []; } } diff --git a/src/Collectors/Routes.php b/src/Collectors/Routes.php index 0e23333..3184324 100644 --- a/src/Collectors/Routes.php +++ b/src/Collectors/Routes.php @@ -27,7 +27,9 @@ * Routes collector * * Adapted from CodeIgniter 4 to be framework-agnostic. - * Requires a RouterInterface implementation (e.g., WizdamRouterAdapter). + * + * Usage: + * Routes::setRouter(new WizdamRouterAdapter()); */ class Routes extends BaseCollector { @@ -56,7 +58,21 @@ class Routes extends BaseCollector protected $title = 'Routes'; /** - * Returns the data of this collector to be formatted in the toolbar + * Registered router adapter, shared across all instances. + */ + private static ?RouterInterface $router = null; + + /** + * Register the router adapter. + * Call this once at bootstrap before the toolbar is rendered. + */ + public static function setRouter(RouterInterface $router): void + { + self::$router = $router; + } + + /** + * Returns the data of this collector to be formatted in the toolbar. * * @return array{ * matchedRoute: list + * params: list * }>, - * routes: list + * routes: list * } - * - * @throws ReflectionException */ public function display(): array { - $rawRoutes = service('routes', true); - $router = service('router', null, null, true); - - // Get our parameters - // Closure routes - if (is_callable($router->controllerName())) { - $method = new ReflectionFunction($router->controllerName()); - } else { - try { - $method = new ReflectionMethod($router->controllerName(), $router->methodName()); - } catch (ReflectionException) { - try { - // If we're here, the method doesn't exist - // and is likely calculated in _remap. - $method = new ReflectionMethod($router->controllerName(), '_remap'); - } catch (ReflectionException) { - // If we're here, page cache is returned. The router is not executed. - return [ - 'matchedRoute' => [], - 'routes' => [], - ]; - } - } + if (self::$router === null) { + return [ + 'matchedRoute' => [], + 'routes' => [], + ]; } - $rawParams = $method->getParameters(); - - $params = []; + $rawParams = self::$router->getParams(); + $params = []; - foreach ($rawParams as $key => $param) { + foreach ($rawParams as $name => $value) { $params[] = [ - 'name' => '$' . $param->getName() . ' = ', - 'value' => $router->params()[$key] ?? - ' | default: ' - . var_export( - $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, - true, - ), + 'name' => (string) $name, + 'value' => is_scalar($value) ? (string) $value : print_r($value, true), ]; } $matchedRoute = [ [ - 'directory' => $router->directory(), - 'controller' => $router->controllerName(), - 'method' => $router->methodName(), - 'paramCount' => count($router->params()), + 'directory' => '', + 'controller' => self::$router->getController(), + 'method' => self::$router->getMethod(), + 'paramCount' => count($rawParams), 'truePCount' => count($params), 'params' => $params, ], ]; - // Defined Routes - $routes = []; - - $definedRouteCollector = new DefinedRouteCollector($rawRoutes); - - foreach ($definedRouteCollector->collect() as $route) { - // filter for strings, as callbacks aren't displayable - if ($route['handler'] !== '(Closure)') { - $routes[] = [ - 'method' => strtoupper($route['method']), - 'route' => $route['route'], - 'handler' => $route['handler'], - ]; - } - } - return [ 'matchedRoute' => $matchedRoute, - 'routes' => $routes, + 'routes' => [], ]; } /** - * Returns a count of all the routes in the system. + * Returns the number of matched route entries as the badge value. */ public function getBadgeValue(): int { - $rawRoutes = service('routes', true); + return self::$router !== null ? 1 : 0; + } - return count($rawRoutes->getRoutes()); + /** + * Does this collector have any data collected? + */ + public function isEmpty(): bool + { + return self::$router === null; } /** @@ -174,4 +147,12 @@ public function icon(): string { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg=='; } + + /** + * Reset the registered router. + */ + public static function reset(): void + { + self::$router = null; + } } diff --git a/src/Collectors/Timers.php b/src/Collectors/Timers.php index 820e526..fdb8db2 100644 --- a/src/Collectors/Timers.php +++ b/src/Collectors/Timers.php @@ -25,6 +25,11 @@ * Timers collector * * Adapted from CodeIgniter 4 to be framework-agnostic. + * + * Usage: + * Timers::start('my_block'); + * // ... code ... + * Timers::stop('my_block'); */ class Timers extends BaseCollector { @@ -52,6 +57,39 @@ class Timers extends BaseCollector */ protected $title = 'Timers'; + /** + * @var array + */ + private static array $timers = []; + + /** + * Start a named timer. + */ + public static function start(string $name): void + { + self::$timers[$name] = ['start' => microtime(true), 'end' => null]; + } + + /** + * Stop a named timer. + */ + public static function stop(string $name): void + { + if (isset(self::$timers[$name])) { + self::$timers[$name]['end'] = microtime(true); + } + } + + /** + * Returns all recorded timers (read-only). + * + * @return array + */ + public static function getTimers(): array + { + return self::$timers; + } + /** * Child classes should implement this to return the timeline data * formatted for correct usage. @@ -60,22 +98,29 @@ protected function formatTimelineData(): array { $data = []; - $benchmark = service('timer', true); - $rows = $benchmark->getTimers(6); - - foreach ($rows as $name => $info) { + foreach (self::$timers as $name => $timer) { if ($name === 'total_execution') { continue; } + $end = $timer['end'] ?? microtime(true); + $data[] = [ 'name' => ucwords(str_replace('_', ' ', $name)), 'component' => 'Timer', - 'start' => $info['start'], - 'duration' => $info['end'] - $info['start'], + 'start' => $timer['start'], + 'duration' => $end - $timer['start'], ]; } return $data; } + + /** + * Reset all timers (e.g. between requests in worker mode). + */ + public static function reset(): void + { + self::$timers = []; + } } diff --git a/src/Collectors/Views.php b/src/Collectors/Views.php index cefd99c..2b8c293 100644 --- a/src/Collectors/Views.php +++ b/src/Collectors/Views.php @@ -25,7 +25,11 @@ * Views collector * * Adapted from CodeIgniter 4 to be framework-agnostic. - * Works with any template engine via manual logging. + * + * Usage: + * $start = microtime(true); + * // ... render template ... + * Views::logView('article/view.tpl', $start, microtime(true), $templateData); */ class Views extends BaseCollector { @@ -70,22 +74,26 @@ class Views extends BaseCollector protected $title = 'Views'; /** - * Instance of the shared Renderer service - * - * @var RendererInterface|null + * @var list */ - protected $viewer; + private static array $renderedViews = []; /** - * Views counter + * Log a rendered view. * - * @var array + * @param string $view Template name / path + * @param float $start microtime(true) before rendering + * @param float $end microtime(true) after rendering + * @param array $data Variables passed to the template (optional) */ - protected $views = []; - - private function initViewer(): void + public static function logView(string $view, float $start, float $end, array $data = []): void { - $this->viewer ??= service('renderer'); + self::$renderedViews[] = [ + 'view' => $view, + 'start' => $start, + 'end' => $end, + 'data' => $data, + ]; } /** @@ -94,13 +102,9 @@ private function initViewer(): void */ protected function formatTimelineData(): array { - $this->initViewer(); - $data = []; - $rows = $this->viewer->getPerformanceData(); - - foreach ($rows as $info) { + foreach (self::$renderedViews as $info) { $data[] = [ 'name' => 'View: ' . $info['view'], 'component' => 'Views', @@ -114,37 +118,34 @@ protected function formatTimelineData(): array /** * Gets a collection of data that should be shown in the 'Vars' tab. - * The format is an array of sections, each with their own array - * of key/value pairs: - * - * $data = [ - * 'section 1' => [ - * 'foo' => 'bar, - * 'bar' => 'baz' - * ], - * 'section 2' => [ - * 'foo' => 'bar, - * 'bar' => 'baz' - * ], - * ]; */ public function getVarData(): array { - $this->initViewer(); + $merged = []; - return [ - 'View Data' => $this->viewer->getData(), - ]; + foreach (self::$renderedViews as $info) { + foreach ($info['data'] as $key => $value) { + $merged[(string) $key] = $value; + } + } + + return ['View Data' => $merged]; } /** - * Returns a count of all views. + * Returns a count of all views rendered. */ public function getBadgeValue(): int { - $this->initViewer(); + return count(self::$renderedViews); + } - return count($this->viewer->getPerformanceData()); + /** + * Does this collector have any data collected? + */ + public function isEmpty(): bool + { + return self::$renderedViews === []; } /** @@ -154,6 +155,14 @@ public function getBadgeValue(): int */ public function icon(): string { - return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII='; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJURA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII='; + } + + /** + * Reset all logged views. + */ + public static function reset(): void + { + self::$renderedViews = []; } } diff --git a/src/Middleware/DebugToolbarMiddleware.php b/src/Middleware/DebugToolbarMiddleware.php index d7f1b8c..e892428 100644 --- a/src/Middleware/DebugToolbarMiddleware.php +++ b/src/Middleware/DebugToolbarMiddleware.php @@ -108,19 +108,13 @@ public function process(array $request, callable $handler): string // --------------------------------------------------------------- /** - * Inject toolbar HTML ke dalam response sebelum tag . + * Inject toolbar script loader ke dalam response. + * Menggunakan DebugToolbar::prepare() yang memodifikasi response by reference. */ private function inject(string $response): string { - $toolbarHtml = $this->debugBar->render(); - - // Inject tepat sebelum agar tidak mengganggu layout - if (stripos($response, '') !== false) { - return str_ireplace('', $toolbarHtml . '', $response); - } - - // Fallback: tambahkan di akhir jika tidak ada - return $response . $toolbarHtml; + $this->debugBar->prepare($response); + return $response; } /** diff --git a/views/_config.tpl b/views/_config.tpl index e3235ec..bb2d21b 100644 --- a/views/_config.tpl +++ b/views/_config.tpl @@ -1,48 +1,48 @@ -

- Read the CodeIgniter docs... -

- + - - + + - + - + + + + + - + - - - - - - - - - +
CodeIgniter Version:{ ciVersion }WizdamDebugToolbar Version:
PHP Version:{ phpVersion }
PHP SAPI:{ phpSAPI }
Server OS:
Environment:{ environment }
Base URL: - { if $baseURL == '' } -
- The $baseURL should always be set manually to prevent possible URL personification from external parties. -
- { else } - { baseURL } - { endif } + + baseURL not configured. + + +
Timezone:{ timezone }
Locale:{ locale }
Content Security Policy Enabled:{ if $cspEnabled } Yes { else } No { endif }
diff --git a/views/_database.tpl b/views/_database.tpl index 054dd36..4515eb2 100644 --- a/views/_database.tpl +++ b/views/_database.tpl @@ -1,26 +1,32 @@ + + - {queries} - - - - + + + + + - + - {/queries} +
Time Query StringCaller
{duration}{! sql !}{trace-file}
+ +
- {trace} - {index}{file}
- {function}

- {/trace} +
diff --git a/views/_events.tpl b/views/_events.tpl index 88d732f..b472267 100644 --- a/views/_events.tpl +++ b/views/_events.tpl @@ -1,3 +1,6 @@ + @@ -7,12 +10,12 @@ - {events} + - - - + + + - {/events} +
{ duration } ms{event}{count} ms
diff --git a/views/_files.tpl b/views/_files.tpl index 9c992ab..339f3a4 100644 --- a/views/_files.tpl +++ b/views/_files.tpl @@ -1,16 +1,22 @@ + - {userFiles} + - - + + - {/userFiles} - {coreFiles} + + - - + + - {/coreFiles} +
{name}{path}
{name}{path}
diff --git a/views/_history.tpl b/views/_history.tpl index 7f22f56..ccaa712 100644 --- a/views/_history.tpl +++ b/views/_history.tpl @@ -1,28 +1,34 @@ + - - - - - - - - - + + + + + + + + + - {files} - + + + - - - - - - + + + + + - {/files} +
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
- + + + {datetime}{status}{method}{url}{contentType}{isAJAX}
diff --git a/views/_logs.tpl b/views/_logs.tpl index 7c80d84..ecf0d26 100644 --- a/views/_logs.tpl +++ b/views/_logs.tpl @@ -1,6 +1,11 @@ -{ if $logs == [] } -

Nothing was logged. If you were expecting logged items, ensure that LoggerConfig file has the correct threshold set.

-{ else } + + +

Nothing was logged. If you were expecting logged items, call + WizdamDebugToolbar\Collectors\Logs::addLog($level, $message) + from your application code.

+ @@ -9,12 +14,12 @@ - {logs} + - - + + - {/logs} +
{level}{msg}
-{ endif } + diff --git a/views/_routes.tpl b/views/_routes.tpl index e277046..bed70f1 100644 --- a/views/_routes.tpl +++ b/views/_routes.tpl @@ -1,35 +1,40 @@ +

Matched Route

- {matchedRoute} + - + - + - + - + - {params} + - - + + - {/params} - {/matchedRoute} + +
Directory:{directory}
Controller:{controller}
Method:{method}
Params:{paramCount} / {truePCount} /
{name}{value}
-

Defined Routes

@@ -41,12 +46,18 @@ - {routes} - - - - - - {/routes} + + + + + + + + + + +
{method}{route}{handler}
No defined routes registered.
+ +
diff --git a/views/toolbar.tpl.php b/views/toolbar.tpl.php index b86baba..bb9e44b 100644 --- a/views/toolbar.tpl.php +++ b/views/toolbar.tpl.php @@ -1,24 +1,21 @@ @@ -41,7 +38,7 @@ 🔅 - + @@ -78,14 +75,14 @@ - + - + @@ -103,21 +100,20 @@ - renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?> + renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?> - - -
-

- - setData($c['display'])->render("_{$c['titleSafe']}.tpl") ?> -
- + +
+

+ renderPartial($viewsPath, '_' . $c['titleSafe'] . '.tpl', $c['display']) ?> +
@@ -127,13 +123,10 @@ $items) : ?> -

- - $value) : ?> @@ -144,7 +137,6 @@
-

No data to display.

@@ -155,7 +147,6 @@

Session User Data

- @@ -178,100 +169,58 @@

Request ( )

- -

$_GET

-
- -
- - $value) : ?> - - - - - - -
+

$_GET

+ + $value) : ?> + + +
- -

$_POST

-
- - - - $value) : ?> - - - - - - -
+

$_POST

+ + $value) : ?> + + +
- -

Headers

-
- - - - $value) : ?> - - - - - - -
+

Headers

+ + $value) : ?> + + +
- -

Cookies

-
- - - - $value) : ?> - - - - - - - +

Cookies

+ + $value) : ?> + + +

Response ( )

- - -

Headers

-
- - - - $value) : ?> - - - - - - -
+

Headers

+ + $value) : ?> + + +

System Configuration

- - setData($config)->render('_config.tpl') ?> + renderPartial($viewsPath, '_config.tpl', $config) ?>