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..b7fc034 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;
@@ -99,15 +114,16 @@ public function setFiles(string $current, int $limit = 20)
$time = sprintf('%.6F', $time[1] ?? 0);
// Debugbar files shown in History Collector
+ $dt = date_create_from_format('U.u', $time);
$files[] = [
'time' => $time,
- 'datetime' => DateTime::createFromFormat('U.u', $time)->format('Y-m-d H:i:s.u'),
+ 'datetime' => $dt !== false ? $dt->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 +162,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