ForgeDebugbar

Development debugging toolbar for Forge Kernel. Provides automatic HTML injection, multiple data collectors, and a tabbed interface for debugging information.

Overview

ForgeDebugbar is a comprehensive debugging toolbar that automatically injects into HTML responses during development. It provides real-time insights into memory usage, execution time, database queries, routes, sessions, views, exceptions, and more.

Key Features

Automatic HTML injection (non-intrusive)
Multiple data collectors (memory, time, queries, routes, etc.)
Tabbed interface with collapsible panels
Only active in debug mode (APP_DEBUG)
Content-Type aware (only HTML responses)
Helper functions for custom data
Lifecycle hook integration
Extensible collector architecture

What ForgeDebugbar Provides

  • Automatic HTML Injection: Injects debug bar into HTML responses before the </body> tag without requiring manual integration
  • Memory Tracking: Monitors current memory usage, peak memory, and memory percentage of limit
  • Execution Time: Tracks total request execution time in milliseconds
  • Timeline Events: Custom timeline events for tracking request lifecycle
  • Debug Messages: Custom messages with labels (info, warning, error)
  • Exception Tracking: Captures exceptions with type, message, code, file, and stack trace
  • Request Information: Displays URL, method, IP, headers, query parameters, body, cookies, and files
  • Route Information: Shows current route URI, HTTP method, handler, and middleware stack
  • Session Data: Displays session ID and all session data
  • View Tracking: Lists all rendered views with their data
  • Database Queries: Tracks SQL queries with execution time and performance classification (if integrated)

Generic Module: ForgeDebugbar is a generic module (type: 'generic', order: 3), providing debugging functionality that can be installed as needed. It has no dependencies and automatically integrates with the request lifecycle.

Architecture & Design Philosophy

ForgeDebugbar uses a collector-based architecture that allows for extensible data collection. It integrates seamlessly with the Forge Kernel lifecycle hooks and automatically injects the debug bar into HTML responses.

Collector-Based Architecture

All debugging data is collected through collectors that implement the CollectorInterface. Collectors are registered with the DebugBar using callables, allowing for lazy evaluation of data.

interface CollectorInterface
{
    public static function collect(...$args): mixed;
}

Singleton Pattern

The DebugBar class uses a singleton pattern to ensure only one instance exists per request, maintaining consistent state across the application lifecycle.

Lifecycle Hook Integration

ForgeDebugbar integrates with Forge Kernel lifecycle hooks:

  • BEFORE_REQUEST: Initializes timeline tracking
  • AFTER_REQUEST: Collects all data and injects debug bar into response

Content-Type Awareness

The debug bar only injects into HTML responses. It checks the Content-Type header and skips:

  • Non-HTML content types
  • JSON responses (detects {"html":... pattern)
  • Responses without a </body> tag

Debug Mode Only

The debug bar is only active when both conditions are met:

  • APP_DEBUG environment variable is true
  • forge_debug_bar.enabled config is true

Installation

ForgeDebugbar can be installed via ForgePackageManager. It's a generic module with no dependencies.

Install via ForgePackageManager

php forge.php module:install ForgeDebugbar

Post-Install

After installation, the module automatically:

  • Links assets via asset:link command
  • Registers the DebugBar service in the container
  • Sets up lifecycle hooks for data collection

Uninstall

php forge.php module:uninstall ForgeDebugbar

On uninstall, the module automatically unlinks assets via asset:unlink command.

Collectors Overview

Collectors are the core of ForgeDebugbar's data collection system. Each collector implements the CollectorInterface and provides a static collect() method.

Collector Interface

interface CollectorInterface
{
    public static function collect(...$args): mixed;
}

Collector Registration

Collectors are registered with the DebugBar using callables, allowing for lazy evaluation:

$debugbar->addCollector('memory', function () {
    return MemoryCollector::collect();
});

Built-in Collectors

ForgeDebugbar includes the following built-in collectors:

  • MemoryCollector - Memory usage tracking
  • TimeCollector - Request execution time
  • TimelineCollector - Request lifecycle events
  • MessageCollector - Debug messages
  • ExceptionCollector - Exception tracking
  • RequestCollector - HTTP request information
  • RouteCollector - Current route information
  • SessionCollector - Session data
  • ViewCollector - Rendered views
  • DatabaseCollector - Database queries (available but requires integration)

Built-in Collectors

MemoryCollector

Tracks memory usage throughout the request lifecycle.

class MemoryCollector implements CollectorInterface
{
    public static function collect(...$args): array
    {
        return self::instance()->getMemoryUsage();
    }

    public function getMemoryUsage(): array
    {
        return [
            'current' => 'X.XX MB',      // Current memory usage
            'used' => 'X.XX MB',        // Memory used (delta)
            'peak' => 'X.XX MB',         // Peak memory usage
            'percentage' => 'XX.X%'      // Percentage of memory limit
        ];
    }
}

The MemoryCollector tracks:

  • Current memory usage in MB
  • Memory used since request start (delta)
  • Peak memory usage during the request
  • Memory percentage of PHP's memory limit

TimeCollector

Tracks total request execution time.

class TimeCollector implements CollectorInterface
{
    public static function collect(...$args): string
    {
        $startTime = $args[0] ?? microtime(true);
        return round((microtime(true) - $startTime) * 1000, 2) . 'ms';
    }
}

Returns the total request execution time in milliseconds, calculated from the request start time.

TimelineCollector

Tracks custom timeline events throughout the request lifecycle.

class TimelineCollector implements CollectorInterface
{
    public function addEvent(string $name, string $label = 'event', array $data = []): void
    {
        $this->events[] = [
            'name' => $name,
            'label' => $label,
            'time' => microtime(true),
            'data' => $data,
            'origin' => Debuger::backtraceOrigin()
        ];
    }
}

Timeline events include:

  • Event name and label
  • Timestamp in microseconds
  • Optional event data
  • Origin (class and method where event was added)

MessageCollector

Collects custom debug messages with labels.

class MessageCollector implements CollectorInterface
{
    public function addMessage(mixed $message, string $label = 'info'): void
    {
        $this->messages[] = [
            'message' => $message,
            'label' => $label,      // 'info', 'warning', 'error'
            'time' => microtime(true),
        ];
    }
}

Messages support labels: info, warning, error.

ExceptionCollector

Tracks exceptions that occur during the request.

class ExceptionCollector implements CollectorInterface
{
    public function addException(\Throwable $exception): void
    {
        $this->exceptions[] = [
            'type' => get_class($exception),
            'message' => $exception->getMessage(),
            'code' => $exception->getCode(),
            'file' => BASE_PATH . $exception->getFile() . ':' . $exception->getLine(),
            'trace' => $exception->getTraceAsString(),
        ];
    }
}

Captures exception type, message, code, file location, and full stack trace.

RequestCollector

Collects HTTP request information.

class RequestCollector
{
    public static function collect(Request $request): array
    {
        return [
            'url' => $request->getUrl(),
            'method' => $request->getMethod(),
            'ip' => $request->getClientIp(),
            'headers' => $request->getHeaders(),
            'query' => $request->getQuery(),
            'body' => $request->all(),
            'cookies' => $_COOKIE,
            'files' => $_FILES,
        ];
    }
}

RouteCollector

Collects current route information.

class RouteCollector implements CollectorInterface
{
    public function collectRoutes(): array
    {
        return [
            'uri' => '/example',
            'method' => 'GET',
            'handler' => 'App\\Controllers\\HomeController::index',
            'middleware' => ['auth', 'csrf'],
        ];
    }
}

SessionCollector

Collects session data and metadata.

class SessionCollector
{
    public function collectSessionData(): array
    {
        return [
            'session_id' => 'abc123...',
            'data' => [...],      // Formatted session data
            'count' => 5           // Number of session keys
        ];
    }
}

ViewCollector

Tracks rendered views and their data.

class ViewCollector implements CollectorInterface
{
    public function addView(string $viewPath, array|object $data = []): void
    {
        $this->views[] = [
            'path' => $viewPath,
            'data' => $this->formatDebugData($data),
        ];
    }
}

DatabaseCollector

Tracks database queries with execution time and performance classification. Note: This collector exists but requires integration with the database layer to automatically collect queries.

class DatabaseCollector implements CollectorInterface
{
    public function addQuery(
        string $query,
        array $bindings,
        float $time,
        string $connectionName,
        string $origin
    ): void {
        $this->queries[] = [
            'query' => $query,
            'bindings' => $bindings,
            'time_ms' => number_format($time, 2),
            'connection_name' => $connectionName,
            'origin' => $origin,
            'performance' => $this->classifyQueryPerformance($time)  // 'fast', 'medium', 'slow'
        ];
    }

    private function classifyQueryPerformance(float $time): string
    {
        if ($time > 100) return 'slow';
        if ($time > 50) return 'medium';
        return 'fast';
    }
}

Query performance is classified as:

  • Fast: < 50ms
  • Medium: 50-100ms
  • Slow: > 100ms

Helper Functions

ForgeDebugbar provides helper functions for easy integration with your application code.

add_timeline_event()

Adds a custom timeline event to track specific points in your application's execution.

add_timeline_event(string $name, string $label, array $data = []): void

Parameters:

  • $name - Event name (e.g., 'user_authenticated', 'data_processed')
  • $label - Event label (e.g., 'start', 'end', 'info', 'warning')
  • $data - Optional event data array

Example:

// Track when a user is authenticated
add_timeline_event('user_authenticated', 'info', [
    'user_id' => $user->id,
    'method' => 'email'
]);

// Track start of data processing
add_timeline_event('data_processing', 'start');

// Track end of data processing
add_timeline_event('data_processing', 'end');

Note: This function only works when APP_DEBUG is true.

collect_view_data()

Collects view rendering data for debugging. This is automatically called by the View class, but can be called manually if needed.

collect_view_data(string $view, mixed $data = []): void

Parameters:

  • $view - View path
  • $data - View data (array or object)

Example:

// Automatically called by View class
// But can be called manually if needed
collect_view_data('pages/home/index', [
    'title' => 'Home Page',
    'users' => $users
]);

Automatic HTML Injection

ForgeDebugbar automatically injects the debug bar into HTML responses without requiring any manual integration. The injection is non-intrusive and only occurs when certain conditions are met.

Injection Process

The debug bar is injected during the AFTER_REQUEST lifecycle hook:

  1. All collectors gather their data
  2. Debug bar HTML is rendered with collected data
  3. CSS and JavaScript assets are injected
  4. Everything is inserted before the </body> tag

Injection Conditions

The debug bar is only injected when all of the following conditions are met:

  • APP_DEBUG is true
  • forge_debug_bar.enabled config is true
  • Response Content-Type is text/html (or contains 'text/html')
  • Response content is a string
  • Response contains a </body> tag
  • Response does not start with {"html": (JSON response pattern)

Injection Implementation

public function injectDebugBarIntoHtml(
    string $htmlContent,
    string $debugBarHtml,
    Container $container
): string {
    $cssLinkTag = sprintf(
        '<link rel="stylesheet" href="/assets/modules/forge-debug-bar/css/debugbar.css">'
    );
    $jsScriptTag = sprintf(
        '<script src="/assets/modules/forge-debug-bar/js/debugbar.js"></script>'
    );

    $injectionPoint = strripos($htmlContent, '</body>');

    if ($injectionPoint !== false) {
        $injectedContent = substr($htmlContent, 0, $injectionPoint) .
            $cssLinkTag . "\n" .
            $debugBarHtml . "\n" .
            $jsScriptTag . "\n" .
            substr($htmlContent, $injectionPoint);
        return $injectedContent;
    }

    return $htmlContent . "\n" . $cssLinkTag . "\n" . $debugBarHtml . "\n" . $jsScriptTag;
}

Asset Injection

The following assets are automatically injected:

  • /assets/modules/forge-debug-bar/css/debugbar.css - Debug bar styles
  • /assets/modules/forge-debug-bar/js/debugbar.js - Debug bar JavaScript

These assets are linked during module installation via the asset:link command.

Lifecycle Hooks

ForgeDebugbar integrates with Forge Kernel lifecycle hooks to collect data and inject the debug bar at the appropriate times.

BEFORE_REQUEST Hook

Initializes timeline tracking at the start of the request:

#[LifecycleHook(hook: LifecycleHookName::BEFORE_REQUEST)]
public function onBeforeRequest(Request $request): void
{
    add_timeline_event('onBeforeRequest', 'start');
}

AFTER_REQUEST Hook

Collects all data and injects the debug bar into the response:

#[LifecycleHook(hook: LifecycleHookName::AFTER_REQUEST)]
public function onAfterRequest(Request $request, Response $response): void
{
    add_timeline_event('onAfterRequest', 'end');

    // Initialize memory collector
    self::$memoryCollector = MemoryCollector::instance();

    // Get debug bar instance
    $debugbar = $this->getDebugbarInstance();

    // Register all collectors
    $requestData = RequestCollector::collect($request);
    $debugbar->addCollector('request', function () use ($requestData) {
        return $requestData;
    });

    $sessionData = SessionCollector::collect();
    $debugbar->addCollector('session', function () use ($sessionData) {
        return $sessionData;
    });

    $debugbar->addCollector('memory', function () {
        return self::$memoryCollector ? 
            self::$memoryCollector->getMemoryUsage() : 
            ['error' => 'Memory collector not initialized'];
    });

    $timeData = TimeCollector::collect();
    $debugbar->addCollector('time', function () use ($timeData) {
        return $timeData;
    });

    $timelineData = TimelineCollector::collect();
    $debugbar->addCollector('timeline', function () use ($timelineData) {
        return $timelineData;
    });

    $messagesData = MessageCollector::collect();
    $debugbar->addCollector('messages', function () use ($messagesData) {
        return $messagesData;
    });

    $routeData = RouteCollector::collect();
    $debugbar->addCollector('route', function () use ($routeData) {
        return $routeData;
    });

    $viewData = ViewCollector::collect();
    $debugbar->addCollector('views', function () use ($viewData) {
        return $viewData;
    });

    $exceptionData = ExceptionCollector::collect();
    $debugbar->addCollector('exceptions', function () use ($exceptionData) {
        return $exceptionData;
    });

    // Inject debug bar into response
    DebugBar::getInstance()->injectDebugBarIfEnabled($response, Container::getInstance());
}

Configuration

ForgeDebugbar requires both an environment variable and a configuration setting to be enabled.

Environment Variable

Set APP_DEBUG in your .env file:

APP_DEBUG=true

Security Warning: Never set APP_DEBUG=true in production. The debug bar exposes sensitive information and should only be used in development environments.

Module Configuration

The module provides a default configuration:

#[ConfigDefaults(defaults: [
    'forge_debug_bar' => [
        'enabled' => true
    ]
])]

You can override this in your application configuration:

// config/forge_debug_bar.php
return [
    'enabled' => env('APP_DEBUG', false),
];

Enablement Check

The debug bar is only shown when both conditions are true:

public function shouldEnableDebugBar(Container $container): bool
{
    $forgeDebug = env('APP_DEBUG');
    $config = $container->get(Config::class);
    $configEnabled = $config->get('forge_debug_bar.enabled', true);
    
    return $configEnabled && $forgeDebug;
}

Debug Bar UI

The debug bar provides a tabbed interface with multiple panels for different types of debugging information.

Tabbed Interface

The debug bar includes the following tabs:

  • Memory: Current memory usage, peak memory, and memory percentage
  • Messages: Custom debug messages with labels (info, warning, error)
  • Timeline: Request lifecycle events with timestamps and origins
  • Exceptions: Exceptions with type, message, code, file, and stack trace
  • Views: Rendered views with their data
  • Route: Current route URI, method, handler, and middleware
  • Queries: Database queries with execution time and performance classification
  • Session: Session ID and all session data
  • Request: URL, method, IP, headers, query parameters, body, cookies, and files

Tab Counts

Tabs display counts for collections:

  • Messages tab shows: Messages (5)
  • Timeline tab shows: Timeline (12)
  • Exceptions tab shows: Exceptions (2)
  • Queries tab shows: Queries (8)

Collapsible Panels

Panels can be collapsed/expanded by clicking on the Forge logo. Individual sections within panels can also be collapsed for better organization.

JavaScript Interactivity

The debug bar JavaScript handles:

  • Tab switching
  • Panel collapse/expand
  • Section toggle (for nested data)

Display Values

The debug bar header displays key metrics:

  • Memory usage (current)
  • Execution time (in milliseconds)
  • PHP version

Integration Points

ForgeDebugbar integrates with various parts of the Forge Kernel to collect debugging information.

View Rendering

The View class automatically calls collect_view_data() when rendering views:

// engine/Core/View/View.php
private function ensureRequestCollectorExist(string $view, array $data): void
{
    $ready = is_file($requestCollectorFile)
        && class_exists(\App\Modules\ForgeDebugbar\Collectors\RequestCollector::class);
    
    if ($ready) {
        if (function_exists('collect_view_data')) {
            collect_view_data($view, $data);
        }
    }
}

Exception Handling

The Debuger helper class provides methods for logging exceptions:

// engine/Core/Helpers/Debuger.php
public static function logException(\Throwable $exception): void
{
    if (class_exists(ExceptionCollector::class)) {
        if (env('APP_ENV', false)) {
            $exceptionCollector = Container::getInstance()->get(ExceptionCollector::class);
            $exceptionCollector::instance()->addException($exception);
        }
    }
}

Debug Messages

The Debuger helper class provides a method for adding debug messages:

// engine/Core/Helpers/Debuger.php
public static function message(mixed $message, string $label = 'info'): void
{
    if (class_exists(\App\Modules\ForgeDebugbar\DebugBar::class)) {
        if (env('APP_ENV', false)) {
            $messageCollector = Container::getInstance()->get(MessageCollector::class);
            $messageCollector::instance()->addMessage($message, $label);
        }
    }
}

Request Lifecycle

ForgeDebugbar hooks into the request lifecycle via lifecycle hooks:

  • BEFORE_REQUEST: Initializes timeline tracking
  • AFTER_REQUEST: Collects all data and injects debug bar

Session Management

SessionCollector automatically collects session data from the active session if available.

Database Queries

DatabaseCollector exists and can track queries, but requires integration with the database layer. The collector expects queries to be added via:

DatabaseCollector::instance()->addQuery(
    $query,
    $bindings,
    $timeInMilliseconds,
    $connectionName,
    $origin
);

Usage Examples

Adding Timeline Events

// Track when a user logs in
add_timeline_event('user_login', 'info', [
    'user_id' => $user->id,
    'method' => 'email'
]);

// Track start of expensive operation
add_timeline_event('data_processing', 'start');

// Process data...
$result = processLargeDataset($data);

// Track end of expensive operation
add_timeline_event('data_processing', 'end', [
    'records_processed' => count($result)
]);

Adding Debug Messages

use Forge\Core\Helpers\Debuger;

// Add info message
Debuger::message('User authenticated successfully', 'info');

// Add warning message
Debuger::message('Cache miss detected', 'warning');

// Add error message
Debuger::message('Failed to connect to external API', 'error');

Logging Exceptions

use Forge\Core\Helpers\Debuger;

try {
    // Some code that might throw an exception
    $result = riskyOperation();
} catch (\Exception $e) {
    // Log exception to debug bar
    Debuger::logException($e);
    
    // Handle exception...
    throw $e;
}

Configuration Example

// .env
APP_DEBUG=true

// config/forge_debug_bar.php
return [
    'enabled' => env('APP_DEBUG', false),
];

Custom Collector (Advanced)

You can create custom collectors by implementing the CollectorInterface:

use App\Modules\ForgeDebugbar\Collectors\CollectorInterface;

class CustomCollector implements CollectorInterface
{
    private array $data = [];

    public static function collect(...$args): array
    {
        return self::instance()->data;
    }

    public static function instance(): self
    {
        static $instance = null;
        if (null === $instance) {
            $instance = new self();
        }
        return $instance;
    }

    public function addData(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }
}

// Register in DebugBarModule::onAfterRequest()
$customData = CustomCollector::collect();
$debugbar->addCollector('custom', function () use ($customData) {
    return $customData;
});

Best Practices

Development Only

  • Always disable APP_DEBUG in production
  • Never deploy with debug bar enabled to production environments
  • Use environment-specific configuration to ensure debug bar is disabled in production

Timeline Events

  • Use timeline events to track performance bottlenecks
  • Add events at the start and end of expensive operations
  • Include relevant data in event data for debugging
  • Use descriptive event names and labels

Debug Messages

  • Use messages for important debugging information
  • Use appropriate labels (info, warning, error)
  • Keep messages concise but informative
  • Remove debug messages before committing to production

Memory Monitoring

  • Monitor memory usage during development
  • Watch for memory leaks in long-running processes
  • Use memory percentage to identify potential issues
  • Check peak memory usage for optimization opportunities

Query Performance

  • Review query execution times in the Queries tab
  • Identify slow queries (marked in red)
  • Optimize queries that exceed performance thresholds
  • Use query origin to identify where queries are executed

Exception Tracking

  • Review exceptions in the Exceptions tab
  • Use stack traces to identify root causes
  • Check exception types and messages for patterns
  • Fix exceptions before deploying to production

View Tracking

  • Review rendered views to understand view hierarchy
  • Check view data to ensure correct data is passed
  • Identify unnecessary view renders
  • Optimize view rendering for performance

Security Considerations

  • Never enable debug bar in production
  • Debug bar exposes sensitive information (session data, request data, etc.)
  • Use environment variables to control debug bar visibility
  • Consider IP-based restrictions for development environments