DebugBar & Model Eager Loading

Professional DebugBar for real-time application insights and N+1 query prevention through model eager loading

Chapter 27: Part of the Larafony Framework - A modern PHP 8.5 framework built from scratch. Full implementation details with tests available at masterphp.eu

Overview

Chapter 27 introduces two critical development features: a professional DebugBar for real-time application insights and N+1 query prevention through model eager loading. The DebugBar provides comprehensive debugging information during development, while eager loading ensures production-grade performance by eliminating the notorious N+1 query problem.

The DebugBar is a non-intrusive toolbar injected into HTML responses, collecting data through event listeners without modifying application logic. It tracks database queries with execution time and backtrace, cache operations (hits/misses/writes/deletes) with hit ratio calculation, view rendering with template names and data, route matching with parameters and controller info, request/response details (method, URI, headers, status), application performance (execution time, memory usage, peak memory), and timeline visualization showing the complete request lifecycle.

Model eager loading solves the N+1 query problem by loading related models in bulk rather than one-by-one. Instead of executing 1 + N queries (one to fetch parent models, then one query per parent for its relations), eager loading executes just 2 queries (one for parents, one for all related models), dramatically reducing database load and improving response times.

Key Performance Impact:

Key Components

DebugBar System

Data Collectors

Each collector implements DataCollectorContract and listens to framework events:

Model Eager Loading

Usage Examples

DebugBar Integration

The DebugBar is automatically enabled in development environments and displays at the bottom of HTML pages:

// config/app.php - DebugBar is registered via DebugBarServiceProvider
use Larafony\Framework\DebugBar\ServiceProviders\DebugBarServiceProvider;

return [
    'providers' => [
        // ... other providers
        DebugBarServiceProvider::class,
    ],
];

// config/debugbar.php - Configure DebugBar behavior
use Larafony\Framework\Config\Environment\EnvReader;
use Larafony\Framework\DebugBar\Collectors\CacheCollector;
use Larafony\Framework\DebugBar\Collectors\PerformanceCollector;
use Larafony\Framework\DebugBar\Collectors\QueryCollector;
use Larafony\Framework\DebugBar\Collectors\RequestCollector;
use Larafony\Framework\DebugBar\Collectors\RouteCollector;
use Larafony\Framework\DebugBar\Collectors\TimelineCollector;
use Larafony\Framework\DebugBar\Collectors\ViewCollector;

return [
    'enabled' => EnvReader::read('APP_DEBUG', false),

    'collectors' => [
        'queries' => QueryCollector::class,
        'cache' => CacheCollector::class,
        'views' => ViewCollector::class,
        'route' => RouteCollector::class,
        'request' => RequestCollector::class,
        'performance' => PerformanceCollector::class,
        'timeline' => TimelineCollector::class,
    ]
];

// The middleware is automatically registered in HTTP kernel
// No manual configuration needed!

What You See:

When you load any HTML page in development, the DebugBar appears at the bottom showing:

Basic Eager Loading

use App\Models\User;

// ❌ N+1 Problem (101 queries for 100 users)
$users = User::query()->get(); // 1 query

foreach ($users as $user) {
    echo $user->role->name; // 100 queries (one per user)
}
// Total: 101 queries

// ✅ With Eager Loading (2 queries for 100 users)
$users = User::query()->with(['role'])->get(); // 2 queries (users + roles)

foreach ($users as $user) {
    echo $user->role->name; // No query - already loaded
}
// Total: 2 queries

DebugBar Shows:

Nested Eager Loading

use App\Models\Post;

// Load posts with author and author's profile
$posts = Post::query()
    ->with(['author.profile'])
    ->get();

// 3 queries total:
// 1. SELECT * FROM posts
// 2. SELECT * FROM users WHERE id IN (...)
// 3. SELECT * FROM profiles WHERE user_id IN (...)

foreach ($posts as $post) {
    echo $post->author->profile->bio; // No queries - all loaded
}

Nested Relation Syntax:

Multiple Relations

use App\Models\User;

// Load multiple relations at once
$users = User::query()
    ->with(['role', 'permissions', 'posts'])
    ->get();

// 4 queries total:
// 1. SELECT * FROM users
// 2. SELECT * FROM roles WHERE id IN (...)
// 3. SELECT * FROM permissions WHERE user_id IN (...)
// 4. SELECT * FROM posts WHERE author_id IN (...)

foreach ($users as $user) {
    echo $user->role->name;
    echo count($user->permissions);
    echo count($user->posts);
    // All data already loaded - no additional queries
}

Complex Nested Loading

use App\Models\Category;

// Deep nesting with multiple branches
$categories = Category::query()
    ->with([
        'posts.author.profile',      // Posts -> Authors -> Profiles
        'posts.comments.user',        // Posts -> Comments -> Users
        'posts.tags',                 // Posts -> Tags
    ])
    ->get();

// 7 queries total:
// 1. SELECT * FROM categories
// 2. SELECT * FROM posts WHERE category_id IN (...)
// 3. SELECT * FROM users WHERE id IN (...)  -- authors
// 4. SELECT * FROM profiles WHERE user_id IN (...)
// 5. SELECT * FROM comments WHERE post_id IN (...)
// 6. SELECT * FROM users WHERE id IN (...)  -- comment authors
// 7. SELECT * FROM tags JOIN post_tag WHERE post_id IN (...)

foreach ($categories as $category) {
    foreach ($category->posts as $post) {
        echo $post->author->profile->bio;
        foreach ($post->comments as $comment) {
            echo $comment->user->name;
        }
        foreach ($post->tags as $tag) {
            echo $tag->name;
        }
    }
}
// All data accessed without additional queries!

DebugBar Timeline Shows:

Implementation Details

DebugBar

Location: src/Larafony/DebugBar/DebugBar.php

Purpose: Central orchestrator managing all data collectors and coordinating data collection.

Key Methods:

InjectDebugBar Middleware

Location: src/Larafony/DebugBar/Middleware/InjectDebugBar.php

Purpose: PSR-15 middleware that injects DebugBar toolbar HTML into responses.

Injection Logic:

  1. Check if DebugBar is enabled - if not, return original response
  2. Check Content-Type - must contain 'text/html'
  3. Check status code - must be < 400 (not error page)
  4. Find </body> tag in response body
  5. Render toolbar view with collected data
  6. Insert toolbar HTML before </body>
  7. Return modified response

Safety Checks:

DebugBarServiceProvider

Location: src/Larafony/DebugBar/ServiceProviders/DebugBarServiceProvider.php

Purpose: Service provider responsible for bootstrapping DebugBar with configuration-driven collector registration.

Bootstrap Algorithm:

  1. Check if DebugBar is enabled in config - early return if disabled (zero overhead in production)
  2. Create DebugBar instance
  3. Load collectors configuration from config/debugbar.php
  4. Iterate through collector class names
  5. Resolve each collector from container (supports DI)
  6. Register collector with DebugBar
  7. Store collector instances for event listener discovery
  8. Enable DebugBar
  9. Register DebugBar singleton in container
  10. Discover and register event listeners using ListenerDiscovery
Performance Optimization: The provider uses an early return pattern when DebugBar is disabled, ensuring zero overhead in production environments - no collectors are instantiated, no event listeners registered, and no memory allocated for debugging infrastructure.

EagerRelationsLoader

Location: src/Larafony/Database/ORM/EagerLoading/EagerRelationsLoader.php

Purpose: Orchestrate eager loading of model relations to prevent N+1 queries.

Algorithm:

  1. For each configured relation:
  2. Get relation instance from first model
  3. Determine loader type (BelongsTo, HasMany, etc.)
  4. Delegate to specific loader
  5. Pass nested relations for recursive loading

HasManyLoader

Location: src/Larafony/Database/ORM/EagerLoading/HasManyLoader.php

Purpose: Load hasMany relations efficiently with single query.

Algorithm:

  1. Extract foreign_key, local_key, related class from relation via reflection
  2. Collect local key values from all parent models
  3. Execute single whereIn(foreign_key, local_keys) query
  4. Support nested eager loading recursively
  5. Group results by foreign key value
  6. Assign grouped arrays to parent models
// Given: 100 users, each with multiple posts
// Without eager loading: 1 + 100 queries
// With eager loading: 1 + 1 queries

$users = User::query()->with(['posts'])->get();

// 2 queries:
// SELECT * FROM users
// SELECT * FROM posts WHERE user_id IN (1,2,3,...,100)

Testing

The DebugBar and eager loading features are tested through integration tests:

DebugBar Integration Tests

Coverage: Tests verify:

Eager Loading Tests

Coverage: Tests verify:

// Without eager loading
$users = User::query()->get();
$this->assertQueryCount(101); // 1 + 100

// With eager loading
$users = User::query()->with(['role'])->get();
$this->assertQueryCount(2); // 1 + 1
Learn More: This implementation is explained in detail with step-by-step tutorials, tests, and best practices at masterphp.eu