Inspector
The Canvas Inspector is a debugging tool that provides insights into your application's performance, database queries, and request processing. After your controller executes and returns a Response, the Inspector injects a debug toolbar into HTML responses.
Enabling the Inspector
Configure the Inspector in config/inspector.php:
<?php
// config/inspector.php
return [
'enabled' => true // Set to true to enable debug toolbar injection
];
Production Warning: Always disable the Inspector in production. It exposes sensitive information including database queries, request parameters, file paths, execution times, and memory usage. Set enabled to false in your production configuration files.
Screenshot
How the Inspector Works
Request Lifecycle Integration
The Inspector integrates into Canvas's request handling through these steps:
- Initialization: When
Kernel::handle()starts processing a request, it creates anEventCollectorinstance ifconfig/inspector.phphas'enabled' => true - Event Collection: Throughout request processing, components emit debug signals (e.g.,
debug.objectquel.query,debug.canvas.query). The EventCollector automatically captures all signals starting withdebug. - Controller Execution: Your application processes normally - routing, controller methods, database queries, etc.
- Performance Signal: After your controller returns,
Kernelemits adebug.canvas.querysignal containing route info, execution time, and memory usage - Injection: The
Inspector::inject()method checks if the Response is HTML and injects the debug toolbar before the closing</body>tag - Response Sent: The modified Response (with debug toolbar) is sent to the browser
HTML Detection and Injection
The Inspector only modifies HTML responses:
- HTML responses (Content-Type: text/html): Debug toolbar is injected before
</body>, or before</html>if no body tag exists - JSON/XML/other responses: Passed through unmodified - no debugging overhead for API endpoints
- Malformed HTML: If HTML lacks proper structure, the Inspector wraps content in complete HTML document with the debug toolbar
Signal-Based Event System
The Inspector uses Canvas's SignalHub system to collect debugging data without tight coupling between components. SignalHub is based on Qt's signals and slots pattern, providing type-safe event handling.
How Signals Work
- Signal Definition: Components create named signals like
debug.cache.hitusingSignalclass - Registration: Signals are registered with the
SignalHub, a centralized registry that uses WeakMap for automatic memory management - Connection: The
EventCollectorautomatically connects to all signals matching the patterndebug.* - Emission: When an operation occurs (e.g., database query), the component emits its signal with data payload
- Collection: EventCollector's connected callback stores the event with signal name, data, and high-precision timestamp
- Panel Access: Inspector panels retrieve relevant events by filtering on signal patterns
Emitting Debug Signals
To add debugging for your own operations, emit signals following the debug.* naming convention:
<?php
use Quellabs\SignalHub\HasSignals;
use Quellabs\SignalHub\SignalHubLocator;
class CacheService {
use HasSignals;
public function __construct() {
// Connect to the application's SignalHub
$this->setSignalHub(SignalHubLocator::getInstance());
// Create a signal for cache hits/misses
// Signal name must start with 'debug.' to be collected by Inspector
$this->createSignal(['array'], 'debug.cache.operation');
}
public function get(string $key): mixed {
$start = microtime(true);
$hit = /* ... check cache ... */;
$duration = microtime(true) - $start;
// Emit the signal with event data
$this->getSignal('debug.cache.operation')->emit([
'key' => $key,
'hit' => $hit,
'execution_time_ms' => $duration * 1000
]);
return /* ... cached value ... */;
}
}
Event Structure
Each collected event contains:
- signal (string): The signal name (e.g.,
'debug.cache.operation') - data (array): The payload emitted with the signal (e.g.,
['key' => 'user:123', 'hit' => true]) - timestamp (float): High-precision timestamp from
microtime(true)
// Example event structure from EventCollector::getEvents()
[
[
'signal' => 'debug.cache.operation',
'data' => [
'key' => 'user:123',
'hit' => true,
'execution_time_ms' => 0.42
],
'timestamp' => 1735000123.4567
],
// ... more events
]
Built-in Debug Panels
Request Panel
Displays HTTP request information captured from the debug.canvas.query signal:
- Route Information: Controller class name, method name, route pattern (e.g.,
/users/{id}), route parameters - Request Details: HTTP method (GET/POST/PUT/PATCH/DELETE), full URI, client IP address, user agent string
- POST Data: All form fields submitted via POST/PUT/PATCH
- File Uploads: Uploaded file names, sizes, MIME types, temporary paths
- Cookies: All cookies sent with the request (names and values)
- Legacy Indicator: Whether the request was handled by a legacy PHP file or Canvas controller
Query Panel
Displays database operations (requires database layer to emit debug.objectquel.query or similar signals):
- SQL Statements: Complete SQL queries (ObjectQuel generated or raw SQL)
- Execution Time: Time in milliseconds for each individual query
- Bound Parameters: Values bound to prepared statement placeholders
- Query Count: Total number of database queries executed during the request
- Total Time: Cumulative time spent on all database operations
Creating Custom Inspector Panels
Extend the Inspector with custom panels by implementing InspectorPanelInterface:
<?php
namespace App\Inspector\Panels;
use Quellabs\Canvas\Inspector\EventCollector;
use Quellabs\Contracts\Inspector\InspectorPanelInterface;
use Symfony\Component\HttpFoundation\Request;
class CachePanel implements InspectorPanelInterface {
private array $cacheEvents = [];
private EventCollector $collector;
public function __construct(EventCollector $collector) {
$this->collector = $collector;
}
/**
* Return signal patterns this panel wants to receive
* Supports wildcards: 'debug.cache.*' matches all cache signals
*/
public function getSignalPatterns(): array {
return ['debug.cache.*'];
}
/**
* Called by Inspector to process collected events
* Retrieve events matching your patterns from the collector
*/
public function processEvents(): void {
$this->cacheEvents = $this->collector->getEventsBySignals($this->getSignalPatterns());
}
/**
* Return unique panel identifier (used in DOM IDs)
*/
public function getName(): string {
return 'cache';
}
/**
* Return tab label text (can include dynamic counts)
*/
public function getTabLabel(): string {
$hits = count(array_filter($this->cacheEvents, fn($e) => $e['data']['hit'] ?? false));
$total = count($this->cacheEvents);
return "Cache ({$hits}/{$total})";
}
/**
* Return emoji or icon for the tab (optional)
*/
public function getIcon(): string {
return '🗂️';
}
/**
* Return data to pass to the JavaScript template
* This data becomes available in your JS template as the 'data' variable
*/
public function getData(Request $request): array {
return [
'events' => $this->cacheEvents,
'hit_ratio' => $this->calculateHitRatio(),
'total_time_ms' => $this->calculateTotalTime()
];
}
/**
* Return statistics for the header bar (optional)
* These appear in the collapsed debug bar
*/
public function getStats(): array {
return [
'cache_hits' => $this->countHits(),
'cache_misses' => $this->countMisses()
];
}
/**
* Return JavaScript template that renders the panel content
* Receives data from getData() method
* Available helper functions: escapeHtml(), formatTimeBadge()
*/
public function getJsTemplate(): string {
return <<<'JS'
const events = data.events.map(event => `
${event.data.hit ? '✅ HIT' : '❌ MISS'}
${formatTimeBadge(event.data.execution_time_ms || 0)}
Key:
${escapeHtml(event.data.key)}
`).join('');
return `
Cache Operations (Hit Ratio: ${data.hit_ratio}%)
${events || 'No cache operations recorded
'}
Total Time: ${data.total_time_ms.toFixed(2)}ms
`;
JS;
}
/**
* Return custom CSS for this panel (optional)
*/
public function getCss(): string {
return <<<'CSS'
.canvas-debug-item .cache-hit {
color: #28a745;
}
.canvas-debug-item .cache-miss {
color: #dc3545;
}
CSS;
}
// Helper methods
private function calculateHitRatio(): float {
$total = count($this->cacheEvents);
if ($total === 0) return 0.0;
$hits = count(array_filter($this->cacheEvents, fn($e) => $e['data']['hit'] ?? false));
return round(($hits / $total) * 100, 1);
}
private function calculateTotalTime(): float {
return array_reduce($this->cacheEvents,
fn($sum, $e) => $sum + ($e['data']['execution_time_ms'] ?? 0),
0.0
);
}
private function countHits(): int {
return count(array_filter($this->cacheEvents, fn($e) => $e['data']['hit'] ?? false));
}
private function countMisses(): int {
return count($this->cacheEvents) - $this->countHits();
}
}
Registering Custom Panels
Add your custom panel to the Inspector configuration:
<?php
// config/inspector.php
return [
'enabled' => true,
'panels' => [
'cache' => \App\Inspector\Panels\CachePanel::class,
'email' => \App\Inspector\Panels\EmailPanel::class,
'api' => \App\Inspector\Panels\ApiPanel::class
]
];
Panel Lifecycle
Understanding when panel methods are called:
- Construction: Panel is instantiated with EventCollector injected via constructor
- Event Collection: During request processing, EventCollector captures signals matching
debug.* - Process Events: After response is created,
processEvents()is called to let panels filter their events - Get Data:
getData()is called to prepare data for JavaScript template - Render: JavaScript template receives data and renders HTML
- Inject: Rendered HTML is injected into the response before
</body>
JavaScript Template Helpers
Available functions in getJsTemplate():
escapeHtml(str)
Escapes HTML special characters to prevent XSS:
escapeHtml('<script>alert("xss")</script>')
// Returns: '<script>alert("xss")</script>'
formatTimeBadge(milliseconds)
Formats execution time as a styled badge:
formatTimeBadge(1.234) // Fast: green badge
formatTimeBadge(25.5) // Medium: yellow badge
formatTimeBadge(150.0) // Slow: red badge
Inspector Interface
The debug toolbar appears at the bottom of your browser window with these features:
Collapsed State
- Header Statistics: Execution time, query count, total query time, memory usage
- Panel Tabs: Quick access to each panel (shows counts/icons)
- Minimize/Expand Toggle: Click to expand for detailed view
Expanded State
- Tab Navigation: Switch between panels
- Panel Content: Full debugging information rendered by each panel
- Persistent State: Stays expanded across page loads (browser localStorage)
- Responsive Layout: Adjusts for desktop and mobile screens
Signal Naming Conventions
Follow these patterns for consistency:
// Component naming: debug.{component}.{action}
'debug.cache.hit' // Cache hit occurred
'debug.cache.miss' // Cache miss occurred
'debug.cache.write' // Cache write operation
// Wildcards in panels match multiple signals
'debug.cache.*' // Matches all cache signals
'debug.email.*' // Matches all email signals
'debug.api.request.*' // Matches all API request signals
Performance Considerations
Overhead When Enabled
- EventCollector stores all
debug.*signals in memory during the request - Panels process events and generate HTML after controller executes
- Response content is parsed to find
</body>tag for injection - Typical overhead: 5-20ms and 1-5MB memory depending on event count
Overhead When Disabled
- Zero overhead - EventCollector is not instantiated
- Signal emissions still occur but have no connected receivers
- No memory used for event collection
- No response parsing or HTML injection
Best Practices
Signal Design
- Start with 'debug.' - Only signals with this prefix are collected
- Use namespacing:
debug.module.action(e.g.,debug.email.sent) - Include timing: Add
execution_time_msfor performance tracking - Keep data small: Don't emit large payloads (full response bodies, images, etc.)
Panel Development
- Use specific patterns: Match only relevant signals, not
debug.*which matches everything - Handle missing data: Use null coalescing (
??) when accessing event data - Escape output: Always use
escapeHtml()in JavaScript templates - Show empty states: Display helpful message when no events collected
Production Safety
- Never enable in production: Use environment-based config
- Don't emit sensitive data: Passwords, API keys, session tokens should not be in debug signals
- Test panel performance: Ensure panels don't slow down debugging with expensive operations