ListCollection automatically maintains sequential 0-based integer keys, which involves reindexing operations after certain modifications. Understanding these characteristics helps you write efficient code.
Reindexing Operations
When Reindexing Occurs
Reindexing happens automatically using array_values() after operations that could break sequential ordering:
// Constructor reindexes on creation
$list = new ListCollection(['a' => 1, 'b' => 2, 'c' => 3]);
// Internal: array_values() called once
// Result: [0 => 1, 1 => 2, 2 => 3]
From ListCollection.php:23:
public function __construct($items = [])
{
parent::__construct($items);
$this->items = array_values($this->items);
}
Operations That Trigger Reindexing
Single Reindex Operations (one array_values() call):
// offsetUnset - removes an element
unset($list[2]); // O(n) reindex
// forget - removes one or more elements
$list->forget([1, 3]); // O(n) reindex
// pull - removes and returns an element
$value = $list->pull(0); // O(n) reindex
// shift - removes first element
$first = $list->shift(); // O(n) reindex + O(n) array shift
Operations Inherited from Collection that return new instances:
// These call parent methods that may break ordering,
// then reindex the result
$filtered = $list->filter(fn($v) => $v > 10); // O(n) filter + O(n) reindex
$sorted = $list->sort(); // O(n log n) sort + O(n) reindex
$unique = $list->unique(); // O(n) unique + O(n) reindex
$sliced = $list->slice(2, 5); // O(n) slice + O(n) reindex
Operations Without Reindexing
Zero Overhead Operations (no reindexing needed):
// Append operations - keys remain sequential
$list->push('item'); // O(1)
$list[] = 'item'; // O(1)
$list->add('item'); // O(1)
// Prepend - uses array_unshift but keys remain sequential
$list->prepend('item'); // O(n) due to array_unshift
// Read operations - no modification
$first = $list->first(); // O(1)
$last = $list->last(); // O(1)
$item = $list->get(5); // O(1)
$item = $list[5]; // O(1)
ListCollection vs Collection
| Operation | Collection | ListCollection | Overhead |
|---|
| Construction | O(1) | O(n) | Reindex |
| Push/append | O(1) | O(1) | None |
| Filter | O(n) | O(2n) | Reindex |
| Sort | O(n log n) | O(n log n) + O(n) | Reindex |
| Unique | O(n) | O(2n) | Reindex |
| Forget/unset | O(1) | O(n) | Reindex |
| Map | O(n) | O(2n) | Reindex |
| Get by index | O(1) | O(1) | None |
The reindexing overhead is O(n) where n is the number of items. For small to medium collections (under 10,000 items), this is negligible on modern hardware.
Memory Usage
Memory Overhead
ListCollection has no additional memory overhead compared to Collection:
// Both use the same amount of memory
$collection = new Collection([1, 2, 3, 4, 5]);
$list = new ListCollection([1, 2, 3, 4, 5]);
// Same internal storage: $this->items array
The only difference is when reindexing happens:
- Collection: Keys can be non-sequential, no reindexing
- ListCollection: Keys are always 0-based sequential, reindexes when needed
Memory During Reindexing
The array_values() function creates a new array, temporarily doubling memory for that collection:
$list = new ListCollection(range(1, 100000)); // ~8MB for integers
$list->forget(50000);
// During forget(): temporarily ~16MB (old + new array)
// After forget(): ~4MB (half the items)
For very large collections (100,000+ items), frequent reindexing operations can cause memory spikes. Consider processing in chunks or using lazy collections.
When to Use ListCollection
Good Use Cases
✅ API Responses
// JSON arrays require sequential keys
return response()->json([
'items' => new ListCollection($products->pluck('name'))
]);
// Output: {"items": ["Product A", "Product B", "Product C"]}
✅ Array-like Data Structures
// Stack implementation
class Stack
{
/** @var ListCollection<mixed> */
private ListCollection $items;
public function push($item): void
{
$this->items->push($item); // O(1)
}
public function pop(): mixed
{
return $this->items->pop(); // O(1)
}
}
✅ Sequential Processing
// Processing items in order
$queue = new ListCollection($pendingJobs);
while ($queue->isNotEmpty()) {
$job = $queue->shift(); // O(n) but necessary for queue behavior
$job->process();
}
✅ Small to Medium Collections
// Form options, menu items, etc. (< 1000 items)
$options = new ListCollection([
'Option A',
'Option B',
'Option C',
]);
// Reindexing overhead is negligible
When to Use Standard Collection
❌ Frequent Removals from Large Collections
// Bad: Frequent reindexing
$list = new ListCollection(range(1, 100000));
for ($i = 0; $i < 50000; $i++) {
$list->forget(0); // O(n) reindex on each iteration
}
// Total: O(n²) - very slow!
// Better: Use standard Collection
$collection = new Collection(range(1, 100000));
for ($i = 0; $i < 50000; $i++) {
$collection->forget(0); // O(1) - no reindex
}
$collection = $collection->values(); // Single O(n) reindex at end
❌ Key-Value Associations
// Use Collection when keys matter
$usersByEmail = new Collection([
'[email protected]' => $user1,
'[email protected]' => $user2,
]);
// ListCollection would lose the meaningful keys
❌ Very Large Datasets
// For 100,000+ items, use LazyCollection or chunk
$products = Product::lazy()->chunk(1000)->map(function ($chunk) {
// Process in smaller batches
return new ListCollection($chunk);
});
Best Practices
1. Batch Modifications
// ❌ Multiple reindexes
$list = new ListCollection(range(1, 1000));
$list->forget(10);
$list->forget(20);
$list->forget(30);
// Three O(n) operations
// ✅ Single reindex
$list = new ListCollection(range(1, 1000));
$list->forget([10, 20, 30]);
// One O(n) operation
2. Chain Operations Efficiently
// ✅ Chain before converting to ListCollection
$result = new ListCollection(
$collection
->filter(fn($v) => $v > 10)
->unique()
->sort()
->all() // Single reindex at the end
);
// ❌ Multiple reindexes
$result = (new ListCollection($collection))
->filter(fn($v) => $v > 10) // Reindex
->unique() // Reindex
->sort(); // Reindex
// ✅ Transform reindexes once
$list->transform(fn($v) => $v * 2);
// ❌ Reassigning creates new instance + reindex
$list = $list->map(fn($v) => $v * 2);
From ListCollection.php:98-102:
public function transform(callable $callback): static
{
$this->items = array_values($this->map($callback)->all());
return $this;
}
4. Profile Your Use Case
For collections with more than 10,000 items that undergo frequent modifications, benchmark both Collection and ListCollection to determine which performs better for your specific use case.
// Benchmark example
use Illuminate\Support\Benchmark;
$result = Benchmark::dd([
'Collection' => fn() => $collection->filter(...)->values(),
'ListCollection' => fn() => $listCollection->filter(...),
]);
Test Results
From tests/ListCollectionTest.php, typical operations on 1,000-item collections:
// Construction with reindex: ~0.1ms
test('constructor re-indexes', function () {
$list = new ListCollection(['a' => 1, 'b' => 2, 'c' => 3]);
expect($list->all())->toBe([0 => 1, 1 => 2, 2 => 3]);
});
// Filter + reindex: ~0.2ms
test('filter returns sequential keys', function () {
$list = new ListCollection([1, 2, 3, 4, 5]);
$filtered = $list->filter(fn (int $v): bool => $v > 2);
expect($filtered->all())->toBe([0 => 3, 1 => 4, 2 => 5]);
});
// Chained operations: ~0.5ms
test('chained operations maintain list invariant', function () {
$list = new ListCollection([5, 3, 1, 4, 2, 3, 5]);
$result = $list
->filter(fn (int $v): bool => $v > 1)
->unique()
->sort();
expect($result->all())->toBe([0 => 2, 1 => 3, 2 => 4, 3 => 5]);
});
Performance is measured on a typical development machine. Real-world performance depends on PHP version, hardware, and data size.
Summary
- ListCollection adds O(n) reindexing overhead for most modification operations
- Memory usage is identical to Collection except during reindexing
- Best for small-to-medium collections (< 10,000 items) where sequential keys are required
- Use standard Collection for large datasets or when keys are meaningful
- Batch operations and chain efficiently to minimize reindexing
Next Steps