An LLM call is just IO

One method, response back. Laravel 13 makes calling a model feel like calling a mailer, and the demo level is now twenty minutes of work. Which is exactly when architecture starts to matter, because everybody will ship the demo. An LLM call is IO. Slow IO, expensive IO, IO that sometimes fails and sometimes lies. We know how to handle IO like that. We forget, because the response text looks smart and the API looks native to the framework. ...

March 11, 2026 · 2 min · Murat Useinov

Dependency upgrades without a big bang

A composer.lock that had not changed in three years. A colleague showed it to me last week, not as a confession, as a normal fact about the project. Nobody decided this. Every single upgrade looked small and risky, so everybody postponed it. Three years of postponed small risks equals one migration project with a budget and a name. The alternative is dull and it works. Upgrades as normal operations, like backups. ...

February 27, 2026 · 2 min · Murat Useinov

PHP 8.5 one month later

Last week of December, low traffic, good test suite. That is when we moved one project to PHP 8.5. Not brave, convenient. A month later I can sort the release into two piles. Pile one is the demo features. Every release has them. They look great in a tweet-sized example, then you search the codebase and find two places to use them. Fine. Not why you upgrade. Pile two is the boring changes. Less boilerplate in places you touch every day. Types that say what a function really returns. Small things that make PHPStan complain earlier instead of production complaining later. This pile is why you upgrade, and you only see it after some weeks, never in the release notes. ...

January 19, 2026 · 2 min · Murat Useinov

From Kohana to PHP 8.5

A Kohana ORM, an N+1 on a catalog page, and a blog post about it. That was November 2014. It is December 2025, the code around me runs PHP 8.5, and I went back and read that first post. The surface changed completely. A 2014 controller was a fat method: it read the request, validated by hand, called the ORM, formatted the response, sometimes sent an email on the way out. Types lived in docblocks and in hope. Deploy was files over FTP and a prayer. Today the same feature is a typed handler with constructor injection, validation at the boundary, a use case object, a message on a queue for the email, a trace id through the whole thing. The engine checks what a comment used to promise. I do not miss the old way. ...

December 11, 2025 · 2 min · Murat Useinov

The November upgrade train

PHP 8.5 came out yesterday. Symfony 7.4 LTS and 8.0 land at the end of the month, same as every year. November is upgrade month now. First the cargo, then the timetable. From 8.5 I care about three things. The pipe operator makes transformation chains read left to right: $slug = $title |> trim(...) |> strtolower(...) |> (fn ($s) => preg_replace('/\s+/', '-', $s)); The new URI extension puts standards-correct URL parsing in core. parse_url had opinions instead of a specification, I will not miss it. And #[\NoDiscard] lets a method declare that ignoring its return value is a bug. Made for result-style APIs, where a dropped return is a swallowed error. ...

November 21, 2025 · 2 min · Murat Useinov

Coding agents meet legacy PHP

A strange-looking condition in a 2015 module, “fixed” by an agent. All tests green, static analysis clean, and the condition was a business rule with history. My review caught it. Barely. That came out of an experiment I ran this month: a coding agent on one legacy PHP module, since agents can do multi-file changes now and every old codebase suddenly looks like a target. The setup matters more than the model. I did not ask “modernize this”. I gave a narrow migration goal on one bounded module: replace a deprecated API, add proper namespaces, keep behavior. The same shape of task I would give to Rector. The difference is that the agent also handles the irregular cases Rector cannot express, the places where people wrote creative code in 2015. ...

October 10, 2025 · 2 min · Murat Useinov

PostgreSQL 18, the upgrade notes

PostgreSQL 18 went GA yesterday. I ran the beta in July. Now it is not a preview, it is a ticket, and these are the notes from planning it. The quiet feature that matters most for the upgrade itself: pg_upgrade now carries planner statistics over. Before, the minutes or hours after switching versions were the scary part. Fresh cluster, empty statistics, the planner guessing, production limping until ANALYZE finished. That cliff is mostly gone. This alone changes how a major upgrade feels at 2 a.m. ...

September 26, 2025 · 2 min · Murat Useinov

vendor/autoload.php is not free

46 milliseconds. That is how long the bootstrap of one large application took before the framework even started, in a trace I pulled this week. A good share of it was autoloading. require vendor/autoload.php looks like a constant of nature. With plain PSR-4 rules it is a loop: every class load walks the prefixes and asks the filesystem whether a file exists. A few thousand classes on a cold request, and the loop becomes a number you can see in a flame graph. ...

August 20, 2025 · 2 min · Murat Useinov

A weekend with PostgreSQL 18 beta

A staging copy of one database, restored over the weekend onto PostgreSQL 18 beta. GA is expected in autumn. Two things I wanted to touch: asynchronous I/O and the built-in uuidv7(). Start with uuidv7, because it is about a mistake many of us already made, me included. Random UUIDv4 primary keys scatter inserts across the whole index. Every insert lands on a random page, the working set is the entire index, buffers churn. UUIDv7 is time-ordered: new rows go to the same few pages, like a sequence, but still globally unique. ...

July 2, 2025 · 2 min · Murat Useinov

One TTL does not fit the whole page

“Hello, Ivan” in the header. That is what broke the full-page cache on one project: either the whole page became uncacheable, or Ivan’s greeting was served to strangers. We managed both, in the same month. Full-page cache behind a reverse proxy is a great first step. It dies on the first personalized element. The way out is to stop thinking of the page as one object. It is regions with different speeds. The header with the user name changes per user. Navigation changes per deploy. The product list changes every few minutes. Prices and stock change every few seconds. Give all of that one TTL and you must pick the minimum, which means almost no cache at all. ...

June 16, 2025 · 2 min · Murat Useinov