Native lazy objects, the ORM view

Open the Doctrine proxy folder of any project older than a year. A generated subclass for every entity, getters overridden, one file per class in the cache dir. It worked for fifteen years, and the seams always showed: final classes, public typed properties, instanceof surprises, a folder of code you pretend not to see. PHP 8.4 lazy objects were designed with exactly this in mind. The native way needs no generated class: ...

April 9, 2025 · 2 min · Murat Useinov

Lazy objects move into the engine

Fifty ghosts initialized in a loop are still fifty one queries. PHP 8.4 is out since Thursday. Property hooks get the headlines, but the feature I keep coming back to is native lazy objects. It legalizes a trick ORMs have done with generated code for fifteen years. The trick: you load an order, $order->customer should be a Customer, and you do not want a query until someone actually touches it. Doctrine generates a proxy class at build time, a subclass that overrides every method with “initialize first, then call parent”. It works. It is also a pile of magic: generated files, edge cases with final classes and private properties, strange things in var_dump. ...

November 23, 2024 · 2 min · Murat Useinov

N+1 hides on your laptop

Ten rows in the dev database, on the same machine, 0.1 ms per query. Ten extra queries is one millisecond. The page feels instant, the code ships. Production has a thousand rows and the database one network hop away. Round trip is about a millisecond even in a good datacenter. A thousand queries is a second of pure network waiting. Not slow SQL. Each query is fast. The plural is slow. ...

July 5, 2019 · 2 min · Murat Useinov

Eloquent observers and hidden control flow

Three thousand welcome emails. A colleague ran a user import on one project last month: loop over a CSV, $user->save(), go home. Next morning we found the script had also warmed the search index three thousand times and invalidated cache after every row. Nobody wrote that in the import script. The observers did. Laravel 5.8 came out yesterday, and reading the changelog brought that evening back, so here is the note. ...

February 27, 2019 · 2 min · Murat Useinov

Doctrine and the 100 000 row import

Row sixty thousand. Allowed memory size exhausted. The import script on one project died there, and the code was the obvious loop: read a row, persist() an entity, next row, flush() at the end. On a hundred test rows it worked perfectly. It was my loop. The reason is the Unit of Work. Doctrine keeps every managed entity in memory, plus a snapshot of its original data for change tracking. Persist a hundred thousand entities and you hold a hundred thousand objects twice. This is not a bug. It is the price of the ORM’s main feature, and on a normal web request the price is invisible because the request dies young. ...

June 19, 2016 · 2 min · Murat Useinov

Where Doctrine spends your milliseconds

$order->getCustomer()->getName() inside a foreach. Every time I open a slow Doctrine endpoint, I find some version of this line within ten minutes. The PHP is rarely slow. The shape of the data access is. Same few places every time. Lazy loading. The classic N+1. Doctrine proxies make it invisible: you touch the relation in a loop and every touch is a query. The profiler query count tells the truth. The code does not. ...

April 23, 2015 · 2 min · Murat Useinov

Kohana ORM and my first real N+1

Fifty one queries. That is what the SQL panel of the Kohana profiler showed for one catalog page. Fifty posts on the page. Nothing changed in the code. The page took two seconds because there was more data than in spring, and the code was written for spring. $post->author->name in a loop. One query for the list of posts, then one more for the author of every post, inside the foreach. The line looks innocent, and that is the problem: lazy loading hides the price. You write the relation, you go home early, and the bill comes in six months. ...

November 12, 2014 · 2 min · Murat Useinov