Reducing allocations — avoiding unnecessary object creation in hot paths
Concept
Lazy loading and lazy collections defer work until results are actually needed. Instead of loading all data upfront, lazy approaches load on demand — improving memory usage and potentially skipping work entirely for data that's never accessed.
PHP generators for lazy iteration: yield produces values one at a time. A generator function returns a Generator object that produces the next value only when asked. This is the fundamental lazy primitive in PHP — processing a million-row database result set with a generator uses constant memory, whereas ->get() loads all rows into memory.
Laravel Lazy Collections (LazyCollection): Wrap generators in a Collection-style API. LazyCollection::make(fn() => yield from generator()) creates a lazy chain. Operations like filter, map, take, skip, chunk are lazy — no data is loaded until you iterate. Converting to a regular Collection (->collect()) materializes it.
cursor() vs get() in Eloquent:
Model::all()/->get(): Loads ALL records into PHP memory as a Collection of Model instances.->cursor(): Returns aLazyCollectionthat fetches records one at a time via a generator. Memory: O(1) vs O(n).->chunk(1000): Loads 1000 records at a time. Good balance — less memory than->get(), simpler code than->cursor().->chunkById(1000): Likechunkbut usesWHERE id > last_idinstead ofLIMIT/OFFSET— avoids performance degradation on large offsets.
Lazy property initialization: PHP 8.4 lazy objects allow deferred construction of object properties until first access.
Code Example
<?php
declare(strict_types=1);
// Generator — lazy row-by-row processing
function readUsersFromDb(\PDO $pdo): \Generator
{
$stmt = $pdo->query("SELECT * FROM users ORDER BY id");
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
yield $row; // one row at a time — O(1) memory
}
}
foreach (readUsersFromDb($pdo) as $user) {
sendEmail($user); // constant memory regardless of user count
}
// Eloquent cursor — lazy collection over all users
User::cursor()->each(function(User $user) {
sendEmail($user);
});
// Memory: one User model at a time (+ query result set in driver buffer)
// LazyCollection API
use Illuminate\Support\LazyCollection;
$results = LazyCollection::make(function() {
foreach (file('/var/log/app.log') as $line) {
yield $line;
}
})
->filter(fn($line) => str_contains($line, 'ERROR'))
->map(fn($line) => trim($line))
->take(100) // stop after first 100 errors — lazy, reads only what it needs
->collect(); // materialize to a Collection
// chunkById — efficient large-table processing
User::where('active', true)
->chunkById(500, function(\Illuminate\Support\Collection $users) {
foreach ($users as $user) {
processUser($user);
}
});
// 500 rows per query, uses WHERE id > last_id to avoid offset slowdown
// Lazy vs eager — memory comparison
$allUsers = User::all(); // SELECT * — all 1M users in memory: ~2GB
$lazyUsers = User::cursor(); // lazy — processes one at a time: ~1MB