<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on Murat Useinov</title>
    <link>https://useinov.com/posts/</link>
    <description>Recent content in Posts on Murat Useinov</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 01 Sep 2026 19:40:00 +0300</lastBuildDate>
    <atom:link href="https://useinov.com/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>A single server and a database is enough here</title>
      <link>https://useinov.com/posts/2026/single-server-is-enough/</link>
      <pubDate>Tue, 01 Sep 2026 19:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/single-server-is-enough/</guid>
      <description>&lt;p&gt;Ten years ago a system design conversation was short. A server, a monolith, Postgres, maybe a load balancer in front. For most products that was the entire architecture, and it held.&lt;/p&gt;
&lt;p&gt;Now the same conversation opens with API gateways and doesn&amp;rsquo;t stop: Kubernetes, service meshes, Kafka, Redis, three databases because each one is good at one thing, gRPC, GraphQL, Elasticsearch, Terraform, Prometheus, tracing, sharding, replication, autoscaling.&lt;/p&gt;
&lt;p&gt;The awkward part is that none of it is junk. Every one of those tools exists because someone hit a real wall: more users, more data, more teams, more regions, more nines of uptime.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Reading PHP 8.6 RFCs as constraints</title>
      <link>https://useinov.com/posts/2026/php-86-rfcs/</link>
      <pubDate>Mon, 17 Aug 2026 21:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/php-86-rfcs/</guid>
      <description>&lt;p&gt;Accepted, declined, discussion drama. That is how most posts about PHP 8.6 RFCs read, and the vote cycle is in full swing this month. I read RFCs for a different reason. Not &amp;ldquo;what do I get&amp;rdquo;, but &amp;ldquo;what will my current code look like next year&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;One RFC is a feature. Ten RFCs are a direction. When several push the same way, toward stricter types, explicit declarations, deprecating some old freedom, the direction tells you which of your habits are becoming legacy while you still type them. Syntax sugar is the least interesting part. Deprecations are the most interesting, because every deprecation is a migration with a countdown attached.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Testing PostgreSQL 19 beta without guessing</title>
      <link>https://useinov.com/posts/2026/testing-postgres-beta/</link>
      <pubDate>Wed, 08 Jul 2026 21:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/testing-postgres-beta/</guid>
      <description>&lt;p&gt;A production-like dump, a separate PostgreSQL 19 beta instance, one evening plus machine time. That is the whole method, and it is the only thing a database beta is for: finding out where the new version changes the behavior of your application while the change is still a bug report and not your incident.&lt;/p&gt;
&lt;p&gt;Restore the dump first. Data size matters. A plan that is fine on a thousand rows goes bad on fifty million.&lt;/p&gt;</description>
    </item>
    <item>
      <title>When version numbers stop talking</title>
      <link>https://useinov.com/posts/2026/calendar-versions/</link>
      <pubDate>Thu, 11 Jun 2026 22:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/calendar-versions/</guid>
      <description>&lt;p&gt;Major means breakage possible, minor means safe. For twenty years a MySQL version number carried that promise, and you could plan by reading it. With the move to calendar-style releases the number tells you one thing: when it was built. The promise moved elsewhere, and you have to know where to look.&lt;/p&gt;
&lt;p&gt;The signals now are the channel and the dates. Is this an LTS or an innovation release. When does support end. What do the compatibility notes actually say, because &amp;ldquo;no major version&amp;rdquo; does not mean &amp;ldquo;no behavior change&amp;rdquo;. Optimizer changes ship in any release. Your query plans do not read version numbers.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A worker is not a controller</title>
      <link>https://useinov.com/posts/2026/worker-not-controller/</link>
      <pubDate>Mon, 25 May 2026 21:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/worker-not-controller/</guid>
      <description>&lt;p&gt;All the logic inside &lt;code&gt;execute()&lt;/code&gt;, exit code always zero, no lifecycle thinking at all. A controller without a request. I still see this console command in every second codebase. Symfony stopped being only an HTTP framework a long time ago, but habits are slower than releases, and with 8.1 giving console and workers first-class attention the excuse is gone.&lt;/p&gt;
&lt;p&gt;An HTTP request lives for milliseconds. The kernel builds services, handles, throws everything away. A worker lives for hours. Same container, very different lifecycle. Every service that quietly keeps state, an in-memory cache, an accumulating buffer, an entity manager full of tracked objects, is invisible in HTTP and becomes a memory leak in a worker. If your consumer needs a nightly restart by cron, this is where the night went.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL 9.7 LTS is an operational choice</title>
      <link>https://useinov.com/posts/2026/mysql-97-lts/</link>
      <pubDate>Fri, 10 Apr 2026 20:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/mysql-97-lts/</guid>
      <description>&lt;p&gt;An SQL mode set in 2019 that nobody remembers why. An authentication plugin the driver happened to support. A charset and collation nobody chose. That is what a MySQL upgrade is made of. MySQL 9.7 LTS is out, so I am making the list again.&lt;/p&gt;
&lt;p&gt;An LTS release of a database is a date more than a feature list. The date until which somebody else worries about patches, and the date after which the worry is yours. For a working PHP project the question is not &amp;ldquo;what is new&amp;rdquo;. It is &amp;ldquo;what did we assume&amp;rdquo;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>An LLM call is just IO</title>
      <link>https://useinov.com/posts/2026/llm-call-is-io/</link>
      <pubDate>Wed, 11 Mar 2026 20:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/llm-call-is-io/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Dependency upgrades without a big bang</title>
      <link>https://useinov.com/posts/2026/small-dependency-upgrades/</link>
      <pubDate>Fri, 27 Feb 2026 19:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/small-dependency-upgrades/</guid>
      <description>&lt;p&gt;A &lt;code&gt;composer.lock&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;The alternative is dull and it works. Upgrades as normal operations, like backups.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.5 one month later</title>
      <link>https://useinov.com/posts/2026/php-85-month-later/</link>
      <pubDate>Mon, 19 Jan 2026 21:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/php-85-month-later/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>From Kohana to PHP 8.5</title>
      <link>https://useinov.com/posts/2025/eleven-years-later/</link>
      <pubDate>Thu, 11 Dec 2025 21:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/eleven-years-later/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>The November upgrade train</title>
      <link>https://useinov.com/posts/2025/november-upgrade-train/</link>
      <pubDate>Fri, 21 Nov 2025 22:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/november-upgrade-train/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;From 8.5 I care about three things. The pipe operator makes transformation chains read left to right:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$slug&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$title&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;trim&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;strtolower&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;preg_replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/\s+/&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The new URI extension puts standards-correct URL parsing in core. &lt;code&gt;parse_url&lt;/code&gt; had opinions instead of a specification, I will not miss it. And &lt;code&gt;#[\NoDiscard]&lt;/code&gt; 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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Coding agents meet legacy PHP</title>
      <link>https://useinov.com/posts/2025/agents-and-legacy/</link>
      <pubDate>Fri, 10 Oct 2025 21:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/agents-and-legacy/</guid>
      <description>&lt;p&gt;A strange-looking condition in a 2015 module, &amp;ldquo;fixed&amp;rdquo; by an agent. All tests green, static analysis clean, and the condition was a business rule with history. My review caught it. Barely.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The setup matters more than the model. I did not ask &amp;ldquo;modernize this&amp;rdquo;. 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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 18, the upgrade notes</title>
      <link>https://useinov.com/posts/2025/postgres-18-upgrade/</link>
      <pubDate>Fri, 26 Sep 2025 22:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/postgres-18-upgrade/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The quiet feature that matters most for the upgrade itself: &lt;code&gt;pg_upgrade&lt;/code&gt; 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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>vendor/autoload.php is not free</title>
      <link>https://useinov.com/posts/2025/autoload-not-free/</link>
      <pubDate>Wed, 20 Aug 2025 20:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/autoload-not-free/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;require vendor/autoload.php&lt;/code&gt; 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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A weekend with PostgreSQL 18 beta</title>
      <link>https://useinov.com/posts/2025/postgres-18-beta/</link>
      <pubDate>Wed, 02 Jul 2025 18:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/postgres-18-beta/</guid>
      <description>&lt;p&gt;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 &lt;code&gt;uuidv7()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>One TTL does not fit the whole page</title>
      <link>https://useinov.com/posts/2025/one-ttl-lie/</link>
      <pubDate>Mon, 16 Jun 2025 21:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/one-ttl-lie/</guid>
      <description>&lt;p&gt;&amp;ldquo;Hello, Ivan&amp;rdquo; in the header. That is what broke the full-page cache on one project: either the whole page became uncacheable, or Ivan&amp;rsquo;s greeting was served to strangers. We managed both, in the same month.&lt;/p&gt;
&lt;p&gt;Full-page cache behind a reverse proxy is a great first step. It dies on the first personalized element.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 7.3 and the code I get to delete</title>
      <link>https://useinov.com/posts/2025/delete-your-helpers/</link>
      <pubDate>Fri, 30 May 2025 15:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/delete-your-helpers/</guid>
      <description>&lt;p&gt;Symfony 7.3 was released this week. I opened the release notes with a text file next to them. The file is a list of things to delete.&lt;/p&gt;
&lt;p&gt;Every project older than three years carries a layer of custom helpers that were reasonable when written. The framework moves, the helpers stay. Nobody reviews them, because they work.&lt;/p&gt;
&lt;p&gt;Two examples from one project.&lt;/p&gt;
&lt;p&gt;A base console command class with sugar for arguments and options, written around 2021. Symfony 7.3 has invokable commands, arguments come as parameters with attributes:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Native lazy objects, the ORM view</title>
      <link>https://useinov.com/posts/2025/doctrine-lazy-objects/</link>
      <pubDate>Wed, 09 Apr 2025 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/doctrine-lazy-objects/</guid>
      <description>&lt;p&gt;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, &lt;code&gt;instanceof&lt;/code&gt; surprises, a folder of code you pretend not to see.&lt;/p&gt;
&lt;p&gt;PHP 8.4 lazy objects were designed with exactly this in mind. The native way needs no generated class:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Observability has a budget</title>
      <link>https://useinov.com/posts/2025/observability-budget/</link>
      <pubDate>Mon, 10 Mar 2025 22:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/observability-budget/</guid>
      <description>&lt;p&gt;Last year we turned tracing on everywhere. This year the telemetry invoice is a line item a manager asks about by name. The dashboards did not get more useful. More data, same confusion at 2 a.m.&lt;/p&gt;
&lt;p&gt;Time to design instead of collect.&lt;/p&gt;
&lt;p&gt;The design starts from the questions I actually ask when the pager goes off. In practice there are four: is the latency of key endpoints normal, did the error rate jump, how far behind are the queues, is the database saturated. That is the core set. Metrics for these must be cheap, always on, with alerts. Everything else can be sampled traces I pull up on demand.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 12 is a boring release</title>
      <link>https://useinov.com/posts/2025/laravel-12-boring/</link>
      <pubDate>Wed, 26 Feb 2025 23:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/laravel-12-boring/</guid>
      <description>&lt;p&gt;Laravel 12 came out on Monday. Tuesday evening I upgraded one mid-size project. The whole diff is composer constraints and the lock file. Tests green, deployed before midnight.&lt;/p&gt;
&lt;p&gt;People in chats are disappointed. Where are the big features. A major that mostly refreshes dependencies and starter kits, is it even a major.&lt;/p&gt;
&lt;p&gt;I remember 4.2 to 5.0. New folder structure, new config system, half of the packages dead on arrival. We planned it as a separate project, because it was one. Skip a major, and the next one cost double.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.4 after the hype</title>
      <link>https://useinov.com/posts/2025/php84-in-production/</link>
      <pubDate>Wed, 15 Jan 2025 15:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/php84-in-production/</guid>
      <description>&lt;p&gt;A private field, a getter, a setter with one line of normalization. Three members for one idea, repeated through every entity folder I have ever opened. PHP 8.4 is two months old, the conference demos are done, and the honest question is which features earn a place in a working codebase.&lt;/p&gt;
&lt;p&gt;My filter is simple. A feature is good when it deletes code.&lt;/p&gt;
&lt;p&gt;Property hooks pass:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;User&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$email&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;strtolower&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;trim&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The class is shorter and the normalization cannot be bypassed. But keep hooks boring. A hook that talks to the database is a magic getter from 2008 in new syntax.&lt;/p&gt;</description>
    </item>
    <item>
      <title>The fastest request never reaches PHP</title>
      <link>https://useinov.com/posts/2024/http-caching/</link>
      <pubDate>Tue, 03 Dec 2024 12:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/http-caching/</guid>
      <description>&lt;p&gt;One header, and the FPM load graph fell off a cliff.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Cache-Control: public, s-maxage=60
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A public catalog page on one project renders the same HTML for every anonymous visitor. Same queries, same JSON from the search service, same template, thousands of times per hour. We profiled it, we tuned it, and only then asked the obvious question: why is PHP involved in the second request at all.&lt;/p&gt;
&lt;p&gt;HTTP had the answer before my career started. &lt;code&gt;public&lt;/code&gt; says a shared cache may store the response. &lt;code&gt;s-maxage&lt;/code&gt; gives the CDN or reverse proxy its own lifetime, separate from browser &lt;code&gt;max-age&lt;/code&gt;. With Varnish in front, the request path splits in two. MISS: full stack, FPM, database, sixty milliseconds. HIT: the proxy answers from memory, the PHP process never starts, the database never hears about it. Nothing in the application got faster. There was simply less application running.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Lazy objects move into the engine</title>
      <link>https://useinov.com/posts/2024/lazy-objects/</link>
      <pubDate>Sat, 23 Nov 2024 12:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/lazy-objects/</guid>
      <description>&lt;p&gt;Fifty ghosts initialized in a loop are still fifty one queries.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The trick: you load an order, &lt;code&gt;$order-&amp;gt;customer&lt;/code&gt; 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 &amp;ldquo;initialize first, then call parent&amp;rdquo;. It works. It is also a pile of magic: generated files, edge cases with final classes and private properties, strange things in var_dump.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Property hooks, before the hype settles</title>
      <link>https://useinov.com/posts/2024/property-hooks/</link>
      <pubDate>Mon, 14 Oct 2024 20:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/property-hooks/</guid>
      <description>&lt;p&gt;Six lines of getter and setter ceremony around one &lt;code&gt;strtolower&lt;/code&gt;. PHP 8.4 is in release candidates, and property hooks delete exactly that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Customer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$email&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;strtolower&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;trim&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assignment stays assignment, &lt;code&gt;$customer-&amp;gt;email = $input&lt;/code&gt;, and normalization happens on the way in. For this exact case, one value, no dependencies, I am convinced. The property remains a property.&lt;/p&gt;
&lt;p&gt;Where I get careful is everything past normalization. A setter method is ugly, but it announces itself. &lt;code&gt;setEmail()&lt;/code&gt; in a call stack tells you code ran. &lt;code&gt;$obj-&amp;gt;email = $x&lt;/code&gt; looks free, and with hooks it is not. Put validation that throws into a hook, and every plain assignment becomes a possible exception, invisible at the call site. Put a side effect in, and you have built magic the next person discovers through a debugger at midnight. We spent years removing &lt;code&gt;__get&lt;/code&gt; and &lt;code&gt;__set&lt;/code&gt; from codebases for this exact crime. Hooks are their respectable cousins, better typed, analyzable, and the temptation is identical.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 17 and why VACUUM is an API concern</title>
      <link>https://useinov.com/posts/2024/postgres-vacuum/</link>
      <pubDate>Fri, 27 Sep 2024 15:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/postgres-vacuum/</guid>
      <description>&lt;p&gt;A reporting transaction open for hours, and &lt;code&gt;n_dead_tup&lt;/code&gt; on the hot table climbing all day while autovacuum ran non-stop and changed nothing.&lt;/p&gt;
&lt;p&gt;That was one project. PostgreSQL 17 came out yesterday, and the headline work is exactly this plumbing: vacuum got a new memory structure for tracking dead tuples, far less memory and no longer capped the old way, plus better throughput for high-concurrency writes and bulk loads. Maintenance internals. Which is why backend people should care.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Pick the library with a profiler, not a chart</title>
      <link>https://useinov.com/posts/2024/profiler-not-benchmark/</link>
      <pubDate>Wed, 07 Aug 2024 20:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/profiler-not-benchmark/</guid>
      <description>&lt;p&gt;Two hundred thousand ops per second against eighty. That was the bar chart, and the team was ready to pick a serializer from it. I asked for one day.&lt;/p&gt;
&lt;p&gt;The day went like this.&lt;/p&gt;
&lt;p&gt;Take a representative workload. Not a synthetic three-field object, but a hundred real payloads from production logs, with the nested structures, the nullable mess, the one field that is sometimes a list and sometimes a map. Anonymize them, save as fixtures. This step is the whole method. The bar chart was measured on someone else&amp;rsquo;s data with someone else&amp;rsquo;s configuration, and a serializer is exactly the kind of code whose cost depends on the shape of the input.&lt;/p&gt;</description>
    </item>
    <item>
      <title>WebSockets change your operational model</title>
      <link>https://useinov.com/posts/2024/reverb-operations/</link>
      <pubDate>Wed, 03 Jul 2024 23:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/reverb-operations/</guid>
      <description>&lt;p&gt;Every client reconnects at once. That is what a deploy means now on one project of mine, since Reverb made WebSockets a first-party Laravel feature this spring and order updates go to the browser instead of polling. The code was the easy part.&lt;/p&gt;
&lt;p&gt;A classic PHP app is stateless between requests. An FPM worker takes a request, answers, forgets. Capacity planning is requests per second. A WebSocket server is the opposite animal: thousands of open connections that mostly do nothing, but each one holds memory and a file descriptor, and each one is state that dies with the process. Your quiet realtime feature has a thundering herd built in. Plan for reconnect storms, raise descriptor limits, and make the client reconnect with jitter, never on a fixed timer.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A faster sprintf will not fix your p99</title>
      <link>https://useinov.com/posts/2024/microbenchmark-trap/</link>
      <pubDate>Tue, 18 Jun 2024 23:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/microbenchmark-trap/</guid>
      <description>&lt;p&gt;Three times faster. That is the number in the benchmark posts about PHP 8.4, where simple &lt;code&gt;sprintf()&lt;/code&gt; calls with basic placeholders get turned into plain string operations at compile time. The function call disappears.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1000000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$s&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%s-%d&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$prefix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I like this work. I do not like how people read it.&lt;/p&gt;
&lt;p&gt;A microbenchmark measures one function in a loop, alone, with a warm cache and nothing else to do. Now profile a real request from the same application. In every backend I have profiled the picture has the same shape: most of the wall time is waiting. SQL queries, Redis round trips, an HTTP call to some partner API. The CPU spent inside PHP is a modest slice, and &lt;code&gt;sprintf&lt;/code&gt; inside that slice is a rounding error. Three times faster rounding error is still a rounding error.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 7.1 and the value of boring releases</title>
      <link>https://useinov.com/posts/2024/symfony-seven-one/</link>
      <pubDate>Fri, 31 May 2024 12:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/symfony-seven-one/</guid>
      <description>&lt;p&gt;&lt;code&gt;composer update&lt;/code&gt;, the changelog over coffee, two deprecation notices fixed, done before lunch. That was Symfony 7.1 on one project this morning. Nothing in the release will change your architecture, and I mean this as a compliment.&lt;/p&gt;
&lt;p&gt;There are nice small things. An attribute that maps an uploaded file straight into a controller argument, one more piece of request plumbing gone. A new experimental component for type introspection that libraries will quietly benefit from. A layer of deprecations preparing the next major. That is the whole show.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL 8.4 gives us a place to stand</title>
      <link>https://useinov.com/posts/2024/mysql-lts/</link>
      <pubDate>Tue, 30 Apr 2024 21:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/mysql-lts/</guid>
      <description>&lt;p&gt;Half the industry is sitting on MySQL 5.7 right now, unsupported since last autumn, and calling it stability.&lt;/p&gt;
&lt;p&gt;MySQL 8.4 came out today, and the important word in the announcement is not a feature. It is LTS. First long-term release of the new model: innovation releases every quarter for the brave, an LTS every couple of years for production, bugfixes only after that.&lt;/p&gt;
&lt;p&gt;People underestimate how much pain the old model caused. 8.0 was a rolling target. A &amp;ldquo;minor&amp;rdquo; 8.0.x could change optimizer behavior or deprecate something you used. Staying current meant re-testing, staying behind meant missing security fixes.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 11 removes files, not decisions</title>
      <link>https://useinov.com/posts/2024/laravel-eleven/</link>
      <pubDate>Mon, 25 Mar 2024 23:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/laravel-eleven/</guid>
      <description>&lt;p&gt;The diff of a Laravel 11 upgrade on one mid-size API this week is mostly deletions. Almost empty &lt;code&gt;app/&lt;/code&gt;, configuration in one fluent bootstrap file, the slim skeleton I wrote about in January now real. It feels great.&lt;/p&gt;
&lt;p&gt;Now look at what survived untouched. The use case class that wraps an order state change in a transaction. The listener that must fire only after commit, because it queues an email about a row that must exist. The cache invalidation that follows every write to a heavily read table. The decision that validation lives in a request class and business rules one layer deeper. None of this is skeleton. None of it got smaller.&lt;/p&gt;</description>
    </item>
    <item>
      <title>One trace through HTTP, queue and database</title>
      <link>https://useinov.com/posts/2024/one-trace/</link>
      <pubDate>Thu, 08 Feb 2024 19:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/one-trace/</guid>
      <description>&lt;p&gt;A request creates a job. The job calls an external API and writes to the database. Something in this chain is slow, and the logs tell four disconnected stories.&lt;/p&gt;
&lt;p&gt;Grepping a request id across services is our folklore. OpenTelemetry finally makes the boring standard version practical in PHP.&lt;/p&gt;
&lt;p&gt;The core is small. One trace id for the whole causal chain, a span id for every operation, and one rule: pass the context along. Over HTTP it is a single header:&lt;/p&gt;</description>
    </item>
    <item>
      <title>The shrinking Laravel skeleton</title>
      <link>https://useinov.com/posts/2024/laravel-slim-skeleton/</link>
      <pubDate>Sat, 06 Jan 2024 22:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/laravel-slim-skeleton/</guid>
      <description>&lt;p&gt;No &lt;code&gt;app/Http/Kernel.php&lt;/code&gt;. That is the first thing you notice in the Laravel 11 preview, and the first thing every comment thread asks: where did my middleware go.&lt;/p&gt;
&lt;p&gt;The skeleton is the loudest change this quarter, louder than any feature. An &lt;code&gt;app/&lt;/code&gt; directory that is almost empty, no folder of middleware stubs, a pile of service providers collapsed into one, config files trimmed.&lt;/p&gt;
&lt;p&gt;The middleware did not go anywhere. It moved into the framework. The mechanism exists exactly as before, only the file does not, and &lt;code&gt;bootstrap/app.php&lt;/code&gt; gets a small fluent API to reconfigure the stack when you actually need to.&lt;/p&gt;</description>
    </item>
    <item>
      <title>AI assistants do not know your production</title>
      <link>https://useinov.com/posts/2023/ai-assistants-limits/</link>
      <pubDate>Sat, 23 Dec 2023 10:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/ai-assistants-limits/</guid>
      <description>&lt;p&gt;A DTO with a dozen fields, a framework migration, a slow query incident. Three tasks from the last few months, all done with Copilot and ChatGPT open. Here is where the line runs for me.&lt;/p&gt;
&lt;p&gt;Task one, boilerplate. The DTO, its validation rules, a test with fixtures. The assistant writes this faster than I type, and the result needs only a quick read. Same for converting an array-shaped legacy structure into typed classes. This is honest time saved, maybe an hour a day. The code is the kind where being generic is correct.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.3 and Symfony 7 in one month</title>
      <link>https://useinov.com/posts/2023/november-baselines/</link>
      <pubDate>Thu, 30 Nov 2023 10:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/november-baselines/</guid>
      <description>&lt;p&gt;&lt;code&gt;public const int TIMEOUT_SECONDS = 30;&lt;/code&gt; One week old, and the one thing I waited for from PHP 8.3.&lt;/p&gt;
&lt;p&gt;Busy November. 8.3 released last week, and yesterday Symfony shipped 6.4 LTS and 7.0 on the same day. Time to plan, not to jump.&lt;/p&gt;
&lt;p&gt;Typed class constants are a small thing, but constants were the last untyped island in a class, and a child class could silently redefine one to a different type. Also &lt;code&gt;json_validate()&lt;/code&gt;, which checks JSON syntax without building the whole tree in memory. Everyone had a helper that did &lt;code&gt;json_decode&lt;/code&gt; and threw the result away. Now the helper can retire.&lt;/p&gt;</description>
    </item>
    <item>
      <title>FrankenPHP: the server absorbs PHP</title>
      <link>https://useinov.com/posts/2023/frankenphp-first-look/</link>
      <pubDate>Fri, 27 Oct 2023 18:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/frankenphp-first-look/</guid>
      <description>&lt;p&gt;One binary, my code, a Caddyfile. That is the whole deployment of a small internal service I moved to FrankenPHP in staging this week.&lt;/p&gt;
&lt;p&gt;The classic PHP deployment is a couple: Nginx in front, PHP-FPM behind, FastCGI between them. Two configs, two processes to supervise, one socket to misconfigure. It works, we all know it by heart, and knowing it by heart is the only reason we call it simple.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 16, read through one workload</title>
      <link>https://useinov.com/posts/2023/postgres-sixteen/</link>
      <pubDate>Sat, 16 Sep 2023 20:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/postgres-sixteen/</guid>
      <description>&lt;p&gt;A nightly import, a few hundred million rows through &lt;code&gt;COPY&lt;/code&gt;, then reporting queries in the morning. That is the only workload I care about, so that is how I read the PostgreSQL 16 release notes: with one pipeline in mind and everything else skipped.&lt;/p&gt;
&lt;p&gt;Three items survived the filter.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY&lt;/code&gt; got faster, noticeably when several loads run at once. Our import is exactly this shape, parallel COPY streams into partitioned tables. I will take the free speed. But the old lesson stays: if your import still does row-by-row INSERT from PHP, no Postgres release will save you. Batch into COPY first, then talk about versions.&lt;/p&gt;</description>
    </item>
    <item>
      <title>UUID primary keys and the B-tree bill</title>
      <link>https://useinov.com/posts/2023/uuid-primary-keys/</link>
      <pubDate>Thu, 03 Aug 2023 21:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/uuid-primary-keys/</guid>
      <description>&lt;p&gt;The same table, the same rows, about 1.5 times the disk. That was the measurement after someone made UUIDv4 the primary key in InnoDB, and that is before counting secondary indexes.&lt;/p&gt;
&lt;p&gt;Every new API wants UUIDs. Distributed generation, no coordination, nothing leaks about row counts. All true. Then the database quietly starts paying.&lt;/p&gt;
&lt;p&gt;InnoDB clusters the table by primary key. Auto-increment inserts always land in the rightmost page, warm and predictable. Random UUIDs land anywhere in the tree: page splits everywhere, half-empty pages, and the set of hot pages becomes the whole index. Secondary indexes store the primary key in every entry, so each of them inherits the full 16 bytes too. In PostgreSQL the heap does not cluster, so the pain is smaller, but index bloat and lost locality are still there.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL now has two speeds</title>
      <link>https://useinov.com/posts/2023/mysql-release-tracks/</link>
      <pubDate>Thu, 20 Jul 2023 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/mysql-release-tracks/</guid>
      <description>&lt;p&gt;MySQL 8.1 came out this week, and its support window is measured in months. That is by design. Oracle changed the release model: two tracks now. Innovation releases every quarter or so, 8.1 is the first, short support, new features fast. And an LTS line with years of support, promised for next year. 8.0 stays on bugfixes until its end of life in 2026.&lt;/p&gt;
&lt;p&gt;For years the question was &amp;ldquo;when do we finally move to 8.0&amp;rdquo;. Now the question has a second dimension: which track are you even on. That is a product decision, and it is worth making explicitly instead of by inertia.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Outbox: events that do not lie</title>
      <link>https://useinov.com/posts/2023/transactional-outbox/</link>
      <pubDate>Wed, 07 Jun 2023 20:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/transactional-outbox/</guid>
      <description>&lt;p&gt;An email confirming an order that does not exist. A colleague showed me this one last week, together with the mirror bug: an order that exists and never got its email.&lt;/p&gt;
&lt;p&gt;The code was the obvious kind. Commit the Doctrine transaction, then dispatch to the AMQP transport. Two systems, two writes, no shared transaction. Publish after commit and the process can die between them, event lost. Publish inside the transaction and the broker gets an event for data that may still roll back. Both orders are wrong, and under real load this fires weekly.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Scheduler component, or cron as messages</title>
      <link>https://useinov.com/posts/2023/symfony-scheduler/</link>
      <pubDate>Wed, 31 May 2023 20:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/symfony-scheduler/</guid>
      <description>&lt;p&gt;&lt;code&gt;crontab -l&lt;/code&gt; under a login nobody remembers creating. That is where the real business logic of one server lived: a nightly cleanup, an export, a retry script.&lt;/p&gt;
&lt;p&gt;Symfony 6.3 came out yesterday with a Scheduler component. Periodic tasks defined in PHP, executed as Messenger messages. Experimental, but the idea deserves a note.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;#[&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;AsSchedule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;default&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;final&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;MainSchedule&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;implements&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ScheduleProviderInterface&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;getSchedule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Schedule&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Schedule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nx&#34;&gt;RecurringMessage&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;every&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;10 minutes&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;CleanupExpiredCarts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then &lt;code&gt;messenger:consume scheduler_default&lt;/code&gt;, and a worker fires the messages.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Persistent PHP workers need discipline</title>
      <link>https://useinov.com/posts/2023/persistent-worker-discipline/</link>
      <pubDate>Mon, 03 Apr 2023 15:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/persistent-worker-discipline/</guid>
      <description>&lt;p&gt;A static array with the comment &amp;ldquo;cache, cleared per request&amp;rdquo;. It went to production on RoadRunner last month, and the memory graph turned into a staircase.&lt;/p&gt;
&lt;p&gt;PHP had one superpower nobody advertised: the request died. Every leak, every forgotten static, every open transaction was erased when the process shut down. Shared-nothing was not architecture. It was amnesia, and amnesia forgave us everything.&lt;/p&gt;
&lt;p&gt;RoadRunner and Swoole take it away. The worker lives for thousands of requests, and the class of bugs changes on the first day. That &amp;ldquo;per request&amp;rdquo; cache became a cache per worker lifetime, and the supervisor killed the process when it ran out of memory. A logger kept the request id in a property, so entries from user B carried the id of user A. And the best one: an exception in the middle of a Doctrine transaction left the connection with the transaction open, and the next request on that worker silently joined it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>DI attributes: config moves into the class</title>
      <link>https://useinov.com/posts/2023/symfony-di-attributes/</link>
      <pubDate>Mon, 27 Mar 2023 23:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/symfony-di-attributes/</guid>
      <description>&lt;p&gt;Three lines of YAML in a file two directories away, for one integer that one service reads. That was the first thing I converted when I started moving a project&amp;rsquo;s service config from YAML into attributes, to see where the line is.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;fm&#34;&gt;__construct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;#[&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;Autowire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%env(int:IMPORT_BATCH_SIZE)%&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$batchSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the class tells you everything about itself. Same with &lt;code&gt;#[TaggedIterator]&lt;/code&gt; for collecting all implementations of an interface, and &lt;code&gt;#[AsDecorator]&lt;/code&gt; for wrapping a service. These are local facts. A local fact belongs next to the code it describes. When I open the class, I want to stop searching.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 10 is a quiet major</title>
      <link>https://useinov.com/posts/2023/laravel-ten-quiet/</link>
      <pubDate>Thu, 16 Feb 2023 20:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/laravel-ten-quiet/</guid>
      <description>&lt;p&gt;&lt;code&gt;Process::run(&#39;gzip -k dump.sql&#39;)&lt;/code&gt;. Out of the whole Laravel 10 release this week, that is the line I will actually use.&lt;/p&gt;
&lt;p&gt;The rest is quiet. Skeleton and framework code got native type declarations instead of docblocks, PHP 8.1 is the floor, and there is the Process facade. That is roughly the whole story. Some people are disappointed. I am not. A major release that is mostly maintenance means the framework is an adult.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Before Laravel 10: raise the baseline first</title>
      <link>https://useinov.com/posts/2023/laravel-upgrade-baseline/</link>
      <pubDate>Sat, 07 Jan 2023 22:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/laravel-upgrade-baseline/</guid>
      <description>&lt;p&gt;&lt;code&gt;composer why-not php 8.1&lt;/code&gt;. One command, and it tells you who holds you back. Run it before making any plans.&lt;/p&gt;
&lt;p&gt;Laravel 10 comes next month and requires PHP 8.1. One project I help with is on Laravel 9 and PHP 8.0. The temptation is one heroic branch: new PHP, new framework, new package versions. When that branch breaks in production, you will not know which of the three changes broke it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.2: stricter by default</title>
      <link>https://useinov.com/posts/2022/php-eight-two/</link>
      <pubDate>Fri, 16 Dec 2022 21:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/php-eight-two/</guid>
      <description>&lt;p&gt;PHP 8.2 was released on December 8. My checklist for it looks like every other December, which is the whole point of a yearly cadence.&lt;/p&gt;
&lt;p&gt;On the surface a quiet release. Readonly classes, DNF types, standalone &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;null&lt;/code&gt; types, constants in traits. Underneath, the more important half: another round of cleaning old dynamic behavior. Dynamic properties deprecated, &lt;code&gt;${var}&lt;/code&gt; string interpolation deprecated, &lt;code&gt;utf8_encode&lt;/code&gt; deprecated. The language keeps trading looseness for predictability, and I keep voting for the trade.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Readonly classes for value objects</title>
      <link>https://useinov.com/posts/2022/readonly-classes/</link>
      <pubDate>Tue, 08 Nov 2022 22:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/readonly-classes/</guid>
      <description>&lt;p&gt;Two &lt;code&gt;readonly&lt;/code&gt; keywords for two properties in a two-property class. That is what an immutable &lt;code&gt;Money&lt;/code&gt; looks like in PHP 8.1:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;final&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Money&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;fm&#34;&gt;__construct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;readonly&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;readonly&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$currency&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PHP 8.2 is at release candidate stage, and the feature I am waiting for is this one. The keyword moves up and says it once:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;final&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;readonly&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Money&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;fm&#34;&gt;__construct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$currency&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every property is readonly, and the class refuses dynamic properties on top. Cosmetics, yes. But cosmetics that make the right thing the short thing, and that changes what people actually write.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MERGE arrives in PostgreSQL 15</title>
      <link>https://useinov.com/posts/2022/postgres-merge/</link>
      <pubDate>Thu, 27 Oct 2022 20:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/postgres-merge/</guid>
      <description>&lt;p&gt;A supplier price list, loaded into &lt;code&gt;staging_prices&lt;/code&gt;, then reconciled against &lt;code&gt;prices&lt;/code&gt;. Since PostgreSQL 15 came out two weeks ago, that is one statement:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;MERGE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INTO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;prices&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;USING&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;staging_prices&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sku&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sku&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;MATCHED&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AND&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;IS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;THEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DELETE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;MATCHED&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AND&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;THEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;updated_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;MATCHED&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;THEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INSERT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sku&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;VALUES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sku&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before 15 this was three statements in a transaction, or a stored procedure, or a loop in PHP. Now it is one statement that says what it does. Update changed rows, delete withdrawn ones, insert new ones. People coming from Oracle and SQL Server waited a decade for this. The conditional &lt;code&gt;WHEN MATCHED AND ...&lt;/code&gt; branches are the real value. &lt;code&gt;ON CONFLICT&lt;/code&gt; cannot express &amp;ldquo;delete when the source says so&amp;rdquo; at all.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Dynamic properties are leaving</title>
      <link>https://useinov.com/posts/2022/dynamic-properties-deprecated/</link>
      <pubDate>Sat, 03 Sep 2022 20:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/dynamic-properties-deprecated/</guid>
      <description>&lt;p&gt;&lt;code&gt;$invoice-&amp;gt;totall = 250;&lt;/code&gt; One extra letter. No error, no warning. A second property appears, the real &lt;code&gt;total&lt;/code&gt; stays null, and the bug surfaces three screens later as &amp;ldquo;total is empty sometimes&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I have hunted this exact bug. Twice. Both times it took hours, because the write looks correct and the read looks correct. They just disagree on one letter.&lt;/p&gt;
&lt;p&gt;PHP 8.2 lands in December, and the deprecation that will touch the most code is this one. Writing to an undeclared property becomes deprecated. In some future major it becomes an error. For fifteen years it was legal:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Fast query, slow endpoint</title>
      <link>https://useinov.com/posts/2022/hydration-cost/</link>
      <pubDate>Tue, 23 Aug 2022 15:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/hydration-cost/</guid>
      <description>&lt;p&gt;The profiler said the query takes 20 ms. The export endpoint took 900 ms and a quarter of a gigabyte of memory. The missing 880 ms was Doctrine turning five thousand rows into five thousand entities.&lt;/p&gt;
&lt;p&gt;Hydration is not free. For every row the ORM builds an object, fills properties through reflection, registers it in the unit of work, creates proxies for relations. Per row it is nothing. Times five thousand, it is the endpoint. The profiler shows SQL because SQL is easy to show.&lt;/p&gt;</description>
    </item>
    <item>
      <title>JSONB or a normal table</title>
      <link>https://useinov.com/posts/2022/jsonb-or-columns/</link>
      <pubDate>Sat, 16 Jul 2022 10:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/jsonb-or-columns/</guid>
      <description>&lt;p&gt;Audit snapshots, product attributes, and a document with two hot keys. Three cases from one Postgres project, one question for each: column or JSONB.&lt;/p&gt;
&lt;p&gt;I answer it by looking at queries, not at data.&lt;/p&gt;
&lt;p&gt;Audit snapshots. When an order changes, we store the whole previous state. Nobody queries inside it. It is read as a blob, whole, rarely, by a human during an incident. Perfect JSONB. Normalizing it would mean a dozen tables for data nobody joins.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Argument resolvers and the HTTP border</title>
      <link>https://useinov.com/posts/2022/argument-resolver-dto/</link>
      <pubDate>Mon, 13 Jun 2022 20:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/argument-resolver-dto/</guid>
      <description>&lt;p&gt;Forty lines of &lt;code&gt;$request-&amp;gt;get()&lt;/code&gt; before the first line of real logic. That was the controller nobody wanted to touch, and the Symfony 6.1 upgrade was my excuse to open it.&lt;/p&gt;
&lt;p&gt;An action that reads ten request fields by hand does two jobs. It translates HTTP into data, and it runs the use case. The first job is boring and repeated in every action, which is exactly why it should not be written by hand ten times. Symfony has argument resolvers for this since 3.1. People just do not use them for their own types.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Redis 7 functions, same old rule</title>
      <link>https://useinov.com/posts/2022/redis-seven-functions/</link>
      <pubDate>Thu, 05 May 2022 20:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/redis-seven-functions/</guid>
      <description>&lt;p&gt;&lt;code&gt;FCALL update_if_newer 1 user:42 17&lt;/code&gt;. One round trip, a name instead of a SHA, and it survives a restart. Redis 7 came out last week, and Functions are the feature I care about. Everything EVAL scripts should have been.&lt;/p&gt;
&lt;p&gt;The problem they solve is real. Take compare-and-update: set a value only if the stored version is older. From PHP it is three round trips and a race between them. WATCH/MULTI works but is ugly and retries under contention. A Lua script does it atomically in one trip, and that is why we all wrote Lua scripts. But scripts are anonymous blobs. The application carries the source, sends it on every deploy, and debugging starts with &amp;ldquo;which SHA is this&amp;rdquo;. Functions fix the operational part: the library lives in Redis, has a name, has a version.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Retry is a strategy, not a loop</title>
      <link>https://useinov.com/posts/2022/messenger-retry-strategy/</link>
      <pubDate>Wed, 27 Apr 2022 20:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/messenger-retry-strategy/</guid>
      <description>&lt;p&gt;A failed job that retries every second, one hundred times, against a payment provider that is already down. That is a small DDoS with a queue in front of it. If the provider needs five minutes to recover, hammering it for the first two achieves nothing except log volume.&lt;/p&gt;
&lt;p&gt;Symfony Messenger has everything needed. You just have to configure it on purpose:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;framework&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;messenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;transports&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;async&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;dsn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%env(MESSENGER_TRANSPORT_DSN)%&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;retry_strategy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;max_retries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;delay&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2000&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;multiplier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;failure_transport&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;failed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two seconds, eight, thirty two, and so on. Exponential backoff gives the external service room to breathe. I also add jitter in a custom retry strategy, because a hundred messages that failed together will otherwise retry together, in one synchronized wave. Same failure, five times.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Backfill in batches, not in one UPDATE</title>
      <link>https://useinov.com/posts/2022/batched-backfill/</link>
      <pubDate>Thu, 17 Mar 2022 22:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/batched-backfill/</guid>
      <description>&lt;p&gt;Sixty million rows in &lt;code&gt;orders&lt;/code&gt;, a new nullable column &lt;code&gt;total_cents&lt;/code&gt; from expand-and-contract, and a Friday afternoon. The question is how to fill it.&lt;/p&gt;
&lt;p&gt;The naive answer is one statement. &lt;code&gt;UPDATE orders SET total_cents = ROUND(total * 100)&lt;/code&gt;. On a test database it works. On production it holds row locks on the whole table for the whole run, writes one giant chunk of WAL, and the replica falls minutes behind. On MySQL the binlog gets the same present. One project I worked on learned this on that Friday. Since then I backfill only in batches.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 9 and the boring major</title>
      <link>https://useinov.com/posts/2022/laravel-nine-cadence/</link>
      <pubDate>Fri, 25 Feb 2022 12:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/laravel-nine-cadence/</guid>
      <description>&lt;p&gt;One evening. That was the Laravel 9 upgrade on a mid-size project last week. Most of the diff was &lt;code&gt;composer.json&lt;/code&gt; and a few config files.&lt;/p&gt;
&lt;p&gt;Laravel 9 is out this month. Symfony 6 components under the hood, PHP 8.0 minimum, Flysystem 3. And from now on, one major per year. The upgrade guide is short, and the &amp;ldquo;high impact&amp;rdquo; section is shorter. This is a policy: small majors, often.&lt;/p&gt;
&lt;p&gt;I want to defend this policy, because I remember the other kind. A project that sat on an old major for three years, because &amp;ldquo;we will upgrade when there is time&amp;rdquo;. There is never time. Then the version goes EOL, a security fix forces the jump, and you pay three years of drift in one horrible month. Packages you depend on drop support one by one, and suddenly it is not a framework upgrade anymore. It is archaeology.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Order status is an enum now</title>
      <link>https://useinov.com/posts/2022/enums-for-status/</link>
      <pubDate>Sat, 08 Jan 2022 23:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/enums-for-status/</guid>
      <description>&lt;p&gt;One row in &lt;code&gt;orders&lt;/code&gt; had status &lt;code&gt;&#39;canceled&#39;&lt;/code&gt;, with one l. Nobody knows how long it sat there. The enum found it on the first day.&lt;/p&gt;
&lt;p&gt;PHP 8.1 enums are six weeks old, enough time in one project to say something practical. The first candidate was obvious: order status. For years it was a class with string constants, &lt;code&gt;Order::STATUS_PAID&lt;/code&gt;, &lt;code&gt;Order::STATUS_SHIPPED&lt;/code&gt;. Constants look safe, but nothing stops a function from receiving &lt;code&gt;&#39;payed&#39;&lt;/code&gt;. The parameter type was &lt;code&gt;string&lt;/code&gt;, and &lt;code&gt;string&lt;/code&gt; accepts everything.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 6.0 is boring, take the compliment</title>
      <link>https://useinov.com/posts/2021/symfony6-via-54/</link>
      <pubDate>Thu, 09 Dec 2021 22:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/symfony6-via-54/</guid>
      <description>&lt;p&gt;The deprecation counter in the profiler, three digits. That is where one project stands this week, and that number is the whole Symfony 6 migration.&lt;/p&gt;
&lt;p&gt;Symfony shipped 5.4 and 6.0 together at the end of November, same code, same day. 4.4 and 5.0 worked exactly like this two years ago, so the story is rehearsed. 6.0 is 5.4 with the deprecated code deleted. You never migrate to a major. You migrate to the last minor, and the major is a formality.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.1: enums, readonly and a word about Fibers</title>
      <link>https://useinov.com/posts/2021/php81-enums/</link>
      <pubDate>Fri, 26 Nov 2021 20:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/php81-enums/</guid>
      <description>&lt;p&gt;&lt;code&gt;&#39;payed&#39;&lt;/code&gt;. One letter, half a day of debugging, a few years ago. The string walked straight through &lt;code&gt;OrderStatus::isValid()&lt;/code&gt;, because someone had added it to the constants list &amp;ldquo;for compatibility&amp;rdquo;. Every project I touched has that class: a bag of string constants, an &lt;code&gt;isValid()&lt;/code&gt; helper, and a prayer. The type system never knew these strings were special.&lt;/p&gt;
&lt;p&gt;PHP 8.1 came out yesterday. Point release on paper, and it brings the feature I wanted since forever. Enums.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Generics we already have</title>
      <link>https://useinov.com/posts/2021/phpstan-generics/</link>
      <pubDate>Sat, 09 Oct 2021 20:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/phpstan-generics/</guid>
      <description>&lt;p&gt;A template calls &lt;code&gt;getEmail()&lt;/code&gt; on an Order at three in the morning. The method returned &lt;code&gt;array&lt;/code&gt;, the docblock said &amp;ldquo;array of User&amp;rdquo;, and eight months later someone put an Order in it. Everything was fine until it was not.&lt;/p&gt;
&lt;p&gt;PHP still has no generics and will not get them soon. Meanwhile PHPStan and Psalm shipped them anyway, in docblocks. Checked at analysis, not at runtime. For everyday backend work that turns out to be most of the value.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 14 and the price of a connection</title>
      <link>https://useinov.com/posts/2021/postgres-14-connections/</link>
      <pubDate>Thu, 30 Sep 2021 10:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/postgres-14-connections/</guid>
      <description>&lt;p&gt;A few hundred connections, mostly idle, and the whole server gets slower. That was Postgres for as long as I remember it: a process per connection, and past some count the snapshot bookkeeping starts to eat everything.&lt;/p&gt;
&lt;p&gt;PostgreSQL 14 is released today. I had the RC on a test box for a week, so a few notes from a backend seat. The headline for me is that this internal work got a serious rewrite. On our connection-heavy profile, many workers, short queries, lots of idle time, the difference is visible without squinting. Idle connections finally cost close to what people always assumed they cost.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Redis Streams is a log, not a better queue</title>
      <link>https://useinov.com/posts/2021/redis-streams-log/</link>
      <pubDate>Fri, 20 Aug 2021 22:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/redis-streams-log/</guid>
      <description>&lt;p&gt;&amp;ldquo;Should we move the queue to Streams? They are the proper way now.&amp;rdquo; A colleague, this week, about our Redis list queue. Wrong question. Streams are not a newer version of the same thing.&lt;/p&gt;
&lt;p&gt;A list queue is a hand-off. LPUSH on one side, BRPOP on the other, and once a consumer takes the message, it is gone. Crash after the pop and the message died with you, unless you built the pending-list dance yourself. No history, no second reader. For &amp;ldquo;send this email eventually&amp;rdquo; it is honestly enough.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nobody closes your database connection anymore</title>
      <link>https://useinov.com/posts/2021/long-lived-connections/</link>
      <pubDate>Tue, 20 Jul 2021 23:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/long-lived-connections/</guid>
      <description>&lt;p&gt;&amp;ldquo;MySQL server has gone away&amp;rdquo;, first thing in the morning, on a worker that had no traffic all night. That was week one after we moved a project from FPM to RoadRunner.&lt;/p&gt;
&lt;p&gt;For all these years FPM managed our database connections. Not with clever code. By dying. Process ends, connection closes, transaction rolls back, session variables reset. Nobody thought about connection lifecycle, because there was none.&lt;/p&gt;
&lt;p&gt;Move the same code to RoadRunner or Swoole workers and the topic is back. A connection lives for hours now. Two things go wrong, and both showed up within a week.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Attributes in Symfony 5.3</title>
      <link>https://useinov.com/posts/2021/symfony-attributes/</link>
      <pubDate>Fri, 04 Jun 2021 21:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/symfony-attributes/</guid>
      <description>&lt;p&gt;&lt;code&gt;#[Route(&#39;/orders/{id}&#39;)]&lt;/code&gt; right above the method, and nothing in &lt;code&gt;config/routes&lt;/code&gt;. Symfony 5.3 came out last week, and with PHP 8 attributes configuration finally found its place. Routes, autowiring hints, some validation, native syntax next to the code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;#[&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;Route&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/orders/{id}&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;methods&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;show&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nd&#34;&gt;Response&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I spent years defending YAML routing. The argument was separation: code is code, wiring is wiring, one file shows the whole URL map. The argument was never wrong. It lost to practice. In every real project the first thing you do with a route is jump to the controller, and the first thing you do with a controller is wonder which route hits it. Two files, one mental join, forever. Attributes remove the join. Rename a method, the metadata moves with it. Delete the class, no orphaned YAML block stays behind to confuse the next person.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Octane and state that outlives the request</title>
      <link>https://useinov.com/posts/2021/octane-shared-state/</link>
      <pubDate>Wed, 05 May 2021 22:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/octane-shared-state/</guid>
      <description>&lt;p&gt;One customer sees another customer&amp;rsquo;s cart. No crash, no error in the log. That is the bug Laravel Octane brings to an old codebase, and the benchmarks are the least interesting part of the story.&lt;/p&gt;
&lt;p&gt;Octane is out in beta, Swoole and RoadRunner became first-class overnight. In FPM every request gets a fresh application. Boot, handle, die. The model forgives everything. Under Octane the framework boots once and workers reuse it. Bootstrap cost drops to near zero, hence the pretty numbers. But every singleton is now shared between requests, and between users.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony Runtime: front controller as a callable</title>
      <link>https://useinov.com/posts/2021/symfony-runtime/</link>
      <pubDate>Fri, 16 Apr 2021 23:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/symfony-runtime/</guid>
      <description>&lt;p&gt;&lt;code&gt;public/index.php&lt;/code&gt;, unchanged since I learned Symfony. Create the request from globals, run the kernel, send the response, terminate. Symfony 5.3 arrives next month, and the Runtime component rewrites this file. It looks like a small refactoring. It is a statement about where PHP is going.&lt;/p&gt;
&lt;p&gt;Baked into those few lines is one big assumption: one process, one request, then we die. FPM made the assumption true for fifteen years, so nobody saw it as an assumption.&lt;/p&gt;</description>
    </item>
    <item>
      <title>The outbox table</title>
      <link>https://useinov.com/posts/2021/outbox-table/</link>
      <pubDate>Tue, 23 Mar 2021 15:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/outbox-table/</guid>
      <description>&lt;p&gt;About once a month an order existed and nobody heard about it. The order service saved the order, then published &lt;code&gt;order.created&lt;/code&gt; to RabbitMQ, and once a month the process died between the two. Rare enough to be mysterious. Frequent enough to ruin a weekend.&lt;/p&gt;
&lt;p&gt;Two operations, two systems, no common transaction. Whatever order you do them in, you lose. Commit first, then publish: the order exists, the event never leaves. Publish first, then commit: the commit fails, consumers are already busy with an order that does not exist. We had the first variant in production.&lt;/p&gt;</description>
    </item>
    <item>
      <title>TTL is not an invalidation strategy</title>
      <link>https://useinov.com/posts/2021/ttl-not-invalidation/</link>
      <pubDate>Wed, 24 Feb 2021 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/ttl-not-invalidation/</guid>
      <description>&lt;p&gt;Support ticket: a user changed his name, the site still shows the old one. The profile sits in Redis with TTL one hour. Fine, we set five minutes. A week later the hit rate drops and MySQL feels it. This seesaw has no good position. TTL is insurance, not invalidation.&lt;/p&gt;
&lt;p&gt;The obvious upgrade is cache-aside with explicit delete. Read: try the cache, miss, load from the database, write to the cache. Write: update the row, delete the key. Looks correct. It has a race.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8 in production: dependencies first</title>
      <link>https://useinov.com/posts/2021/php8-dependencies-first/</link>
      <pubDate>Fri, 08 Jan 2021 23:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/php8-dependencies-first/</guid>
      <description>&lt;p&gt;&lt;code&gt;composer why-not php 8.0&lt;/code&gt;. That is the first command of the migration, and it has nothing to do with syntax.&lt;/p&gt;
&lt;p&gt;PHP 8.0 is six weeks old. Twitter is full of match expressions and constructor promotion. Meanwhile the real upgrade of a working project is a dependency problem. Your code is maybe twenty percent of what runs in production. The rest is the framework, thirty packages and a dozen extensions, and any one of them can be the blocker.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Attributes: metadata moves in with the code</title>
      <link>https://useinov.com/posts/2020/attributes-near-code/</link>
      <pubDate>Fri, 18 Dec 2020 12:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/attributes-near-code/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;#[&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;Route&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/orders/{id}&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;methods&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;show&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nd&#34;&gt;Response&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Symfony 5.2 accepts this on PHP 8, and the annotation era quietly starts to end. Same shape as the annotation, but now it is language, not a comment. The engine parses it, static analysis sees it, a typo is a compile-time complaint instead of a route that silently does not exist. Doctrine and the validator are heading the same way. Everything that lived in docblocks will move over the next year or two.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.0: ignore the JIT, take the rest</title>
      <link>https://useinov.com/posts/2020/php8-new-baseline/</link>
      <pubDate>Fri, 27 Nov 2020 21:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/php8-new-baseline/</guid>
      <description>&lt;p&gt;JIT on, JIT off, the same Symfony endpoint. Difference within noise. Same for a Laravel endpoint. PHP 8.0 came out yesterday and this was the first thing I checked, because every headline is about the JIT.&lt;/p&gt;
&lt;p&gt;Not a scandal. A web request spends its time in I/O, in MySQL, in framework code full of method calls the JIT cannot do much with. JIT is for tight numeric loops, and your controller has none. If you compute fractals in PHP, congratulations. The rest of us can leave it off.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Stop building running totals in PHP</title>
      <link>https://useinov.com/posts/2020/window-functions-mysql/</link>
      <pubDate>Tue, 06 Oct 2020 15:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/window-functions-mysql/</guid>
      <description>&lt;p&gt;Two hundred thousand rows over the wire to compute forty numbers. That was our monthly report on MySQL 5.7: fetch all transactions for the period, loop in PHP, accumulate a running balance, compare each row with the previous one, rank customers by volume. Classic 5.7 shape, because the database could not say &amp;ldquo;previous row&amp;rdquo; or &amp;ldquo;rank within group&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;We are finally moving that project to MySQL 8. Two years after GA, which by database standards is reckless haste. The first win had nothing to do with performance. We deleted PHP.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 8: the interesting parts are not on the marketing page</title>
      <link>https://useinov.com/posts/2020/laravel-8-maturity/</link>
      <pubDate>Fri, 11 Sep 2020 23:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/laravel-8-maturity/</guid>
      <description>&lt;p&gt;&lt;code&gt;-&amp;gt;refundedTwice()&lt;/code&gt;. That is the line from Laravel 8 I care about, and it is nowhere on the release page. Jetstream gets the screenshots. Class-based factories and the queue changes get my attention, because both are about production.&lt;/p&gt;
&lt;p&gt;Factories used to be closures registered through a global function. Now they are classes with states:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;Order&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;factory&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;paid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;has&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;OrderItem&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;factory&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;create&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test data quality decides test quality. Most integration suites on our project test the happy path fifty times, because the default factory returns a fresh, valid, boring record. The bugs live somewhere else. An order refunded twice. A user registered before the migration added the column. A subscription that expired in the middle of renewal. A factory state gives such a monster a name, and once the name exists, people write tests with it. Named ugly data is the cheapest test improvement I know.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Average response time explains nothing</title>
      <link>https://useinov.com/posts/2020/average-explains-nothing/</link>
      <pubDate>Tue, 04 Aug 2020 20:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/average-explains-nothing/</guid>
      <description>&lt;p&gt;Average response time: 180 ms. Ticket from support: &amp;ldquo;the API is slow&amp;rdquo;. Both true. The average is a diplomat. It offends nobody and tells you nothing.&lt;/p&gt;
&lt;p&gt;Latency is a distribution. Our 180 ms hides a p50 of 90 ms and a p99 above four seconds. One request in a hundred is terrible, and with thirty requests per page load, most users hit that unlucky one regularly. The people complaining are not imagining things. They live in the tail, and the average never visits there.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8 will retire some of our workarounds</title>
      <link>https://useinov.com/posts/2020/php8-old-patterns/</link>
      <pubDate>Fri, 03 Jul 2020 22:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/php8-old-patterns/</guid>
      <description>&lt;p&gt;&lt;code&gt;@param int|string $id&lt;/code&gt;. I grepped one service this morning: dozens of those, and every one is a wish. The docblock says what the signature could not. First alpha of PHP 8 came out last week, and native union types turn the wish into a contract, checked at runtime and by tooling. Half of my phpdoc can go.&lt;/p&gt;
&lt;p&gt;Everyone writes about the JIT. I keep thinking about the less shiny thing: how many of our daily patterns exist only to compensate for a missing language feature, and are about to become legacy.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Types are for reading, not for the compiler</title>
      <link>https://useinov.com/posts/2020/typed-code-navigation/</link>
      <pubDate>Sat, 27 Jun 2020 22:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/typed-code-navigation/</guid>
      <description>&lt;p&gt;&lt;code&gt;OrderRepository&lt;/code&gt; in a constructor. Click. I am there. That is how I explore a big Symfony project now, and I noticed my main reason for types has changed. Not bug catching anymore. Navigation.&lt;/p&gt;
&lt;p&gt;Symfony 5.1 is out, PHP 7.4 is everywhere I work, and the IDE knows every caller and every implementation. Compare with the array-passing style we all wrote for years:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;register&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// what is in $data? read three call sites to find out
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;versus&lt;/p&gt;</description>
    </item>
    <item>
      <title>A deadlock is the database doing its job</title>
      <link>https://useinov.com/posts/2020/deadlock-not-bug/</link>
      <pubDate>Sat, 02 May 2020 20:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/deadlock-not-bug/</guid>
      <description>&lt;p&gt;Error 1213 in the logs, and the chat lights up: the database is broken. It is not. Two transactions locked rows in opposite order, A waits for B, B waits for A, and InnoDB did the only sane thing: picked a victim and killed it. The cycle is gone. This is a feature.&lt;/p&gt;
&lt;p&gt;The classic shape is a money transfer. One request moves funds from account 1 to account 2, another from 2 to 1, both update the first account and then the second. Opposite order, instant cycle under load. Tests never show it, because tests do not run two of these in the same millisecond.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Waiting for three APIs, one at a time</title>
      <link>https://useinov.com/posts/2020/parallel-http-requests/</link>
      <pubDate>Tue, 21 Apr 2020 21:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/parallel-http-requests/</guid>
      <description>&lt;p&gt;Three external calls on one page: prices, stock, delivery estimate. Each answers in about 300 ms. The page waits a full second, because we call them one after another. PHP is synchronous, what can you do.&lt;/p&gt;
&lt;p&gt;Turns out, something. Symfony HttpClient is lazy. &lt;code&gt;request()&lt;/code&gt; sends and returns immediately. The waiting happens when you read the response. So start all three, read later:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$prices&lt;/span&gt;   &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$client&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$pricesUrl&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$stock&lt;/span&gt;    &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$client&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$stockUrl&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$delivery&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$client&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$deliveryUrl&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;prices&amp;#39;&lt;/span&gt;   &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$prices&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;toArray&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;stock&amp;#39;&lt;/span&gt;    &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$stock&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;toArray&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;delivery&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$delivery&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;toArray&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Under the hood it is curl multi. Three requests fly at once, wall time is the slowest one instead of the sum. Our second became 350 ms. No swoole, no reactphp, no async rewrite. Same boring controller.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Sanctum and choosing how much auth you need</title>
      <link>https://useinov.com/posts/2020/sanctum-auth-level/</link>
      <pubDate>Thu, 26 Mar 2020 23:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/sanctum-auth-level/</guid>
      <description>&lt;p&gt;Airlock lived for about a week. Laravel 7 shipped it this month, a trademark scare followed, and it became Sanctum. Fast rename, same idea. And the idea is good, because it names a problem people solve badly.&lt;/p&gt;
&lt;p&gt;The problem: your own SPA needs to talk to your own API. For years the reflex was OAuth2. Install Passport, stand up an authorization server, issue JWTs to a frontend on the same domain as the backend. All that machinery to authenticate first-party code against itself. OAuth is a delegation protocol, it lets a third party act for a user. When there is no third party, you are running a passport office for your own family.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A Redis lock is a promise you cannot fully keep</title>
      <link>https://useinov.com/posts/2020/redis-lock-honesty/</link>
      <pubDate>Wed, 26 Feb 2020 21:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/redis-lock-honesty/</guid>
      <description>&lt;p&gt;Two workers, one order, processed twice. Every project gets this day. Someone writes &lt;code&gt;SETNX&lt;/code&gt;, calls it a distributed lock, closes the ticket. I want to slow down here, because the ticket is not closed.&lt;/p&gt;
&lt;p&gt;The small bugs first. A lock needs a TTL, or a crashed worker holds it forever. A lock needs an owner token, or worker A releases the lock of worker B:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$token&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;bin2hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;random_bytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$ok&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$redis&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;lock:order:&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$orderId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$token&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;nx&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;ex&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the release must be atomic: compare the token and delete in one Lua script. Check in PHP, delete in a second command, and there is a gap. Something will land in that gap.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Cursor pagination is part of your API contract</title>
      <link>https://useinov.com/posts/2020/cursor-pagination-contract/</link>
      <pubDate>Wed, 08 Jan 2020 21:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/cursor-pagination-contract/</guid>
      <description>&lt;p&gt;Someone inserts a row while the client walks the pages, and the whole window shifts. Page two shows an item the client already saw on page one. Or an item falls between pages and the client never sees it. For a feed this is annoying. For an export or a sync endpoint this is a data loss bug that nobody can reproduce.&lt;/p&gt;
&lt;p&gt;I used to think keyset pagination is a performance trick. Deep OFFSET is slow, keyset is fast, end of story. Now I think the performance part is the boring half. Offset pagination over a changing dataset lies to the client. That is the interesting half.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Static state outlives the job</title>
      <link>https://useinov.com/posts/2019/worker-static-state/</link>
      <pubDate>Sun, 22 Dec 2019 23:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/worker-static-state/</guid>
      <description>&lt;p&gt;One static array, from one of our projects:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Settings&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$cache&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tenantId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;isset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$cache&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tenantId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nx&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$cache&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tenantId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;load&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tenantId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$cache&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tenantId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;??&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A reasonable per-request cache. Under PHP-FPM it died with the process. In a queue worker it lives forever.&lt;/p&gt;
&lt;p&gt;PHP had one great architectural feature nobody put in the manual: the process died after every request. Leak memory, cache nonsense in a static, forget to close things. Did not matter. The dying process forgave everything.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 7.4: typed properties first, preloading later</title>
      <link>https://useinov.com/posts/2019/php74-preloading/</link>
      <pubDate>Fri, 29 Nov 2019 15:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/php74-preloading/</guid>
      <description>&lt;p&gt;Two lines of a class, and half of my docblocks can be deleted:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Money&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$currency&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PHP 7.4 came out yesterday, and this is the part I start using on Monday. Those docblocks existed only to say &lt;code&gt;@var int&lt;/code&gt;. Now the language says it, and unlike the docblock, it checks. Assign a string, get a &lt;code&gt;TypeError&lt;/code&gt; at the assignment, not a strange bug three layers later.&lt;/p&gt;
&lt;p&gt;There is one new state to learn. A typed property without a default is uninitialized, and reading it before the first write throws. This is the feature. &amp;ldquo;Object exists but is not filled yet&amp;rdquo; finally fails loudly instead of pretending to be null.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 12 inlines your CTE</title>
      <link>https://useinov.com/posts/2019/postgres12-cte/</link>
      <pubDate>Fri, 18 Oct 2019 21:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/postgres12-cte/</guid>
      <description>&lt;p&gt;&lt;code&gt;grep -rn &amp;quot;WITH &amp;quot; src/&lt;/code&gt; was the first thing I ran after upgrading to PostgreSQL 12 this month. I was looking for a trick that had stopped working.&lt;/p&gt;
&lt;p&gt;For years &lt;code&gt;WITH&lt;/code&gt; was an optimization fence. The planner materialized the CTE first and only then ran the outer query. Everyone used this both ways. As a bug: you wrap a subquery into a CTE for readability, the planner stops pushing conditions inside, a fast query becomes a scan of half a table. As a feature: you write a CTE on purpose, to pin evaluation order and stop the planner from being creative. Half of the CTE advice on the internet is really advice about the fence.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Cleaning up before Symfony 5</title>
      <link>https://useinov.com/posts/2019/symfony5-deprecations/</link>
      <pubDate>Wed, 25 Sep 2019 10:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/symfony5-deprecations/</guid>
      <description>&lt;p&gt;Symfony 5.0 is 4.4 minus everything deprecated. That one sentence is the whole upgrade plan. Both land in November, and if the application runs on 4.4 with zero deprecation warnings, the major is a version bump. Two months is enough to get there in small chunks, with no freeze and no heroic branch that lives for six weeks.&lt;/p&gt;
&lt;p&gt;The order that works for me.&lt;/p&gt;
&lt;p&gt;First, get to 4.3 and make the deprecation report visible. The PHPUnit bridge prints the summary after the test run. In dev, the profiler collects the same warnings per request. You cannot clean what you do not see. The first report will be depressing. Fine. It is a todo list, not a verdict.&lt;/p&gt;</description>
    </item>
    <item>
      <title>200 OK is not health</title>
      <link>https://useinov.com/posts/2019/kubernetes-probes-php/</link>
      <pubDate>Wed, 07 Aug 2019 18:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/kubernetes-probes-php/</guid>
      <description>&lt;p&gt;Route &lt;code&gt;/health&lt;/code&gt;, returns &lt;code&gt;ok&lt;/code&gt;, everyone happy. That is the first version of every health endpoint on the project we are moving to Kubernetes. Then somebody pastes the same path into &lt;code&gt;livenessProbe&lt;/code&gt; and &lt;code&gt;readinessProbe&lt;/code&gt;, and a thirty second database hiccup becomes a long evening.&lt;/p&gt;
&lt;p&gt;The two probes ask different questions.&lt;/p&gt;
&lt;p&gt;Readiness asks: should this pod get traffic right now. Here it is correct to check dependencies. Database unreachable, cache cold, migrations still running: answer no. Kubernetes takes the pod out of the Service, traffic goes to the others, the pod returns when the world improves. Failing readiness is cheap and reversible. A polite &amp;ldquo;not now&amp;rdquo;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>N&#43;1 hides on your laptop</title>
      <link>https://useinov.com/posts/2019/n-plus-one-production/</link>
      <pubDate>Fri, 05 Jul 2019 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/n-plus-one-production/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>One use case, one class</title>
      <link>https://useinov.com/posts/2019/application-services/</link>
      <pubDate>Wed, 26 Jun 2019 15:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/application-services/</guid>
      <description>&lt;p&gt;Four hundred lines in a checkout action, half of them inside one try. That is one way a project dies. The other is &lt;code&gt;OrderManager&lt;/code&gt;: thirty methods, six injected services, and nobody can say what the class is for, because it is for everything about orders.&lt;/p&gt;
&lt;p&gt;Both diseases have the same cure and it is embarrassingly simple. One use case, one class, one public method.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;final&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;PlaceOrder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;fm&#34;&gt;__construct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;OrderRepository&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$orders&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;PaymentGateway&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$payments&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;EventDispatcher&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$events&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;handle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;PlaceOrderCommand&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;OrderId&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// the whole story, top to bottom
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The shape answers three questions by itself. What comes in: one command object, a thing you can log, validate, put in a queue. What it needs: the constructor lists dependencies of this use case, not of orders in general, so when &lt;code&gt;PlaceOrder&lt;/code&gt; suddenly needs the mailer you see it and can ask why. What comes out: one result. Reading &lt;code&gt;handle()&lt;/code&gt; from top to bottom tells the whole story of placing an order. No chapter hidden in a base class or a trait.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Messenger retries and the failure transport</title>
      <link>https://useinov.com/posts/2019/messenger-failure-queue/</link>
      <pubDate>Thu, 30 May 2019 23:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/messenger-failure-queue/</guid>
      <description>&lt;p&gt;A log full of the same stack trace, every second, all night. That is what a queue looks like when a handler throws on a malformed payload and the retry policy is &amp;ldquo;forever&amp;rdquo;. Symfony 4.3 is out this week, and Messenger in it finally has an answer for that night.&lt;/p&gt;
&lt;p&gt;Retry is good for transient errors. Network blinked, deadlock, remote API returned 503. Wait, try again, it passes. But some errors are permanent. Malformed payload, entity deleted, a bug in the handler. Retrying those forever means a worker grinding the same poison message until morning. Infinite retry is not persistence, it is denial.&lt;/p&gt;</description>
    </item>
    <item>
      <title>What Redis does when memory ends</title>
      <link>https://useinov.com/posts/2019/redis-maxmemory/</link>
      <pubDate>Tue, 02 Apr 2019 15:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/redis-maxmemory/</guid>
      <description>&lt;p&gt;Users logged out at random. Not all, not always. Only sometimes, only in the afternoon. Afternoon is when traffic peaks. Traffic peaks fill the cache. The cache lived in the same Redis as the sessions, the instance hit &lt;code&gt;maxmemory&lt;/code&gt;, and the eviction policy was &lt;code&gt;allkeys-lru&lt;/code&gt;. Redis did exactly what we asked: threw away the least recently used keys, and some of them were sessions of people who went for lunch.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Transactional DDL is not portable</title>
      <link>https://useinov.com/posts/2019/transactional-ddl/</link>
      <pubDate>Mon, 18 Mar 2019 22:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/transactional-ddl/</guid>
      <description>&lt;p&gt;Statement three of a migration fails. Column name typo. On PostgreSQL the first two ALTERs roll back with it, the schema returns to the exact state before the migration, you fix the typo and run again. On MySQL the first two ALTERs are already permanent.&lt;/p&gt;
&lt;p&gt;Same &lt;code&gt;up()&lt;/code&gt; method, same &lt;code&gt;php artisan migrate&lt;/code&gt;, same green output. The migration tool gives one abstraction over two very different databases. The abstraction covers syntax. It does not cover what happens on failure.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Eloquent observers and hidden control flow</title>
      <link>https://useinov.com/posts/2019/eloquent-events-cost/</link>
      <pubDate>Wed, 27 Feb 2019 12:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/eloquent-events-cost/</guid>
      <description>&lt;p&gt;Three thousand welcome emails. A colleague ran a user import on one project last month: loop over a CSV, &lt;code&gt;$user-&amp;gt;save()&lt;/code&gt;, 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.&lt;/p&gt;
&lt;p&gt;Laravel 5.8 came out yesterday, and reading the changelog brought that evening back, so here is the note.&lt;/p&gt;</description>
    </item>
    <item>
      <title>One payment is enough</title>
      <link>https://useinov.com/posts/2019/laravel-job-idempotency/</link>
      <pubDate>Mon, 21 Jan 2019 20:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/laravel-job-idempotency/</guid>
      <description>&lt;p&gt;Charge the card, send the receipt, ack the job. The worker lost its Redis connection between step one and step three. The queue delivered the job again. The customer paid twice. Support learned some new words from him.&lt;/p&gt;
&lt;p&gt;Retries are not an edge case. Laravel retries failed jobs by design, and you want that, because networks blink. So every job with a side effect must answer one question: what happens if this runs twice. &amp;ldquo;It will not run twice&amp;rdquo; is not an answer. It will.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 7.3 and the value of small releases</title>
      <link>https://useinov.com/posts/2018/php-73-small-steps/</link>
      <pubDate>Thu, 27 Dec 2018 15:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/php-73-small-steps/</guid>
      <description>&lt;p&gt;A trailing comma after the last argument of a function call. PHP 7.3 came out at the start of the month, and this is the feature I noticed first, because I meet the missing comma in every second diff:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$this&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;logger&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;order created&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;order_id&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$order&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A tiny thing that kills a whole class of noisy diffs, the ones where adding an argument touches the previous line too. Arrays got this years ago. Calls only now.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Deprecations are a to-do list, not noise</title>
      <link>https://useinov.com/posts/2018/deprecation-budget/</link>
      <pubDate>Fri, 30 Nov 2018 15:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/deprecation-budget/</guid>
      <description>&lt;p&gt;&lt;code&gt;SYMFONY_DEPRECATIONS_HELPER=&#39;max[total]=20&#39; ./bin/phpunit&lt;/code&gt;. Twenty is the number of deprecation warnings we had on the day we started counting. The rule is that it only goes down.&lt;/p&gt;
&lt;p&gt;Symfony 4.2 came out yesterday, on the six month schedule. With this cadence deprecation warnings stop being an event and become weather. Every minor release brings a new batch, teams train themselves not to see them, and that is a mistake, because a deprecation is the next major upgrade delivered early, in small pieces, with instructions.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 11: partitions grow up, JIT arrives</title>
      <link>https://useinov.com/posts/2018/postgres-eleven-jit/</link>
      <pubDate>Fri, 26 Oct 2018 19:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/postgres-eleven-jit/</guid>
      <description>&lt;p&gt;&lt;code&gt;jit = on&lt;/code&gt;, restart, run the API test suite. Same numbers as before. That was my first evening with PostgreSQL 11, released last week, and it was the correct result.&lt;/p&gt;
&lt;p&gt;JIT is the loud feature of this release and the misunderstood one. Postgres can now compile expression evaluation into machine code through LLVM. People read &amp;ldquo;compilation&amp;rdquo; and expect their endpoints to get faster. They will not. A primary key lookup takes a fraction of a millisecond. There is nothing in it worth compiling, and the compilation itself costs more than the whole query. JIT is for the other kind of query: an aggregate chewing through millions of rows, where the same expression runs so many times that generating machine code for it pays back.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Anatomy of email verification</title>
      <link>https://useinov.com/posts/2018/email-verification-anatomy/</link>
      <pubDate>Tue, 18 Sep 2018 18:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/email-verification-anatomy/</guid>
      <description>&lt;p&gt;&lt;code&gt;email_verified_at&lt;/code&gt;, a timestamp. Laravel 5.7 came out this month with email verification built in, and this column is the first thing I noticed. A boolean would cost the same and answer less. A timestamp answers not only whether, but when, and when a support ticket arrives half a year later, &amp;ldquo;when&amp;rdquo; is the question.&lt;/p&gt;
&lt;p&gt;The tutorials say: implement &lt;code&gt;MustVerifyEmail&lt;/code&gt;, put the &lt;code&gt;verified&lt;/code&gt; middleware on routes, done. True, and boring. The interesting part is how the feature is put together. It is a small example of a cross-cutting feature done right.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Where the checkout logic goes</title>
      <link>https://useinov.com/posts/2018/thin-controllers-laravel/</link>
      <pubDate>Mon, 13 Aug 2018 22:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/thin-controllers-laravel/</guid>
      <description>&lt;p&gt;Validate input, reserve stock, create the order, charge the card, fire events. Five steps in one checkout action, and the fat model versus fat controller argument offers only two rooms for them. Both rooms are wrong.&lt;/p&gt;
&lt;p&gt;Put it all in the controller and you cannot run checkout from anywhere except HTTP. No console command, no queue job, no test without the kernel. Put it in the Order model and the model now knows about payments, stock and notifications, a strange set of friends for an Eloquent class.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Covering index and the price of SELECT *</title>
      <link>https://useinov.com/posts/2018/covering-index/</link>
      <pubDate>Sun, 15 Jul 2018 12:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/covering-index/</guid>
      <description>&lt;p&gt;&lt;code&gt;Using index&lt;/code&gt; in the Extra column of EXPLAIN. That is the cheapest read MySQL can do, and you often get it almost for free.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;42&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ORDER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DESC&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;LIMIT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With an index on &lt;code&gt;(user_id, created_at)&lt;/code&gt; everything the query needs is in the index leaves. The table is never touched. A composite index finds rows fast, a covering index answers the query on its own. Postgres calls this Index Only Scan, with one condition: the visibility map must be fresh, so a table that vacuum never visits quietly falls back to heap fetches.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Messenger: in-process today, queue tomorrow</title>
      <link>https://useinov.com/posts/2018/messenger-sync-first/</link>
      <pubDate>Tue, 19 Jun 2018 22:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/messenger-sync-first/</guid>
      <description>&lt;p&gt;&lt;code&gt;$bus-&amp;gt;dispatch(new SendWelcomeEmail($user-&amp;gt;getId()));&lt;/code&gt; and then, in the same request, a handler:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SendWelcomeEmailHandler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;fm&#34;&gt;__invoke&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;SendWelcomeEmail&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// load user by id, send the email
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is Messenger in Symfony 4.1, still marked experimental. By default everything is synchronous, so at first it is a function call with extra steps.&lt;/p&gt;
&lt;p&gt;The extra steps are for the transport. Change configuration, route this message class to AMQP, and the same handler runs in a worker process. The calling code does not change. Start synchronous, go async when you need it. I like this order much more than &amp;ldquo;install RabbitMQ on day one&amp;rdquo;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>GDPR: where the user actually lives</title>
      <link>https://useinov.com/posts/2018/gdpr-data-map/</link>
      <pubDate>Wed, 09 May 2018 10:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/gdpr-data-map/</guid>
      <description>&lt;p&gt;The row in &lt;code&gt;users&lt;/code&gt;. Then orders, with the delivery address and phone. Then nginx access logs with emails inside GET parameters of an old unsubscribe endpoint. That is how far I got in the first ten minutes of drawing where one user&amp;rsquo;s data lives on one project, and the map did not fit on one page.&lt;/p&gt;
&lt;p&gt;May 25 is close and everyone discusses consent banners. The lawyers can have the banners. The engineering part is the right to erasure. &amp;ldquo;Delete the user&amp;rdquo; sounds like one DELETE statement until you sit down with a pen.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL 8.0: finally, modern SQL</title>
      <link>https://useinov.com/posts/2018/mysql-eight-sql/</link>
      <pubDate>Tue, 24 Apr 2018 22:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/mysql-eight-sql/</guid>
      <description>&lt;p&gt;&lt;code&gt;@row := @row + 1&lt;/code&gt;. I have a report in production that stands on that trick, and the trick was never guaranteed to work. Evaluation order of user variables in SELECT is undefined, it just happened to behave. MySQL 8.0 went GA last week, and for me the release is about SQL. Window functions and CTEs, the things Postgres people stopped noticing years ago, are here.&lt;/p&gt;
&lt;p&gt;The classic task: top three orders per customer. In 5.7 you had a self-join nobody could read a month later, or the variable trick. Now:&lt;/p&gt;</description>
    </item>
    <item>
      <title>.env is not a secrets store</title>
      <link>https://useinov.com/posts/2018/env-not-secrets/</link>
      <pubDate>Wed, 14 Mar 2018 15:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/env-not-secrets/</guid>
      <description>&lt;p&gt;A &lt;code&gt;.env&lt;/code&gt; file committed to git, &amp;ldquo;temporarily&amp;rdquo;. Database password, API keys, mailer credentials, all in one file. Symfony 4 moved configuration to environment variables, and this is the second project this month where I see the same thing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; is a developer convenience. It exists so local setup does not require exporting fifteen variables by hand before running the app. That is the whole job of this file. &lt;code&gt;.env.dist&lt;/code&gt; goes to git with placeholder values, &lt;code&gt;.env&lt;/code&gt; stays in &lt;code&gt;.gitignore&lt;/code&gt; with your local ones. This part is not negotiable.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Rector: refactoring by machine</title>
      <link>https://useinov.com/posts/2018/rector-machine-refactoring/</link>
      <pubDate>Thu, 08 Feb 2018 15:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/rector-machine-refactoring/</guid>
      <description>&lt;p&gt;Search for &lt;code&gt;-&amp;gt;fetch(&lt;/code&gt; in a project with old Kohana code and count the hits. A method call on a model, a comment, a line in a test fixture, an unrelated class that happens to have a method with the same name. That is what regex refactoring looks like. Regex does not know a method call from a string literal.&lt;/p&gt;
&lt;p&gt;Rector does. A young tool I found this month: it parses PHP into an AST with nikic/php-parser, applies transformation rules, prints the code back. Rename a class across the whole project. Change a method call, add an argument everywhere. The AST sees that this &lt;code&gt;fetch&lt;/code&gt; is called on that type and touches only those places. Mechanical change becomes exact.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Horizon, or the queue becomes visible</title>
      <link>https://useinov.com/posts/2018/laravel-horizon-queues/</link>
      <pubDate>Sat, 20 Jan 2018 10:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/laravel-horizon-queues/</guid>
      <description>&lt;p&gt;&lt;code&gt;redis-cli llen queues:default&lt;/code&gt; and hope. That was my queue monitoring for years. Horizon replaces it with a real dashboard, and I did not know how much I needed one until I saw it.&lt;/p&gt;
&lt;p&gt;The mental shift matters more than the UI. &lt;code&gt;dispatch()&lt;/code&gt; is the beginning of the work, not the end. The job still has to wait in Redis, run, maybe fail, maybe retry. All of that was invisible. Now it is on one screen: throughput, wait time per queue, failed jobs with the full payload and the exception.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Sodium in PHP 7.2 core</title>
      <link>https://useinov.com/posts/2017/php-sodium/</link>
      <pubDate>Sun, 10 Dec 2017 20:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/php-sodium/</guid>
      <description>&lt;p&gt;PHP 7.2 came out on the last day of November. Mcrypt is out of core, libsodium is in. That trade alone makes it a good release.&lt;/p&gt;
&lt;p&gt;Search any forum for &amp;ldquo;php encrypt&amp;rdquo; and you find the same folk recipe: &lt;code&gt;openssl_encrypt&lt;/code&gt; with AES-256-CBC, an IV made from who knows what, no authentication of the ciphertext. Every choice in that recipe is a place to be wrong, and CBC without a MAC is wrong in a way that has published attacks. The developer is not careless. The API hands an application developer decisions that belong to a cryptographer.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 4: the framework as recipes</title>
      <link>https://useinov.com/posts/2017/symfony-flex/</link>
      <pubDate>Thu, 30 Nov 2017 21:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/symfony-flex/</guid>
      <description>&lt;p&gt;A kernel, an &lt;code&gt;index.php&lt;/code&gt;, a config folder with almost nothing in it. That is a Symfony 4 project on day one, and 4.0 came out today together with 3.4 LTS. The code changes are the smaller story. How a project starts is the bigger one.&lt;/p&gt;
&lt;p&gt;The Standard Edition is gone. You start from the empty skeleton and ask for what you need. &lt;code&gt;composer require orm&lt;/code&gt; pulls Doctrine, and the recipe writes the config files, registers the bundle in &lt;code&gt;bundles.php&lt;/code&gt;, adds the variables to &lt;code&gt;.env&lt;/code&gt;. You watch your own repo grow file by file as you add dependencies.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 10: partitions I can finally use</title>
      <link>https://useinov.com/posts/2017/postgres-ten/</link>
      <pubDate>Thu, 26 Oct 2017 19:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/postgres-ten/</guid>
      <description>&lt;p&gt;&lt;code&gt;DROP TABLE events_2017_10&lt;/code&gt;. That is how you delete October now, and it is the reason I am reading PostgreSQL 10 release notes on a Thursday evening instead of waiting a year like usual.&lt;/p&gt;
&lt;p&gt;Partitioning existed before, through inheritance, CHECK constraints and an insert trigger you wrote yourself and hoped was right. Now the database owns it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;events&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bigserial&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;timestamptz&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;jsonb&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PARTITION&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RANGE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;events_2017_10&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PARTITION&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;OF&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;events&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FOR&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;VALUES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;2017-10-01&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;2017-11-01&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The win is the data lifecycle. An events table grows forever, and deleting a year of history with DELETE is a night of I/O plus a bloated table in the morning. With partitions, retiring a month is one DROP. Instant. And a query that filters by &lt;code&gt;created_at&lt;/code&gt; visits only the partitions in range, the planner skips the rest.&lt;/p&gt;</description>
    </item>
    <item>
      <title>First run of PHPStan on legacy code</title>
      <link>https://useinov.com/posts/2017/phpstan-first-run/</link>
      <pubDate>Sat, 09 Sep 2017 15:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/phpstan-first-run/</guid>
      <description>&lt;p&gt;Level 0, a legacy codebase, a few hundred errors on the first run. That was my week with PHPStan. Most of the output was noise about magic the tool cannot see. But in the first hour of reading I found three real bugs, live in production for months.&lt;/p&gt;
&lt;p&gt;One: a repository method returns an entity or null, and a caller chains a method right on the result. The not-found branch was never written. It survived because that path needs a deleted record, and deleted records are rare. Rare is not never.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 5.5 and package auto-discovery</title>
      <link>https://useinov.com/posts/2017/laravel-auto-discovery/</link>
      <pubDate>Thu, 31 Aug 2017 15:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/laravel-auto-discovery/</guid>
      <description>&lt;p&gt;Laravel 5.5 came out yesterday, an LTS: two years of bug fixes, three of security fixes. Good news for projects that outlive the hype cycle, which is most of them.&lt;/p&gt;
&lt;p&gt;The feature in every screenshot is package auto-discovery. A package declares its service provider in its own &lt;code&gt;composer.json&lt;/code&gt;, and after &lt;code&gt;composer require&lt;/code&gt; it is registered. No editing of &lt;code&gt;config/app.php&lt;/code&gt;, no provider line copied from the readme.&lt;/p&gt;
&lt;p&gt;Convenient. Now look at what got deleted. Installing a package used to be two steps, and the second step was consent: a line in my own config saying this code runs inside my application at boot. Now &lt;code&gt;composer require&lt;/code&gt; is the whole ceremony. The list of code that runs at boot lives in &lt;code&gt;vendor/&lt;/code&gt;, spread over other people&amp;rsquo;s files. &lt;code&gt;php artisan package:discover&lt;/code&gt; shows it, and &lt;code&gt;dont-discover&lt;/code&gt; in composer.json switches it off per package. But the default flipped from explicit to implicit.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Multi-stage builds for PHP images</title>
      <link>https://useinov.com/posts/2017/docker-multi-stage/</link>
      <pubDate>Fri, 07 Jul 2017 18:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/docker-multi-stage/</guid>
      <description>&lt;p&gt;720 MB. That was our production PHP image, and most of it was composer, git, unzip, build headers and a pile of apt cache. None of it runs in production. It was there because &lt;code&gt;composer install&lt;/code&gt; needs it, and a Dockerfile was one linear script. People worked around this with two Dockerfiles and a shell script gluing them. Ugly, and everyone&amp;rsquo;s glue was different.&lt;/p&gt;
&lt;p&gt;Docker 17.05 brought multi-stage builds. The first Docker feature in a while that fixes a problem I actually had. Now it is one file:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Keyset pagination instead of OFFSET</title>
      <link>https://useinov.com/posts/2017/keyset-pagination/</link>
      <pubDate>Mon, 19 Jun 2017 10:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/keyset-pagination/</guid>
      <description>&lt;p&gt;&lt;code&gt;LIMIT 50 OFFSET 500000&lt;/code&gt;. Page 10001 of an events table, from a paginator someone wrote in an afternoon. Postgres has no shortcut to row 500000. It walks the index through half a million entries, fetches them, throws them away, and returns fifty. Page one is fast. Page ten thousand is slow, and every page after it is slower. Run EXPLAIN ANALYZE on both: the plans are identical, the numbers are not.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Private services: the container steps back</title>
      <link>https://useinov.com/posts/2017/private-services/</link>
      <pubDate>Tue, 30 May 2017 15:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/private-services/</guid>
      <description>&lt;p&gt;&lt;code&gt;$container-&amp;gt;get(&#39;app.mailer&#39;)&lt;/code&gt; stopped working this week. Symfony 3.3 is out, and in the new configuration services are private by default. The container refuses the call. Everyone is writing about autowiring. This is the change I like more.&lt;/p&gt;
&lt;p&gt;That call was always a smell. A class that pulls dependencies from the container by string id is a class with secrets. Its constructor says nothing, and to test it you boot half the framework, because it may ask for anything at any moment.&lt;/p&gt;</description>
    </item>
    <item>
      <title>ALTER TABLE is part of the deploy</title>
      <link>https://useinov.com/posts/2017/zero-downtime-migrations/</link>
      <pubDate>Mon, 24 Apr 2017 15:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/zero-downtime-migrations/</guid>
      <description>&lt;p&gt;Forty million rows, three application instances behind a balancer, one migration that renames a column. The deploy script runs migrations first, then rolls the code instance by instance. For a few minutes old code was writing to a column that no longer existed. The error rate graph looked like a wall. I had approved that migration. It was one line.&lt;/p&gt;
&lt;p&gt;Migration tools trained us to think a schema change is a code change. Write a file, run it, done. A schema change on a big table takes time, takes locks, and while it runs the previous version of the application is still serving traffic. Both versions must survive both schemas. That is the whole discipline, and it has a name: expand and contract.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Autowiring is coming to Symfony 3.3</title>
      <link>https://useinov.com/posts/2017/symfony-autowiring/</link>
      <pubDate>Wed, 08 Mar 2017 15:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/symfony-autowiring/</guid>
      <description>&lt;p&gt;Our services file repeats every class name three times. Once as the id, once as the class, and again in the arguments of every service that needs it. Add a constructor parameter, edit YAML, clear cache, repeat. Symfony 3.3 lands in May and promises to delete most of that file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;services&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;_defaults&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;autowire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;autoconfigure&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;AppBundle\&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;resource&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;../../src/AppBundle/*&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Autowiring and autoconfiguration on by default, the whole source folder registered in five lines. I have mixed feelings. Sorted, they look like this.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Cache stampede: the hot key problem</title>
      <link>https://useinov.com/posts/2017/cache-stampede/</link>
      <pubDate>Tue, 07 Feb 2017 21:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/cache-stampede/</guid>
      <description>&lt;p&gt;Cache hit rate 98 percent. The database still fell over. Both facts are true, and the second one does not care about the first.&lt;/p&gt;
&lt;p&gt;What happened. One key holds the result of a heavy query for the main page. TTL one minute. At some second the key expires. In that second a few hundred requests all get a miss, and all of them go to recompute the same heavy query. Redis is fine. MySQL is not. This is a cache stampede, and hit rate will not warn you, because hit rate is an average. The stampede lives in the worst second.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel Dusk and what browser tests are for</title>
      <link>https://useinov.com/posts/2017/laravel-dusk/</link>
      <pubDate>Thu, 26 Jan 2017 18:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/laravel-dusk/</guid>
      <description>&lt;p&gt;Laravel 5.4 came out on Tuesday, and the first thing I installed was Dusk. Browser tests without a Selenium server: it talks to ChromeDriver directly, the API is fluent, and a failed test leaves a screenshot behind.&lt;/p&gt;
&lt;p&gt;My first test was the login path:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$this&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;browse&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$browser&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$browser&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;visit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/login&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;user@example.com&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;password&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;secret&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;press&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Log in&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;assertPathIs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/home&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It passed on the third run. The first run failed because the button on the test box had different text. The second failed because a JS animation was slower than the wait. This is the normal life of a browser test. It sees the real application, and the real application moves.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 7.1: nullable is honest</title>
      <link>https://useinov.com/posts/2016/php71-nullable-types/</link>
      <pubDate>Fri, 09 Dec 2016 20:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/php71-nullable-types/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// returns User or false, see wiki
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;findByEmail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every legacy codebase has this method. The comment is the type system. Half the callers check for &lt;code&gt;false&lt;/code&gt;, some check for &lt;code&gt;null&lt;/code&gt; because a sister method returns null, one caller checks nothing and works by luck.&lt;/p&gt;
&lt;p&gt;PHP 7.1 came out last week. Nullable types, &lt;code&gt;void&lt;/code&gt;, &lt;code&gt;iterable&lt;/code&gt;, multi-catch. Small features, but together they continue the direction 7.0 started: less implicit agreement, more signature. The one I care about is &lt;code&gt;?Type&lt;/code&gt;:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Order status is a graph</title>
      <link>https://useinov.com/posts/2016/order-status-graph/</link>
      <pubDate>Wed, 30 Nov 2016 22:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/order-status-graph/</guid>
      <description>&lt;p&gt;&lt;code&gt;if ($order-&amp;gt;status == &#39;paid&#39;)&lt;/code&gt; in the shipping module. Another one in the refund module. A slightly different one in the admin panel. Then someone cancels a shipped order from an admin button, because the button just writes the string, and the warehouse learns about it a week later.&lt;/p&gt;
&lt;p&gt;Symfony 3.2 is out this week and ships a Workflow component. Finally a framework says out loud what every order table has been whispering for years: status is a state machine, not a string column you assign wherever convenient.&lt;/p&gt;</description>
    </item>
    <item>
      <title>The queue will run your job twice</title>
      <link>https://useinov.com/posts/2016/idempotent-jobs/</link>
      <pubDate>Mon, 03 Oct 2016 15:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/idempotent-jobs/</guid>
      <description>&lt;p&gt;The job called the payment provider. The call succeeded. Then the worker timed out before it marked the job done. The queue did what queues do: it retried. The provider did what it was asked: it charged again. The customer did what customers do and wrote an angry email.&lt;/p&gt;
&lt;p&gt;Nobody made a mistake here. The queue promises at-least-once delivery, and &amp;ldquo;at least&amp;rdquo; is written in the contract. Network partitions, worker crashes, deploy restarts. Sooner or later every job runs twice, and the jobs that hurt are exactly the ones with external effects: payments, emails, webhooks, API calls.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Parallel query in PostgreSQL 9.6</title>
      <link>https://useinov.com/posts/2016/postgres-parallel-query/</link>
      <pubDate>Fri, 30 Sep 2016 22:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/postgres-parallel-query/</guid>
      <description>&lt;p&gt;38 seconds to 11. Same query, same forty million rows, no index added, no SQL changed. The only difference is PostgreSQL 9.6, released yesterday, and one setting.&lt;/p&gt;
&lt;p&gt;The query is a typical report: count and sum over an events table, grouped by day, three months of data. On 9.5 the plan is one process grinding through the table. On 9.6 with parallelism enabled:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Finalize HashAggregate
  -&amp;gt;  Gather
        Workers Planned: 4
        -&amp;gt;  Partial HashAggregate
              -&amp;gt;  Parallel Seq Scan on events
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Four workers scan their own chunks, aggregate partially, the leader merges. Just more hands. For years we optimized SQL as if the database had exactly one worker per query. That assumption expired yesterday.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 5.3: the app is not a website anymore</title>
      <link>https://useinov.com/posts/2016/laravel-53-notifications/</link>
      <pubDate>Fri, 26 Aug 2016 12:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/laravel-53-notifications/</guid>
      <description>&lt;p&gt;&lt;code&gt;Mail::send()&lt;/code&gt; inside the checkout controller. A second copy inside the API controller. Then someone adds the chat webhook to one copy and forgets the other. Every project has this code. I have written it more than once.&lt;/p&gt;
&lt;p&gt;Laravel 5.3 came out this week, and the two big pieces, Notifications and Passport, both point the same way. The application is no longer a thing that renders HTML. It is a core that talks to browsers, mobile clients and third parties, and HTML is one of the outputs.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Column order in a composite index</title>
      <link>https://useinov.com/posts/2016/composite-index-order/</link>
      <pubDate>Mon, 04 Jul 2016 23:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/composite-index-order/</guid>
      <description>&lt;p&gt;Fifty rows. Three indexes on the table, one per column. Still slow. The query is the standard one from any multi-tenant application:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tenant_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AND&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ORDER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DESC&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;LIMIT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;ldquo;Add an index&amp;rdquo; is the typical reaction, and one index on each column is the typical result. The number of indexes is not the point. The order of columns inside one index is.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Doctrine and the 100 000 row import</title>
      <link>https://useinov.com/posts/2016/doctrine-batch-memory/</link>
      <pubDate>Sun, 19 Jun 2016 20:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/doctrine-batch-memory/</guid>
      <description>&lt;p&gt;Row sixty thousand. &lt;code&gt;Allowed memory size exhausted&lt;/code&gt;. The import script on one project died there, and the code was the obvious loop: read a row, &lt;code&gt;persist()&lt;/code&gt; an entity, next row, &lt;code&gt;flush()&lt;/code&gt; at the end. On a hundred test rows it worked perfectly. It was my loop.&lt;/p&gt;
&lt;p&gt;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&amp;rsquo;s main feature, and on a normal web request the price is invisible because the request dies young.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony by the piece</title>
      <link>https://useinov.com/posts/2016/symfony-by-piece/</link>
      <pubDate>Tue, 10 May 2016 19:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/symfony-by-piece/</guid>
      <description>&lt;p&gt;A folder of cron scripts, each one a &lt;code&gt;.php&lt;/code&gt; file with hand-parsed &lt;code&gt;$argv&lt;/code&gt;. That is what CLI looked like on our legacy Kohana application until last month. One line fixed it:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;composer require symfony/console
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now each script is a small Command class with named options, &lt;code&gt;--help&lt;/code&gt; for free, and exit codes that cron can actually check. The framework around it did not notice anything. Console does not care who serves your HTTP.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Docker instead of a setup README</title>
      <link>https://useinov.com/posts/2016/docker-for-php/</link>
      <pubDate>Wed, 13 Apr 2016 21:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/docker-for-php/</guid>
      <description>&lt;p&gt;Two days. That is how long the new colleague spent last month getting one project to run on his laptop. The README is two pages: PHP with a specific set of extensions, Nginx, MySQL, Redis, and a paragraph that starts with &amp;ldquo;on OS X it is a bit different&amp;rdquo;. Every laptop in the team is a slightly different snowflake.&lt;/p&gt;
&lt;p&gt;So I finally tried Docker for local development. The compose file:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Freezing the Kohana layer</title>
      <link>https://useinov.com/posts/2016/freezing-kohana/</link>
      <pubDate>Fri, 04 Mar 2016 20:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/freezing-kohana/</guid>
      <description>&lt;p&gt;Kohana 3.3, on a project that earns money every day. The framework is effectively finished. The repository barely moves, the community left years ago. Nobody will approve a rewrite, and I have stopped asking. This is a normal situation and it deserves a better plan than &amp;ldquo;someday we migrate&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The plan we settled on is a freeze.&lt;/p&gt;
&lt;p&gt;Pin the exact framework version and vendor it. Not &amp;ldquo;3.3.*&amp;rdquo;, the exact commit. The build must be reproducible in five years, when the original download link is dead.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Middleware groups in Laravel 5.2</title>
      <link>https://useinov.com/posts/2016/laravel-middleware-groups/</link>
      <pubDate>Mon, 08 Feb 2016 21:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/laravel-middleware-groups/</guid>
      <description>&lt;p&gt;An IP whitelist check placed after auth. That was the whole bug. Every scanner bot on the internet was going through session start and a user lookup just to be told to go away. The fix was to move one line up in the Kernel.&lt;/p&gt;
&lt;p&gt;Laravel 5.2 makes that line easier to see. Middleware groups, and the request pipeline is finally written down in one place:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;protected&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$middlewareGroups&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;web&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;\App\Http\Middleware\EncryptCookies&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;class&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;\Illuminate\Session\Middleware\StartSession&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;class&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;\App\Http\Middleware\VerifyCsrfToken&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;class&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s1&#34;&gt;&amp;#39;api&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;throttle:60,1&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s1&#34;&gt;&amp;#39;bindings&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before 5.2, sessions and CSRF were global middleware. They ran for everything, including API routes that have no use for cookies. Now &lt;code&gt;web&lt;/code&gt; and &lt;code&gt;api&lt;/code&gt; are two separate pipelines, and you can read each one top to bottom.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Upsert in PostgreSQL 9.5</title>
      <link>https://useinov.com/posts/2016/postgres-upsert/</link>
      <pubDate>Tue, 12 Jan 2016 22:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/postgres-upsert/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$row&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$db&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fetchOne&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;SELECT id FROM counters WHERE name = ?&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$row&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$db&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;execute&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;UPDATE counters SET value = value + 1 WHERE name = ?&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$db&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;execute&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;INSERT INTO counters (name, value) VALUES (?, 1)&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Works on the laptop. In production two requests arrive in the same millisecond. Both SELECT, both see nothing, both INSERT. One dies with a duplicate key error. Or worse, there is no unique constraint, and now you have two rows and a bug report you cannot reproduce. The window between SELECT and INSERT is tiny, so it fires once a week, always for someone else.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 7 measured on real code</title>
      <link>https://useinov.com/posts/2015/php7-measured/</link>
      <pubDate>Sat, 26 Dec 2015 21:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/php7-measured/</guid>
      <description>&lt;p&gt;One endpoint. The heaviest catalog page of one project, same data, same opcache settings, PHP 5.6 against 7.0. That is the only benchmark I trust. 7.0 has been out for three weeks, everyone has seen the hello-world numbers, and I do not believe hello-world numbers on principle.&lt;/p&gt;
&lt;p&gt;Response time dropped about 40 percent. Memory per request, more than half. No code changes. I ran it again because I did not believe the first numbers either. The engine rewrite is real: smaller internal structures, cheaper function calls, and typical framework code is exactly that, thousands of small calls and arrays.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 3.0 is not a rewrite</title>
      <link>https://useinov.com/posts/2015/symfony-two-step/</link>
      <pubDate>Mon, 30 Nov 2015 22:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/symfony-two-step/</guid>
      <description>&lt;p&gt;Symfony 2.8 and 3.0, released today, both at once, on purpose. The pairing is the whole message. 2.8 is the last of the 2.x line and an LTS. 3.0 is roughly 2.8 with the deprecated code deleted. Same features, cleaner body.&lt;/p&gt;
&lt;p&gt;This turns a scary major upgrade into two steps.&lt;/p&gt;
&lt;p&gt;Step one, move to 2.8. Minor upgrade, low risk. Then make the deprecation log empty. The phpunit bridge prints every deprecated call your code and tests touch. Burn the list down during normal sprints, item by item, each fix small and releasable on its own.&lt;/p&gt;</description>
    </item>
    <item>
      <title>JSON column is not a schema</title>
      <link>https://useinov.com/posts/2015/json-not-schema/</link>
      <pubDate>Fri, 23 Oct 2015 22:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/json-not-schema/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;users&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ADD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;profile&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL 5.7 went GA this week, and this line is now legal. Native JSON type: validation on insert, binary storage, functions to read paths. The question is not whether it works. It is what belongs in it.&lt;/p&gt;
&lt;p&gt;Classic case: user profile with flexible metadata. Marketing wants a new field every other week. The old options were bad in familiar ways. Forty nullable columns and an ALTER for every idea. Or EAV, key-value rows, where every read is a self-join festival and nothing has a type.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Kohana on PHP 7: whose problem is it</title>
      <link>https://useinov.com/posts/2015/kohana-on-php7/</link>
      <pubDate>Tue, 08 Sep 2015 10:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/kohana-on-php7/</guid>
      <description>&lt;p&gt;The test suite went green on the PHP 7 release candidate on the second try. I expected worse. Then I opened &lt;code&gt;modules/&lt;/code&gt; and the mood changed.&lt;/p&gt;
&lt;p&gt;The core mostly works. The community is patching compatibility in the 3.3 branch on GitHub right now, and the fixes are small: deprecated PHP 4 style constructors, changed engine behavior in dark corners. Whoever says the framework is dead is not reading the commit log. It is not dead. It is volunteers.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Redis cache and Redis queue are different databases</title>
      <link>https://useinov.com/posts/2015/redis-two-jobs/</link>
      <pubDate>Fri, 21 Aug 2015 19:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/redis-two-jobs/</guid>
      <description>&lt;p&gt;Two lines from redis.conf on one project:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;maxmemory 2gb
maxmemory-policy allkeys-lru
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Correct for a cache. Then sessions moved into the same instance, because Redis was already there. Then the job queue, same reason. One process, three tenants. This works until the day it does not.&lt;/p&gt;
&lt;p&gt;Cache data is disposable by definition, and those two lines embrace that. Memory fills up, Redis evicts the coldest keys, the application rebuilds them on demand. Persistence is optional. After a restart a cold cache is an inconvenience, not an incident.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Run your tests on PHP 7 now</title>
      <link>https://useinov.com/posts/2015/php7-early-tests/</link>
      <pubDate>Wed, 08 Jul 2015 22:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/php7-early-tests/</guid>
      <description>&lt;p&gt;Three lines in &lt;code&gt;.travis.yml&lt;/code&gt;. That is the whole cost of knowing in July what will break on PHP 7 in December.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;php&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;m&#34;&gt;5.6&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;nightly&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;matrix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;allow_failures&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;php&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;nightly&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alpha builds are out, release is planned for this autumn, and production is on 5.6 and will stay there for a while. The build stays green on 5.6. The nightly job fails quietly in the corner and produces a list of future problems while nobody is under pressure to read it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 5.1 and the dependency ladder</title>
      <link>https://useinov.com/posts/2015/dependency-ladder/</link>
      <pubDate>Sun, 14 Jun 2015 19:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/dependency-ladder/</guid>
      <description>&lt;p&gt;PHP 5.5.9. That is the minimum for Laravel 5.1, which came out this week, the first Laravel with LTS: two years of bug fixes, three of security fixes. Two weeks ago I wrote the same about Symfony 2.7. Two frameworks arriving at the same idea in one summer is no coincidence. PHP applications got old enough to have a lifecycle.&lt;/p&gt;
&lt;p&gt;Draw the ladder for your project once. OS at the bottom. Then the PHP version the OS ships, or the one you build. Then the framework. Then the packages on top. Every rung has its own end-of-life, and the rungs are coupled. If production sits on some old distro with 5.4, the 5.1 upgrade is suddenly an ops project. And above you, half the packages will require 5.1 in their next major, so staying behind slowly cuts you off from fixes.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 2.7: LTS is the feature</title>
      <link>https://useinov.com/posts/2015/symfony-lts/</link>
      <pubDate>Sun, 31 May 2015 12:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/symfony-lts/</guid>
      <description>&lt;p&gt;Three years of bug fixes. Four years of security fixes. Symfony 2.7 came out this weekend, and that is the line from the announcement I care about. The changelog has new things in it, but the important word is on the label: LTS.&lt;/p&gt;
&lt;p&gt;For a pet project this is boring. For a product with paying users and a team, this is the actual feature.&lt;/p&gt;
&lt;p&gt;A big application does not upgrade for fun. Every framework upgrade is testing time, regression risk, and a sprint that produces nothing visible. Business asks a fair question: what do we get. &amp;ldquo;Newer version&amp;rdquo; is not an answer. &amp;ldquo;We keep receiving security fixes until 2018 without touching anything&amp;rdquo; is.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Where Doctrine spends your milliseconds</title>
      <link>https://useinov.com/posts/2015/doctrine-latency/</link>
      <pubDate>Thu, 23 Apr 2015 10:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/doctrine-latency/</guid>
      <description>&lt;p&gt;&lt;code&gt;$order-&amp;gt;getCustomer()-&amp;gt;getName()&lt;/code&gt; inside a &lt;code&gt;foreach&lt;/code&gt;. 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.&lt;/p&gt;
&lt;p&gt;Same few places every time.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Three ways to get a dependency in Laravel</title>
      <link>https://useinov.com/posts/2015/container-or-facade/</link>
      <pubDate>Mon, 09 Mar 2015 21:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/container-or-facade/</guid>
      <description>&lt;p&gt;Same repository, three ways to get it in Laravel 5.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 1. Constructor injection
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;fm&#34;&gt;__construct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;OrderRepository&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$orders&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$this&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;orders&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$orders&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 2. Facade
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$order&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Orders&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 3. Service locator
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$orders&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;App&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;App\Repositories\OrderRepository&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All three work. The container resolves everything either way. The difference is in what the class tells you about itself.&lt;/p&gt;
&lt;p&gt;With constructor injection the dependencies are in the signature. You open the class, you read the constructor, you know what it needs. A test passes a mock and never touches the container.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Laravel 5 and the death of the models folder</title>
      <link>https://useinov.com/posts/2015/laravel-five-structure/</link>
      <pubDate>Wed, 25 Feb 2015 22:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/laravel-five-structure/</guid>
      <description>&lt;p&gt;&lt;code&gt;app/models&lt;/code&gt; is gone. Laravel 5 came out at the start of the month, and of everything in the release the folder I notice most is the one that no longer exists.&lt;/p&gt;
&lt;p&gt;The old skeleton had &lt;code&gt;app/models&lt;/code&gt; and &lt;code&gt;app/controllers&lt;/code&gt;. You knew where things go because the framework told you. Laravel 5 removes that. There is &lt;code&gt;app/&lt;/code&gt;, it maps to the &lt;code&gt;App&lt;/code&gt; namespace through PSR-4, and inside it you organize classes however you want.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Composer in a legacy Kohana project</title>
      <link>https://useinov.com/posts/2015/kohana-meets-composer/</link>
      <pubDate>Thu, 22 Jan 2015 20:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/kohana-meets-composer/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;require&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;APPPATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;../vendor/autoload.php&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One line in &lt;code&gt;bootstrap.php&lt;/code&gt;. This month it became the whole strategy for a project we decided to keep on Kohana.&lt;/p&gt;
&lt;p&gt;The framework is quiet, the project is alive. These two facts have to coexist somehow. The plan: nothing new gets written the Kohana way. New dependencies come through Composer, and Kohana&amp;rsquo;s autoloader and Composer&amp;rsquo;s autoloader live together fine.&lt;/p&gt;
&lt;p&gt;First candidate was the HTTP client. We talk to two external APIs, and the code around &lt;code&gt;Request_Client_External&lt;/code&gt; was not pretty. Guzzle is better in every way. But Guzzle does not spread through the codebase. There is an interface:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Host header is user input</title>
      <link>https://useinov.com/posts/2014/host-header-is-user-input/</link>
      <pubDate>Tue, 23 Dec 2014 19:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/host-header-is-user-input/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$link&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;https://&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$_SERVER&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;HTTP_HOST&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/reset/&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$token&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I wrote this line. More than once. It builds the link for a password reset email, and it looks like it reads something about the server. It does not. &lt;code&gt;Host&lt;/code&gt; comes from the client, same as any other header.&lt;/p&gt;
&lt;p&gt;Kohana 3.3.4 shipped this month with a fix around exactly this, and the topic is bigger than one framework.&lt;/p&gt;
&lt;p&gt;The attack is short. Someone requests a password reset for your email address and puts his own domain into the &lt;code&gt;Host&lt;/code&gt; header. The application trusts the header, builds the link, sends it. You receive a real email from a real site with a link to &lt;code&gt;evil.example&lt;/code&gt; and your real token in the path. You click. He collects the token. Host header poisoning, and no framework protects you from it out of the box.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony 2.6: dump() and boring upgrades</title>
      <link>https://useinov.com/posts/2014/symfony-26-vardumper/</link>
      <pubDate>Thu, 18 Dec 2014 22:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/symfony-26-vardumper/</guid>
      <description>&lt;p&gt;&lt;code&gt;dump()&lt;/code&gt; instead of &lt;code&gt;var_dump()&lt;/code&gt;. Symfony 2.6 came out at the end of November, and this is the visible gift: VarDumper. Collapsible output, clickable class names, works inside the toolbar. A small thing, used fifty times a day. Tools like this improve life more than big features do.&lt;/p&gt;
&lt;p&gt;But the important story in 2.6 is not a feature. The release notes read like a preparation checklist for Symfony 3: deprecations, everywhere.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Symfony best practices: defaults, not laws</title>
      <link>https://useinov.com/posts/2014/symfony-best-practices/</link>
      <pubDate>Sun, 14 Dec 2014 20:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/symfony-best-practices/</guid>
      <description>&lt;p&gt;One AppBundle. That is the line in the official Symfony Best Practices book I read twice. For years the answer to &amp;ldquo;how many bundles&amp;rdquo; was &amp;ldquo;as many as you have features&amp;rdquo;, and here the framework&amp;rsquo;s own book says: one.&lt;/p&gt;
&lt;p&gt;The book came out this autumn. Worth reading even if you are not on Symfony, because the interesting part is the change of tone.&lt;/p&gt;
&lt;p&gt;The old Symfony way: everything is a bundle. Your application is bundles, reusable, configurable, with their own extensions and semantic configuration. Very flexible. And for a normal business application, mostly ceremony. You write a configuration class for code that will never leave this one project. I wrote such classes. Three of them. None was ever reused.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Debug toolbar in production is a gift to strangers</title>
      <link>https://useinov.com/posts/2014/profiler-in-production/</link>
      <pubDate>Tue, 09 Dec 2014 21:00:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/profiler-in-production/</guid>
      <description>&lt;p&gt;&lt;code&gt;/_profiler&lt;/code&gt; on a live site. Type it after any Symfony domain and sometimes it answers. This autumn the story went around, and I will not retell it. The lesson is bigger than one framework.&lt;/p&gt;
&lt;p&gt;Think what a profiler actually stores. Every SQL query with parameters. Cookies and session data. Routes, controller names, request headers. Sometimes config values. A full X-ray of your application, nicely formatted, with search.&lt;/p&gt;
&lt;p&gt;Now count how many sites have &lt;code&gt;/_profiler&lt;/code&gt; or a debugbar open to the world because someone deployed with &lt;code&gt;app_dev.php&lt;/code&gt;, or left &lt;code&gt;debug = true&lt;/code&gt; in the production config. Not a rare exotic mistake. I saw it. I made it once, on a staging server that stayed reachable from outside longer than anyone planned.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 5.6: small syntax, big TLS surprise</title>
      <link>https://useinov.com/posts/2014/php-56-upgrade/</link>
      <pubDate>Fri, 05 Dec 2014 19:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/php-56-upgrade/</guid>
      <description>&lt;p&gt;A nightly cron failed silently for two days. That was our PHP 5.6 upgrade.&lt;/p&gt;
&lt;p&gt;5.6 is out since the end of August, and we moved one production project this month. The syntax part is pleasant and minor. Variadics:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;log_all&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$level&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$messages&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Argument unpacking with &lt;code&gt;...$args&lt;/code&gt; on the call side. Constant expressions in defaults. &lt;code&gt;use function&lt;/code&gt; for importing functions. All nice. None of it changes your architecture. You will use variadics maybe twice a year and be happy both times.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Services instead of fat controllers</title>
      <link>https://useinov.com/posts/2014/services-instead-of-fat-controllers/</link>
      <pubDate>Tue, 02 Dec 2014 22:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/services-instead-of-fat-controllers/</guid>
      <description>&lt;p&gt;120 lines in one action. Order confirmation: load the order, charge the card, send the email, render the page. It ran fine for a year. Then a console script needed the same operation, and the action turned out to be glued to SMTP, to the database and to the HTTP request. All at once.&lt;/p&gt;
&lt;p&gt;Symfony people talk about dependency injection so much it sounds like religion. It is one simple idea: a class receives its dependencies and does not create them.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Queues in Laravel 4.2: the user should not wait for your SMTP</title>
      <link>https://useinov.com/posts/2014/laravel-queues/</link>
      <pubDate>Thu, 27 Nov 2014 20:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/laravel-queues/</guid>
      <description>&lt;p&gt;1.8 seconds for one registration request. I put a timer around it on a project last month. 1.5 of those seconds was the welcome email going out over SMTP.&lt;/p&gt;
&lt;p&gt;The user waits almost two seconds and looks at a spinner, for a handshake with a mail server he will never hear about.&lt;/p&gt;
&lt;p&gt;Laravel 4.2 makes the fix one line:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;Queue&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;::&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;push&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;SendWelcomeEmail&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;user_id&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$user&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The controller returns in 300 ms. A worker picks the job up and sends the email. &lt;code&gt;php artisan queue:listen&lt;/code&gt; to start, beanstalkd or Redis behind it, the &lt;code&gt;failed_jobs&lt;/code&gt; table for jobs that died.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Composer next to Kohana bootstrap</title>
      <link>https://useinov.com/posts/2014/composer-next-to-kohana/</link>
      <pubDate>Sun, 23 Nov 2014 21:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/composer-next-to-kohana/</guid>
      <description>&lt;p&gt;One line in &lt;code&gt;bootstrap.php&lt;/code&gt;, above &lt;code&gt;Kohana::init()&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;require&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;DOCROOT&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;vendor/autoload.php&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is the whole integration. Kohana keeps its modules and its underscore autoloader. Composer brings Packagist. Two autoloaders in one process, and they do not collide: Composer packages use namespaces, Kohana classes have underscores, the lookups never meet.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;require&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;guzzlehttp/guzzle&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;~5.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This setup runs in production on one of my projects for a few months. Zero problems from the autoloading side. The problems, when they came, were from me.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Kohana is not developed anymore. Now what</title>
      <link>https://useinov.com/posts/2014/kohana-is-over-now-what/</link>
      <pubDate>Wed, 19 Nov 2014 19:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/kohana-is-over-now-what/</guid>
      <description>&lt;p&gt;The commit log of kohana/kohana on GitHub. I opened it in the summer and counted: a few merges a month, then fewer, then nothing. This year the official development basically stopped. Our projects did not stop working on that day. But the question came to my inbox three times since, so here is the answer.&lt;/p&gt;
&lt;p&gt;What to do with a working Kohana product depends on which of four situations you are in.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Cascading filesystem: the best idea in Kohana</title>
      <link>https://useinov.com/posts/2014/cascading-filesystem/</link>
      <pubDate>Sun, 16 Nov 2014 22:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/cascading-filesystem/</guid>
      <description>&lt;p&gt;&lt;code&gt;Kohana::find_file()&lt;/code&gt;. One function, maybe forty lines. If I could keep only one thing from Kohana, this is the one.&lt;/p&gt;
&lt;p&gt;It looks for every file in order: &lt;code&gt;application/&lt;/code&gt;, then modules, then &lt;code&gt;system/&lt;/code&gt;. First found wins. Config, views, classes, i18n, everything goes through the same lookup.&lt;/p&gt;
&lt;p&gt;In practice: a module ships a config file, you put a file with the same name into &lt;code&gt;application/config/&lt;/code&gt;, change one key. The module does not know. Nothing under &lt;code&gt;modules/&lt;/code&gt; is touched.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Kohana ORM and my first real N&#43;1</title>
      <link>https://useinov.com/posts/2014/kohana-orm-n-plus-one/</link>
      <pubDate>Wed, 12 Nov 2014 20:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/kohana-orm-n-plus-one/</guid>
      <description>&lt;p&gt;Fifty one queries. That is what the SQL panel of the Kohana profiler showed for one catalog page. Fifty posts on the page.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$post-&amp;gt;author-&amp;gt;name&lt;/code&gt; in a loop. One query for the list of posts, then one more for the author of every post, inside the &lt;code&gt;foreach&lt;/code&gt;. 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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>HMVC in Kohana: a request inside a request</title>
      <link>https://useinov.com/posts/2014/kohana-hmvc/</link>
      <pubDate>Mon, 10 Nov 2014 21:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2014/kohana-hmvc/</guid>
      <description>&lt;p&gt;&lt;code&gt;Request::factory(&#39;widgets/stats&#39;)&lt;/code&gt; inside a controller. A colleague looked at this line last week and asked the question I hear most often about Kohana: you already have a request, why make another one?&lt;/p&gt;
&lt;p&gt;I keep answering it at the desk. So I will write it down once. Short notes, mostly backend, mostly PHP. This is the first.&lt;/p&gt;
&lt;p&gt;The answer is composition. Take a dashboard. A stats block, a recent orders block, a notifications block. Each one needs its own data and its own logic. You can put all of it into one action. It works. In a month it is 300 lines and nobody wants to open it.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
