eloquent-best-practices
iserter/laravel-claude-agents
Master Laravel Eloquent ORM with query optimization, relationship management, and N+1 query prevention.
What is eloquent-best-practices?
This skill teaches best practices for Laravel Eloquent ORM to write efficient database queries and avoid common performance pitfalls. Use it when building Laravel applications to ensure your data access layer is optimized, maintainable, and scales well.
- Eager load relationships to eliminate N+1 query problems
- Select only needed columns to reduce data transfer
- Define and reuse query logic with scopes
- Safely handle mass assignment with fillable/guarded attributes
- Use type casts for automatic data conversion
- Process large datasets efficiently with chunking and lazy collections
How to install eloquent-best-practices
npx skills add https://github.com/iserter/laravel-claude-agents --skill eloquent-best-practicesHow to use eloquent-best-practices
- 1.Review the query optimization section to understand N+1 problems and eager loading patterns
- 2.Apply eager loading with `with()` and column selection to your existing queries
- 3.Define query scopes in your models for reusable filtering logic
- 4.Configure `$fillable` or `$guarded` on models to enable safe mass assignment
- 5.Add type casts to model properties for automatic data conversion
- 6.Use `withCount()` instead of counting in loops
- 7.Replace loop-based updates with database-level `update()` calls
- 8.Enable `preventLazyLoading()` in development to catch performance issues early
Use cases
- Optimizing a Laravel API endpoint that was making hundreds of queries due to N+1 problems
- Building a data export feature that processes millions of records without memory exhaustion
- Refactoring legacy code to use query scopes for consistent, reusable filtering logic
- Setting up database indexes and query patterns for a new feature to ensure it performs well from day one
- Debugging slow page loads by identifying and fixing eager-loading issues in relationships
- Laravel developers building web applications or APIs
- Backend engineers optimizing database performance
- Teams adopting Laravel and needing ORM best practices
- Developers maintaining legacy Laravel codebases
eloquent-best-practices FAQ
An N+1 query occurs when you load a collection (1 query) then loop through it making a query for each item (N queries). For example, loading all posts then querying each post's user in a loop. Use eager loading with `with()` to fetch relationships in a single query.
Always eager load relationships you know you'll need. Use `with()` to specify relationships upfront. Lazy loading (querying on-demand) is convenient but causes N+1 problems at scale. Enable `preventLazyLoading()` in development to catch accidental lazy loads.
Use `select(['id', 'name', 'email'])` to fetch only needed columns. With relationships, specify columns like `with(['user:id,name'])`. This reduces memory usage and database load, especially important for large result sets.
$fillable is a whitelist of attributes that can be mass-assigned; $guarded is a blacklist. Use $fillable for explicit control over what can be set via `create()` or `update()`. Never set $guarded to an empty array as it disables all protection.
Use `chunk(200, callback)` to process records in batches, or `lazy()` for lazy collections that load one record at a time. This prevents loading millions of records into memory at once.
Full instructions (SKILL.md)
Source of truth, from iserter/laravel-claude-agents.
name: eloquent-best-practices description: Best practices for Laravel Eloquent ORM including query optimization, relationship management, and avoiding common pitfalls like N+1 queries.
Eloquent Best Practices
Query Optimization
Always Eager Load Relationships
// ❌ N+1 Query Problem
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // N additional queries
}
// ✅ Eager Loading
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // No additional queries
}
Select Only Needed Columns
// ❌ Fetches all columns
$users = User::all();
// ✅ Only needed columns
$users = User::select(['id', 'name', 'email'])->get();
// ✅ With relationships
$posts = Post::with(['user:id,name'])->select(['id', 'title', 'user_id'])->get();
Use Query Scopes
// ✅ Define reusable query logic
class Post extends Model
{
public function scopePublished($query)
{
return $query->where('status', 'published')
->whereNotNull('published_at');
}
public function scopePopular($query, $threshold = 100)
{
return $query->where('views', '>', $threshold);
}
}
// Usage
$posts = Post::published()->popular()->get();
Relationship Best Practices
Define Return Types
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
Use withCount for Counts
// ❌ Triggers additional queries
foreach ($posts as $post) {
echo $post->comments()->count();
}
// ✅ Load counts efficiently
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Mass Assignment Protection
class Post extends Model
{
// ✅ Whitelist fillable attributes
protected $fillable = ['title', 'content', 'status'];
// Or blacklist guarded attributes
protected $guarded = ['id', 'user_id'];
// ❌ Never do this
// protected $guarded = [];
}
Use Casts for Type Safety
class Post extends Model
{
protected $casts = [
'published_at' => 'datetime',
'metadata' => 'array',
'is_featured' => 'boolean',
'views' => 'integer',
];
}
Chunking for Large Datasets
// ✅ Process in chunks to save memory
Post::chunk(200, function ($posts) {
foreach ($posts as $post) {
// Process each post
}
});
// ✅ Or use lazy collections
Post::lazy()->each(function ($post) {
// Process one at a time
});
Database-Level Operations
// ❌ Slow - loads into memory first
$posts = Post::where('status', 'draft')->get();
foreach ($posts as $post) {
$post->update(['status' => 'archived']);
}
// ✅ Fast - single query
Post::where('status', 'draft')->update(['status' => 'archived']);
// ✅ Increment/decrement
Post::where('id', $id)->increment('views');
Use Model Events Wisely
class Post extends Model
{
protected static function booted()
{
static::creating(function ($post) {
$post->slug = Str::slug($post->title);
});
static::deleting(function ($post) {
$post->comments()->delete();
});
}
}
Common Pitfalls to Avoid
Don't Query in Loops
// ❌ Bad
foreach ($userIds as $id) {
$user = User::find($id);
}
// ✅ Good
$users = User::whereIn('id', $userIds)->get();
Don't Forget Indexes
// Migration
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->index();
$table->string('slug')->unique();
$table->string('status')->index();
$table->timestamp('published_at')->nullable()->index();
// Composite index for common queries
$table->index(['status', 'published_at']);
});
Prevent Lazy Loading in Development
// In AppServiceProvider boot method
Model::preventLazyLoading(!app()->isProduction());
Checklist
- Relationships eagerly loaded where needed
- Only selecting required columns
- Using query scopes for reusability
- Mass assignment protection configured
- Appropriate casts defined
- Indexes on foreign keys and query columns
- Using database-level operations when possible
- Chunking for large datasets
- Model events used appropriately
- Lazy loading prevented in development
Related skills
More from iserter/laravel-claude-agents and the wider catalog.

gzh-design
Convert Markdown articles to WeChat Official Account HTML with auto-formatting, custom themes, and one-click styling.

fintech-algorithms
717 zero-dependency TypeScript algorithms for market data, trading, and quantitative analytics.

getnote-auth
Manage authentication for Get笔记 CLI

getnote-kb
Manage knowledge bases in Get笔记 via the getnote CLI

getnote-note
Manage notes in Get笔记 via the getnote CLI

getnote-search
Semantic search across notes in Get笔记 via the getnote CLI