Collection Call Optimization Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
collection-call-optimization | ⚡ Performance | High | 45 minutes |
What This Checks
Uses PHPStan/Larastan to detect inefficient collection operations that should be performed at the database query level instead of in-memory:
Model::all()->count()→ Should useModel::count()Model::all()->sum('column')→ Should useModel::sum('column')->get()->count()→ Should use->count()- Other collection aggregations that could be database queries
- Operations that unnecessarily load all records into memory
Detection Method: Leverages Larastan's built-in noUnnecessaryCollectionCall rule for accurate detection.
Why It Matters
- Database Performance: Loading entire result sets into memory when you only need aggregates wastes database resources
- Memory Usage:
Model::all()loads every record; for large tables this causes out-of-memory errors - Query Efficiency: Database aggregation functions (COUNT, SUM, AVG) are orders of magnitude faster than PHP loops
- Scalability: Code that works with 100 records fails catastrophically with 100,000 records
How to Fix
Quick Fix (5 minutes)
Replace collection aggregations with database queries:
Count Records:
php
// ❌ Before: Loads all records into memory
$userCount = User::all()->count();
// ✅ After: Database count
$userCount = User::count();Sum Values:
php
// ❌ Before: Loads all orders, sums in PHP
$totalRevenue = Order::all()->sum('amount');
// ✅ After: Database aggregation
$totalRevenue = Order::sum('amount');Average Values:
php
// ❌ Before: Loads all products, averages in PHP
$avgPrice = Product::all()->avg('price');
// ✅ After: Database aggregation
$avgPrice = Product::avg('price');Proper Fix (45 minutes)
1. Query Builder Optimization:
php
// ❌ Before: Two queries - get() loads data, count() counts in PHP
$activeUsers = User::where('status', 'active')->get()->count();
// ✅ After: Single database count
$activeUsers = User::where('status', 'active')->count();2. Complex Aggregations:
php
// ❌ Before: Loads all records, groups in PHP memory
$ordersByMonth = Order::all()->groupBy(function($order) {
return $order->created_at->format('Y-m');
});
// ✅ After: Database grouping
$ordersByMonth = Order::query()
->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as month, COUNT(*) as count')
->groupBy('month')
->get();3. Conditional Aggregation:
php
// ❌ Before: Loads all users, filters in PHP
$premiumUserCount = User::all()
->where('subscription_type', 'premium')
->count();
// ✅ After: Filter at database level
$premiumUserCount = User::where('subscription_type', 'premium')->count();4. When Collection Operations ARE Appropriate:
php
// ✅ Correct: When you need the actual collection for multiple operations
$activeUsers = User::where('status', 'active')->get();
$userCount = $activeUsers->count(); // Already loaded, no extra query
$avgAge = $activeUsers->avg('age'); // Computing from loaded data
$names = $activeUsers->pluck('name'); // Extracting from loaded data5. Pagination Caveat:
php
// ⚠️ Note: paginate() automatically uses count() internally
// This is correct, not a performance issue
$users = User::paginate(50); // Uses COUNT(*) for pagination automaticallyShieldCI Configuration
This analyzer is automatically skipped in CI environments ($runInCI = false).
Why skip in CI?
- PHPStan analysis can be slow on large codebases
- Prevents CI pipeline slowdowns from comprehensive code analysis
When to run this analyzer:
- ✅ Local development: Pre-commit checks during feature development
- ❌ CI/CD pipelines: Skipped automatically (can be slow)
References
- Laravel Collections
- Laravel Query Builder
- Laravel Eloquent Aggregates
- Larastan Documentation
- PHPStan Documentation
Related Analyzers
- MySQL Single Server Optimization Analyzer - Detects MySQL read/write configuration on single server