Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions .agents/skills/debug-using-debugbar/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: debug-using-debugbar
description: >
Use this skill to optimize requests or debug Laravel application issues — slow pages, N+1 queries, exceptions,
failed requests, or unexpected behavior — by inspecting data captured by Laravel Debugbar via
Artisan CLI commands. Use when the user asks to investigate a bug, diagnose a slow request,
find duplicate queries, check what happened on a previous request, or optimize database
performance, even if they don't explicitly mention "debugbar" or "profiling."
compatibility: Requires Laravel with fruitcake/laravel-debugbar installed and debug mode enabled.
Inspect what a Laravel request actually did — queries, exceptions, timing, cache, auth, views — by reading
the profiling data Laravel Debugbar already captured, via Artisan commands. Use when investigating a bug,
a slow page or endpoint, an N+1 or duplicate query problem, a failing or slow SQL statement, a 500 or
unexpected status code, or when asked to optimize a request. Also covers queued jobs and Artisan commands.
Applies even when the user does not mention "debugbar" or "profiling".
compatibility: Requires Laravel with fruitcake/laravel-debugbar installed, debug mode enabled, and `debugbar.storage.enabled` set.
---

## Debugging and optimizing workflow
Expand All @@ -15,7 +15,7 @@ compatibility: Requires Laravel with fruitcake/laravel-debugbar installed and de
```bash
php artisan debugbar:find --issues --max=50
```
2. Inspect the request summary to identify which collectors have data:
2. Inspect the request summary to see which collectors have data:
```bash
php artisan debugbar:get {id}
```
Expand All @@ -29,6 +29,9 @@ compatibility: Requires Laravel with fruitcake/laravel-debugbar installed and de
```
5. Trace the problem to source code using backtraces, then fix and re-test.

If the storage is empty, there is nothing to debug yet — ask the user to exercise the page or endpoint first,
or trigger it yourself, then run `debugbar:find` again.

## Finding requests

```bash
Expand Down Expand Up @@ -56,6 +59,15 @@ php artisan debugbar:find --min-queries=20

`--issues` flags: exceptions, non-2xx status, high query count, slow queries, duplicate query groups, slow request duration, and failed queries. Issue filtering applies on top of the fetched result set — increase `--max` to scan further back.

Queued jobs and Artisan commands are stored too, with `method` set to `JOB` or `CLI`:

```bash
php artisan debugbar:find --method=JOB # queued jobs

php artisan debugbar:find --method=CLI # artisan commands

```

## Inspecting a request

```bash
Expand All @@ -72,12 +84,16 @@ php artisan debugbar:get {id} --collector=exceptions

Pick the collector by issue type:
- **Error/500** → `exceptions` · **Slow page** → `queries`, `time` · **Auth** → `auth`, `gate` · **Cache** → `cache`
- **N+1 / ORM** → `queries`, `models` · **View overhead** → `views` · **External calls** → `http_client`
- **Log output** → `log` (Laravel log events), `messages` (`debug()` calls), `logs` (log file tail)

If the collector name is wrong, the command lists the collectors that actually have data for that request.

## Analyzing queries

```bash

# Overview with duplicate detection and slow query flags
# Overview with duplicate detection, slow flags and failed statements

php artisan debugbar:queries {id}

Expand All @@ -91,12 +107,34 @@ php artisan debugbar:queries {id} --statement=N --explain
php artisan debugbar:queries {id} --statement=N --result
```

Duplicate queries are a strong N+1 signal. Use `--statement=N` to get the backtrace and find the origin.
The `Flags` column marks `SLOW` and `FAILED` statements; failed statements are listed again below the table with their driver error.

Two separate repetition reports follow the table, and they mean different things:

- **Duplicate queries** — identical SQL *and* identical bindings. Usually a query that should have been cached or hoisted out of a loop.
- **Repeated query shapes with varying bindings** — the same query with a different value each time. This is the classic N+1: an unloaded relation fetched per record. Fix it with eager loading (`with()`). Detection strips literals from the SQL, so `where user_id = 1` and `where user_id = 2` count as one shape.

Use `--statement=N` on any index from those groups to get the backtrace and find the origin.

## JSON output

All three read commands accept `--json`, which is easier to parse than the tables and preserves exact numbers:

```bash
php artisan debugbar:find --issues --json # array of requests, each with an `issues` list

php artisan debugbar:queries {id} --json # statements plus `duplicate_groups` and `n_plus_one_groups`

php artisan debugbar:get {id} --json # raw collector data (`--raw` is the same thing)

```

## Gotchas

- Always start with `debugbar:find --issues` rather than `debugbar:find` — the issue flags surface the most actionable requests immediately.
- The `{id}` is the request ID from the `debugbar:find` output, or use `latest` to inspect the most recent request.
- Collector availability depends on the app's debugbar config — the summary from `debugbar:get` shows which collectors have data.
- `--explain` and `--result` only work on SELECT queries. They re-execute against the current database, so results may differ from the original request.
- The `Dup` column only counts *exact* duplicates (same bindings). For N+1 read the "repeated query shapes" section instead — that is where a per-record lazy load shows up.
- `--explain` and `--result` only work on SELECT queries, and require `--statement=N`. They re-execute against the current database, so results may differ from the original request.
- Very large requests are truncated by the debugbar query limits (`debugbar.options.db.soft_limit` / `hard_limit`); an `info` statement in the output says so when it happens.
- `debugbar:clear` removes all stored data — use it to reset between debugging sessions, not mid-investigation.
2 changes: 1 addition & 1 deletion .agents/skills/laravel-best-practices/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Cross-cutting changes often need more than one rule file.
| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) |
| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) |
| Environment values and application configuration | [`rules/config.md`](rules/config.md) |
| Pest/PHPUnit patterns, factories, fakes | [`rules/testing.md`](rules/testing.md) |
| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill |
| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) |
| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) |

Expand Down
42 changes: 21 additions & 21 deletions .agents/skills/laravel-best-practices/rules/advanced-queries.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Advanced Query Patterns
# Advanced Query Best Practices

## Use `addSelect()` Subqueries for Single Values from Has-Many
## Select Single Relationship Values with Subqueries

Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
When only one value from a has-many relationship is needed, consider a correlated subquery with `addSelect()` instead of loading the entire relationship. This selects the value as part of the main query without an additional relationship query.

```php
public function scopeWithLastLoginAt($query): void
Expand All @@ -16,14 +16,14 @@ public function scopeWithLastLoginAt($query): void
}
```

## Create Dynamic Relationships via Subquery FK
## Create Dynamic Relationships with a Subquery Foreign Key

Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
The same pattern can select a foreign key and expose the selected model through a `belongsTo` relationship. Eager loading that relationship still executes a separate query, but it avoids loading the full has-many collection.

```php
public function lastLogin(): BelongsTo
{
return $this->belongsTo(Login::class);
return $this->belongsTo(Login::class, 'last_login_id');
}

public function scopeWithLastLogin($query): void
Expand All @@ -37,9 +37,9 @@ public function scopeWithLastLogin($query): void
}
```

## Use Conditional Aggregates Instead of Multiple Count Queries
## Combine Related Counts with Conditional Aggregates

Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
Combine several counts over the same filtered data set into one query by using conditional aggregates. Use `toBase()` when only scalar values are needed and model hydration provides no benefit. Confirm the expression syntax against the application's database engine.

```php
$statuses = Feature::toBase()
Expand All @@ -49,50 +49,50 @@ $statuses = Feature::toBase()
->first();
```

## Use `setRelation()` to Prevent Circular N+1
## Reuse Loaded Parent Models with `setRelation()`

When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
When a parent and its children are already loaded and code also accesses `$child->parent`, set the inverse relationship to the existing parent instance. This avoids an additional lazy-loading query for each child.

```php
$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);
```

## Prefer `whereIn` + Subquery Over `whereHas`
## Compare `whereHas()` with an `IN` Subquery

`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
`whereHas()` typically produces an `EXISTS` subquery, while `whereIn()` can express the same filter with an `IN` subquery. Either form may be faster depending on the database engine, indexes, cardinality, and query plan. Measure both forms with representative data; neither subquery loads its result set into PHP memory.

Incorrect (correlated EXISTS re-executes per row):
Option using `EXISTS`:

```php
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
```

Correct (index-friendly subquery, no PHP memory overhead):
Option using `IN`:

```php
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
```

## Sometimes Two Simple Queries Beat One Complex Query
## Measure Two Simple Queries Against One Complex Query

Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
Two targeted queries can outperform one complex correlated subquery or join when the first query is highly selective. They also add a database round trip, can transfer a large identifier list, and do not provide a single-query consistency snapshot. Decide from query plans and production-like measurements.

## Use Compound Indexes Matching `orderBy` Column Order
## Design Composite Indexes for the Query

When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
For common multi-column sorts, consider a composite index whose column order supports the query's filters and ordering. Database engines may combine indexes or choose an explicit sort, so matching the `ORDER BY` list alone does not guarantee that an index will be used. Verify the query plan.

```php
// Migration
$table->index(['last_name', 'first_name']);

// Query — column order must match the index
// Query that this index may support
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
```

## Use Correlated Subqueries for Has-Many Ordering
## Consider a Correlated Subquery for Has-Many Ordering

When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
When sorting by one value from a has-many relationship, a direct join can duplicate parent rows unless it first reduces the related table to one row per parent. A correlated subquery in `orderBy()` is often simpler, but its performance depends on the query plan and supporting indexes.

```php
public function scopeOrderByLastLogin($query): void
Expand Down
Loading
Loading