laravel-systematic-debugging

Systematic debugging process for Laravel applications - ensures root cause investigation before attempting fixes. Use for any Laravel issue (test failures, bugs, unexpected behavior, performance problems).

iserter/laravel-claude-agents84 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: laravel-systematic-debugging
description: Systematic debugging process for Laravel applications - ensures root cause investigation before attempting fixes. Use for any Laravel issue (test failures, bugs, unexpected behavior, performance problems).
license: MIT
---

# Systematic Debugging for Laravel

## Overview

Random fixes waste time and create new bugs in Laravel applications. Quick patches mask underlying issues.

**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.

**Violating the letter of this process is violating the spirit of debugging.**

## The Iron Law

```
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
```

If you haven't completed Phase 1, you cannot propose fixes.

## When to Use

Use for ANY Laravel technical issue:
- Test failures
- Eloquent query issues
- Authentication/authorization bugs
- Validation failures
- Queue job failures
- Route errors
- Migration issues
- N+1 query problems
- Performance issues

**Use this ESPECIALLY when:**
- Under time pressure
- "Just one quick fix" seems obvious
- You've already tried multiple fixes
- Previous fix didn't work
- You don't fully understand the issue

## The Four Phases

You MUST complete each phase before proceeding to the next.

### Phase 1: Root Cause Investigation

**BEFORE attempting ANY fix:**

1. **Read Error Messages Carefully**
   ```
   SQLSTATE[23000]: Integrity constraint violation
   → Check foreign key constraints, not a code bug
   
   Class 'App\Models\Post' not found
   → Check namespace, run composer dump-autoload
   
   Method Illuminate\Database\Eloquent\Collection::save does not exist
   → get() returns Collection, not Model. Use first() or update()
   ```

2. **Check Laravel Logs**
   ```bash
   # Main Laravel log
   tail -f storage/logs/laravel.log
   
   # Check for specific errors
   grep "SQLSTATE" storage/logs/laravel.log
   
   # Clear logs if too large
   > storage/logs/laravel.log
   ```

3. **Enable Debug Mode (Local Only)**
   ```env
   APP_DEBUG=true
   APP_ENV=local
   ```

4. **Use Laravel Telescope**
   ```bash
   composer require laravel/telescope --dev
   php artisan telescope:install
   php artisan migrate
   
   # Access at /telescope
   # View: Requests, Queries, Jobs, Events, Exceptions
   ```

5. **Check Recent Changes**
   ```bash
   # What changed that could cause this?
   git log --oneline -10
   git diff HEAD~5
   
   # Check if migrations ran
   php artisan migrate:status
   
   # Check if config cached
   php artisan config:show
   ```

6. **Reproduce Consistently**
   ```bash
   # Can you trigger it every time?
   php artisan tinker
   >>> App\Models\Post::first();
   
   # Try in different environments
   APP_ENV=testing php artisan test
   ```

7. **Trace Data Flow for Eloquent Issues**
   ```php
   // Enable query logging
   DB::listen(function ($query) {
       Log::debug('Query executed', [
           'sql' => $query->sql,
           'bindings' => $query->bindings,
           'time' => $query->time,
       ]);
   });
   
   // Or in specific code
   DB::enableQueryLog();
   $posts = Post::with('user')->get();
   dd(DB::getQueryLog());
   ```

### Phase 2: Pattern Analysis

**Find the pattern before fixing:**

1. **Find Working Examples in Laravel**
   ```bash
   # Search for similar working code
   grep -r "belongsTo" app/Models/
   grep -r "middleware" app/Http/
   
   # Check Laravel docs for the pattern
   # Check other models that work correctly
   ```

2. **Compare Against Laravel Conventions**
   ```php
   // ❌ What you have
   class Post extends Model {
       public function author() {
           return $this->hasOne(User::class, 'id', 'user_id');
       }
   }
   
   // ✅ Laravel convention
   class Post extends Model {
       public function user(): BelongsTo {
           return $this->belongsTo(User::class);
       }
   }
   ```

3. **Check Laravel Documentation**
   - Read the COMPLETE section, don't skim
   - Follow examples exactly first
   - Customize only after understanding

4. **Identify Differences**
   ```php
   // Working model
   class User extends Model {
       protected $fillable = ['name', 'email'];
   }
   
   // Broken model - difference: missing mass assignment protection
   class Post extends Model {
       // No $fillable or $guarded defined
   }
   ```

### Phase 3: Hypothesis and Testing

**Scientific method:**

1. **Form Single Hypothesis**
   ```
   Hypothesis: "Posts aren't saving because mass assignment 
   protection is blocking the 'user_id' field"
   
   Expected: Adding 'user_id' to $fillable will fix it
   ```

2. **Test Minimally**
   ```php
   // Before (broken)
   protected $fillable = ['title', 'content'];
   
   // Test change (ONE variable)
   protected $fillable = ['title', 'content', 'user_id'];
   
   // Don't change multiple things at once
   ```

3. **Verify in Tinker**
   ```bash
   php artisan tinker
   >>> $post = Post::create(['title' => 'Test', 'content' => 'Test', 'user_id' => 1]);
   >>> $post->user_id; // Should be 1
   ```

4. **When You Don't Know**
   - Say "I don't understand why X is happening"
   - Check Laravel GitHub issues for similar problems
   - Ask in Laravel Discord/Forums with specifics
   - Don't pretend to know

### Phase 4: Implementation

**Fix the root cause, not the symptom:**

1. **Create Failing Test Case**
   ```php
   use Illuminate\Foundation\Testing\RefreshDatabase;
   
   test('user can create post', function () {
       $user = User::factory()->create();
       
       $response = $this->actingAs($user)
           ->post('/posts', [
               'title' => 'Test Post',
               'content' => 'Test content',
           ]);
       
       $response->assertRedirect();
       
       expect(Post::where('title', 'Test Post')->exists())->toBeTrue();
       expect(Post::first()->user_id)->toBe($user->id);
   });
   
   // Run and watch it FAIL first
   php artisan test --filter=user_can_create_post
   ```

2. **Implement Single Fix**
   ```php
   // ONE fix for the root cause
   protected $fillable = ['title', 'content', 'user_id'];
   
   // NO bundled improvements like:
   // - Adding casts
   // - Refactoring methods
   // - Changing other code
   ```

3. **Verify Fix**
   ```bash
   # Test passes now
   php artisan test --filter=user_can_create_post
   
   # All tests still pass
   php artisan test
   
   # Manual verification
   php artisan tinker
   >>> $post = Post::create([...]);
   ```

4. **If Fix Doesn't Work**
   - STOP
   - Count: How many fixes have you tried?
   - If < 3: Return to Phase 1 with new information
   - **If ≥ 3: STOP and question the approach**

5. **If 3+ Fixes Failed: Question Architecture**
   ```
   Pattern indicating architectural problem:
   - Each fix reveals new shared state/coupling
   - Fixes require "massive refactoring"
   - Each fix creates new symptoms elsewhere
   
   STOP and question fundamentals:
   - Is this Laravel pattern correct?
   - Should we use a different approach (repository/service)?
   - Are we fighting the framework?
   
   Discuss with team before attempting more fixes.
   ```

## Laravel-Specific Debug Techniques

### Eloquent Debugging
```php
// See actual SQL
$posts = Post::where('status', 'published');
dd($posts->toSql(), $posts->getBindings());

// Check relationship loading
$post = Post::first();
$post->relationLoaded('user'); // false
$post->load('user');
$post->relationLoaded('user'); // true

// Prevent lazy loading (catch N+1)
Model::preventLazyLoading(!app()->isProduction());
```

### Route Debugging
```bash
# List all routes
php artisan route:list

# Find specific route
php artisan route:list --name=posts

# Check route exists
php artisan tinker
>>> route('posts.show', 1);
```

### Queue Debugging
```bash
# See failed jobs
php artisan queue:failed

# Retry failed job
php artisan queue:retry <id>

# Work queue with verbose output
php artisan queue:work --verbose

# Check job payload
php artisan tinker
>>> DB::table('jobs')->first();
```

### Validation Debugging
```php
// See exact validation errors
protected function failedValidation(Validator $validator)
{
    Log::debug('Validation failed', [
        'errors' => $validator->errors()->toArray(),
        'input' => $this->all(),
    ]);
    
    parent::failedValidation($validator);
}
```

## Red Flags - STOP and Follow Process

If you catch yourself thinking:
- "Quick fix for now, investigate later"
- "Just try changing X and see if it works"
- "Add protected $guarded = [] to see if that helps"
- "Skip the test, I'll manually verify"
- "It's probably the relationship definition"
- "I don't fully understand Eloquent but this might work"
- "The docs say X but I'll adapt it differently"
- **"One more fix attempt" (when already tried 2+)**

**ALL of these mean: STOP. Return to Phase 1.**

## Common Laravel Debugging Scenarios

### Scenario 1: N+1 Query Problem
```
Phase 1: Detect it
- Enable Model::preventLazyLoading()
- Exception thrown showing the problem

Phase 2: Find the pattern
- Check working code that uses with()
- Identify which relationship is lazy loading

Phase 3: Hypothesis
- "Adding with('user') will prevent the N+1"

Phase 4: Fix
- Add test that counts queries
- Add with('user') to the query
- Verify query count reduced
```

### Scenario 2: Route Model Binding Not Working
```
Phase 1: Investigate
- Check route definition: /posts/{post}
- Check controller parameter: Post $post
- Check if using custom key

Phase 2: Pattern
- Compare with working route binding
- Check Post model for getRouteKeyName()

Phase 3: Hypothesis
- "Parameter name doesn't match or model not found"

Phase 4: Fix
- Ensure route parameter matches method parameter
- Or customize: public function getRouteKeyName() { return 'slug'; }
```

### Scenario 3: Mass Assignment Exception
```
Phase 1: Error says "Add [field] to fillable property"
Phase 2: Check other models' $fillable arrays
Phase 3: Hypothesis: "Field not in $fillable"
Phase 4: Add field to $fillable, test
```

## Integration with Laravel Agents

- Use **laravel-debugger** for Laravel-specific debugging help
- Use **laravel-testing-expert** for creating failing tests (Phase 4)
- Use **eloquent-specialist** for relationship debugging
- Use **laravel-performance-optimizer** for performance issues

## Quick Reference

| Phase | Laravel-Specific Activities | Success Criteria |
|-------|----------------------------|------------------|
| **1. Root Cause** | Check logs, Telescope, Tinker, recent changes | Understand WHAT and WHY |
| **2. Pattern** | Find working Laravel examples, check docs | Identify differences |
| **3. Hypothesis** | Form theory, test in Tinker | Confirmed or new hypothesis |
| **4. Implementation** | Create Pest test, fix, verify | Bug resolved, tests pass |

## Remember

- Laravel has excellent error messages - read them fully
- Use Telescope for comprehensive debugging
- Tinker is your friend for testing hypotheses
- Follow Laravel conventions - fighting the framework causes bugs
- 95% of "weird Laravel behavior" is misunderstanding the framework

Always investigate systematically, understand the root cause, then fix once correctly.

More Debugging skills

← All Debugging skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY