<?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>Php on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/php/</link><description>Recent content in Php on MongrelDB</description><image><title>MongrelDB</title><url>https://www.mongreldb.com/logo.png</url><link>https://www.mongreldb.com/logo.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Thu, 16 Jul 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/php/index.xml" rel="self" type="application/rss+xml"/><item><title>PHP 8.4 Features We Use (and PHP 8.5 Features We Now Use)</title><link>https://www.mongreldb.com/articles/2026/07/php-8-4-features-we-use-and-php-8-5-features-we-look-forward-to/</link><pubDate>Thu, 16 Jul 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/07/php-8-4-features-we-use-and-php-8-5-features-we-look-forward-to/</guid><description>PHP 8.4 is the floor of the pure PHP client, and PHP 8.5, released in November 2025, is where the language gives us enough to stop writing workarounds for cURL and readonly cloning.</description><content:encoded><![CDATA[<p>Every time a database client requires a C extension, a deployment story dies, because shared hosts disable <code>phpize</code>, CI images mismatch the ABI, and a developer somewhere is reading a PECL error at 11 PM instead of shipping a feature. That is the entire reason <code>visorcraft/mongreldb-php</code> is pure PHP, and the only way pure PHP works as a strategy is if the language gives you enough concrete features that you do not need to reach for native code to build a database client that feels modern. PHP 8.4 is the floor we ship on today, and PHP 8.5, released in November 2025, is the version that removes several of the remaining workarounds. This post is the tour of the features we use from 8.4, followed by the ones we are now adopting from 8.5, because the difference between a language version and a product decision is smaller than people pretend.</p>
<h2 id="asymmetric-visibility-and-the-shape-of-the-client">Asymmetric visibility and the shape of the client</h2>
<p>The <code>Database</code> class is the front door of the client, and it carries internal state that callers should read but never set: the base URL, the default transport, the current session token, and a few cached metadata objects. Before PHP 8.4, the honest choices were either public readonly properties, which expose the value for reading but still let anyone assign it during construction if the constructor is inside the class, or a pile of private properties with boilerplate getters that exist only to say &ldquo;this is not for you.&rdquo; Asymmetric visibility fixes this by letting the property be <code>public private(set)</code>, which means the world can read it and only the class can write it after construction, so internal mutation in methods like <code>withToken</code> still works while external callers are frozen out. This is exactly what a <code>Database</code> object needs.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Database</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">private</span>(<span style="color:#a6e22e">set</span>) <span style="color:#a6e22e">string</span> $baseUrl,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">private</span>(<span style="color:#a6e22e">set</span>) <span style="color:#a6e22e">TransportInterface</span> $transport,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span> $sessionToken <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">withToken</span>(<span style="color:#a6e22e">string</span> $token)<span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $clone <span style="color:#f92672">=</span> <span style="color:#66d9ef">clone</span> $this;
</span></span><span style="display:flex;"><span>        $clone<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">sessionToken</span> <span style="color:#f92672">=</span> $token;       <span style="color:#75715e">// private property, fine here
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $clone;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is not a cosmetic win; it is a stability win, because the public surface of the object now matches the contract we actually want, and there is no private getter a maintainer can forget to update when they rename a field. The client has a lot of value objects, <code>Column</code>, <code>Table</code>, <code>ResultSet</code>, <code>QueryPlan</code>, and asymmetric visibility keeps them honest without turning the source code into Java.</p>
<h2 id="readonly-properties-and-the-transport-configuration">Readonly properties and the transport configuration</h2>
<p>PHP 8.1 gave us readonly properties, PHP 8.2 added readonly classes, and by PHP 8.4 they are mature enough that we use them as the default for any object that is effectively a record. A transport configuration, for example, is a bundle of timeouts, retry counts, and header defaults that should be immutable once constructed, because a live request is already in flight and changing its timeout halfway through is a great way to get a bug nobody can reproduce. The combination of a <code>readonly</code> class and constructor property promotion means the entire configuration object collapses to a few lines.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">readonly</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TransportConfig</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">int</span> $connectTimeoutMs <span style="color:#f92672">=</span> <span style="color:#ae81ff">5000</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">int</span> $requestTimeoutMs <span style="color:#f92672">=</span> <span style="color:#ae81ff">30000</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">int</span> $maxRetries <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">array</span> $defaultHeaders <span style="color:#f92672">=</span> [],
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Readonly classes are not a free lunch, because <code>clone</code> becomes awkward and you need a <code>with</code> method or <code>clone</code> plus reassignment if you want a modified copy, but for transport config that is exactly what we want: the object is a snapshot, not a living document, and the database client treats it that way.</p>
<h2 id="array-find-functions-and-query-plan-navigation">Array find functions and query-plan navigation</h2>
<p>PHP 8.4 adds <code>array_find</code>, <code>array_find_key</code>, <code>array_any</code>, and <code>array_all</code>, and these are the kind of functions that sound trivial until you write the code that used to do the same thing with a loop and a flag. We hit them constantly when walking query-plan metadata, because a plan node is a nested array of operators, and we need to know whether any operator touches a vector index, or whether all operators are point lookups, or which node is the first one that references a JSON column. The old way was <code>array_filter(...)[0] ?? null</code> for find, and a hand-rolled <code>foreach</code> with <code>return false</code> for any/all, which is fine once but becomes noise when it appears twenty times in a client.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// Before: array_filter gives you all matches, then you take the first.
</span></span></span><span style="display:flex;"><span>$vectorNode <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_filter</span>($planNodes, <span style="color:#a6e22e">fn</span>($n) <span style="color:#f92672">=&gt;</span> $n[<span style="color:#e6db74">&#39;op&#39;</span>] <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;ann&#39;</span>)[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">??</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// After: intent is explicit, no allocation of the full filtered array.
</span></span></span><span style="display:flex;"><span>$vectorNode <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_find</span>($planNodes, <span style="color:#a6e22e">fn</span>($n) <span style="color:#f92672">=&gt;</span> $n[<span style="color:#e6db74">&#39;op&#39;</span>] <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;ann&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$hasVectorScan <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_any</span>($planNodes, <span style="color:#a6e22e">fn</span>($n) <span style="color:#f92672">=&gt;</span> $n[<span style="color:#e6db74">&#39;op&#39;</span>] <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;ann&#39;</span>);
</span></span><span style="display:flex;"><span>$allPointLookups <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_all</span>($planNodes, <span style="color:#a6e22e">fn</span>($n) <span style="color:#f92672">=&gt;</span> $n[<span style="color:#e6db74">&#39;op&#39;</span>] <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;point&#39;</span>);
</span></span></code></pre></div><p>The performance difference is small for most client workloads, but the readability difference is large, and a readable client is one that gets more contributions and fewer misuses.</p>
<h2 id="property-hooks-and-lazy-hydration">Property hooks and lazy hydration</h2>
<p>PHP 8.4 introduces property hooks, which let you attach <code>get</code> and <code>set</code> logic directly to a property declaration without writing a backing field and a separate accessor. We use them for lazy hydration on the <code>ResultSet</code> rows, where each row is a thin wrapper over a raw JSON array until the caller actually asks for a typed value. The hook is not magic; it is just a place to put the conversion logic so that the object remains a plain object while still doing the work on demand.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Row</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(<span style="color:#66d9ef">private</span> <span style="color:#66d9ef">array</span> $data) {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $name {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">get</span> <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">data</span>[<span style="color:#e6db74">&#39;name&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">int</span> $id {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">get</span> <span style="color:#f92672">=&gt;</span> (<span style="color:#a6e22e">int</span>) ($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">data</span>[<span style="color:#e6db74">&#39;id&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">object</span> $document {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">get</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">json_decode</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">data</span>[<span style="color:#e6db74">&#39;document&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;{}&#39;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The tradeoff is that property hooks can hide work, and a getter that does JSON decoding on every access is a trap if you call it in a loop. We keep the hooks thin and cache the heavier conversions in the constructor, so the hook is mostly a type-safe doorway, not a compute sink.</p>
<h2 id="ext-curl-in-php-84-the-http-foundation">ext-curl in PHP 8.4: the HTTP foundation</h2>
<p>The default transport is cURL, because cURL is the only HTTP client that is present on enough hosts that we can assume it exists, and PHP 8.0 gave us the OO <code>CurlHandle</code> and typed exceptions we use in the wrapper, with PHP 8.4 refinements to default encoding behavior keeping the implementation small. The client detects cURL at runtime, falls back to streams if it is missing, and lets you inject a PSR-18 or Symfony HttpClient adapter if you want something else, but the cURL path is the one we optimize for because it is the one that exists everywhere.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CurlTransport</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">TransportInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">\CurlHandle</span> $handle;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">TransportConfig</span> $config)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">curl_init</span>();
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_RETURNTRANSFER</span>, <span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_FOLLOWLOCATION</span>, <span style="color:#66d9ef">false</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_CONNECTTIMEOUT_MS</span>, $config<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">connectTimeoutMs</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_TIMEOUT_MS</span>, $config<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">requestTimeoutMs</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">request</span>(<span style="color:#a6e22e">string</span> $method, <span style="color:#a6e22e">string</span> $url, <span style="color:#a6e22e">string</span> $body <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_CUSTOMREQUEST</span>, $method);
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_URL</span>, $url);
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">curl_setopt</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, <span style="color:#a6e22e">CURLOPT_POSTFIELDS</span>, $body);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $raw <span style="color:#f92672">=</span> <span style="color:#a6e22e">curl_exec</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ($raw <span style="color:#f92672">===</span> <span style="color:#66d9ef">false</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ConnectionException</span>(<span style="color:#a6e22e">curl_error</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span><span style="color:#f92672">::</span><span style="color:#a6e22e">fromCurl</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>, $raw);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The cURL handle is reused for the lifetime of the transport object, which is better than creating a new handle per request, but it is not yet true connection persistence across the whole process, which is where PHP 8.5 comes in.</p>
<h2 id="php-85-the-pipe-operator-and-persistent-curl-sharing">PHP 8.5: the pipe operator and persistent cURL sharing</h2>
<p>PHP 8.5 shipped in November 2025 with the pipe operator, persistent cURL share handles, and <code>clone</code> with modifications. These are not theoretical wins for the client; they are the features that let us simplify the code we already have.</p>
<p>The pipe operator lets you write <code>$value |&gt; $this-&gt;normalize(...) |&gt; $this-&gt;send(...)</code> instead of nesting calls right-to-left or assigning to a temporary variable. For a client that chains encoding, validation, and request building, the pipe operator makes the middle of the call stack readable without turning it into a fluent builder.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// PHP 8.5
</span></span></span><span style="display:flex;"><span>$response <span style="color:#f92672">=</span> $requestBody
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">json_encode</span>(<span style="color:#f92672">...</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">|&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">transport</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">request</span>(<span style="color:#e6db74">&#39;POST&#39;</span>, $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">baseUrl</span> <span style="color:#f92672">.</span> <span style="color:#e6db74">&#39;/sql&#39;</span>, <span style="color:#f92672">...</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">Response</span><span style="color:#f92672">::</span><span style="color:#a6e22e">fromRaw</span>(<span style="color:#f92672">...</span>);
</span></span></code></pre></div><p>Persistent cURL share handles are the bigger deal. In PHP 8.4, the <code>CurlTransport</code> reuses a single <code>CurlHandle</code> for the lifetime of the transport object, but each transport object still starts a fresh TCP handshake. PHP 8.5 lets share handles persist across multiple PHP requests, which means an FPM or FrankenPHP worker can keep a warm connection to the database server instead of reopening it. We are wiring this into the transport layer so that the same <code>CurlTransport</code> can attach to a process-wide share handle when the runtime offers one, and the fallback for PHP 8.4 stays exactly the same.</p>
<p><code>clone</code> with modifications is the last piece. Readonly classes are great for value objects, but changing one field used to mean either a constructor spread with <code>get_object_vars</code> or a hand-written <code>with</code> method. PHP 8.5&rsquo;s <code>clone($this, ['field' =&gt; $value])</code> collapses that to a single expression, so the <code>withAlpha</code> style of method becomes small enough that we no longer need helper traits to keep it readable.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// PHP 8.5
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">withAlpha</span>(<span style="color:#a6e22e">int</span> $alpha)<span style="color:#f92672">:</span> <span style="color:#a6e22e">self</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">clone</span>($this, [<span style="color:#e6db74">&#39;alpha&#39;</span> <span style="color:#f92672">=&gt;</span> $alpha]);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="the-backward-compatibility-tradeoff">The backward-compatibility tradeoff</h2>
<p>The client currently supports PHP 8.4 as its minimum, and PHP 8.5 is now stable and widely available in the distributions we track. Runtime features like persistent cURL share handles can be guarded with <code>function_exists</code> and <code>curl_share_init</code>, so the same package can run faster on PHP 8.5 without breaking PHP 8.4, but syntax features like the pipe operator cannot be runtime-guarded. That means the pipe operator and <code>clone</code> with modifications will arrive as part of a major version bump that raises the minimum to PHP 8.5, while the cURL sharing lands as a backward-compatible performance improvement for anyone already on 8.5.</p>
<h2 id="the-modern-equivalent">The modern equivalent</h2>
<p>In 2010, a PHP database client would have been a thin wrapper around <code>mysql_connect</code> and the main challenge was escaping strings correctly. In 2026, the challenge is building a client that is safe, typed, and deployable without native code, and PHP 8.4 is the first version where that feels natural rather than defensive. PHP 8.5 is now the version that removes the remaining reasons a database client would need to touch anything outside Composer, not by adding one big feature, but by making the small workarounds unnecessary. That is the goal, and the language is finally moving in the same direction.</p>
]]></content:encoded></item><item><title>Pure PHP in 2026: A Database Client That Ships With Composer</title><link>https://www.mongreldb.com/articles/2026/07/pure-php-in-2026-a-database-client-that-ships-with-composer/</link><pubDate>Fri, 10 Jul 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/07/pure-php-in-2026-a-database-client-that-ships-with-composer/</guid><description>The mongreldb-php client installs with composer require, needs no phpize, no PECL, and no native ABI matching, because pure PHP is still a feature in 2026.</description><content:encoded><![CDATA[<p>PHP runs everywhere, and everywhere has a different OS version, a different architecture, and a hosting provider whose control panel says &ldquo;Native extensions: contact support.&rdquo; That sentence is not hypothetical; it describes the deployment environment for a substantial fraction of PHP applications in production today, and if your database client requires a compile step or a PECL install, you either open a support ticket or you pick a different database. MongrelDB&rsquo;s PHP client ships as pure PHP, installs with <code>composer require visorcraft/mongreldb-php</code>, and speaks to a <code>mongreldb-server</code> daemon over plain HTTP, which means the only thing it requires of the host is a working PHP 8.4+ runtime and a network path to the server.</p>
<h2 id="what-pure-php-actually-means-in-2026">What pure PHP actually means in 2026</h2>
<p>The phrase &ldquo;pure PHP&rdquo; has accumulated some baggage over the years, partly because PHP&rsquo;s early reputation as a slow interpreted language made &ldquo;pure PHP&rdquo; sound like &ldquo;slow PHP,&rdquo; and partly because the PHP ecosystem spent a decade leaning on C extensions as proof of seriousness. Neither of those associations is accurate anymore, and the PHP client does not trade on either of them.</p>
<p>What pure PHP means here is concrete: the entire client is written in PHP 8.4 syntax, it ships as a Composer package, it has no native code dependencies, and it does not call any function that requires a C extension. HTTP is handled by <code>curl</code> via ext-curl when PHP 8.5 persistent handle sharing is available (PHP 8.5 being the newer of the two runtimes, so the stream-context path remains the common case for now), and by regular stream contexts when it is not; both are in the standard PHP distribution and neither requires a compile step. JSON is handled by <code>json_decode</code> and <code>json_encode</code>, which have been built into PHP since 5.2 and have been reliably fast since 7.0. There is no binary .so to load, no phpize to run, no ABI version to match against the server&rsquo;s architecture, and no risk that <code>pecl install mongrel</code> fails because your host has disabled exec() in php.ini.</p>
<h2 id="the-install-experience">The install experience</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">composer</span> <span style="color:#66d9ef">require</span> <span style="color:#a6e22e">visorcraft</span><span style="color:#f92672">/</span><span style="color:#a6e22e">mongreldb</span><span style="color:#f92672">-</span><span style="color:#a6e22e">php</span>
</span></span></code></pre></div><p>That is the entire install. The package lands in <code>vendor/</code>, autoloading is configured, and you can instantiate the client in one line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Visorcraft\MongrelDB\Client</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$db <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Client</span>(<span style="color:#e6db74">&#39;https://db.example.com&#39;</span>, <span style="color:#a6e22e">token</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">getenv</span>(<span style="color:#e6db74">&#39;MONGRELDB_TOKEN&#39;</span>));
</span></span></code></pre></div><p>No bootstrap scripts, no extension loading in php.ini, no running <code>mongreldb-php-setup</code> as a post-install script. The Composer package ships the client, the type stubs, and the error hierarchy; you require the package and you are talking to the database.</p>
<h2 id="why-http-is-the-right-transport-for-a-pure-php-client">Why HTTP is the right transport for a pure PHP client</h2>
<p>The PHP client could have used a Unix socket or a raw TCP protocol with a binary framing layer, and in a pure-server context those choices would make sense for latency. But PHP is not a long-running process by default; PHP-FPM spawns a process per request or per few requests, and the typical PHP application opens a fresh database connection on every web request, closes it at the end, and does not reuse it unless you have gone out of your way to implement connection pooling. An HTTP client reuses keep-alive connections across requests in the same process lifetime, and when PHP-FPM recycles a worker the next request starts fresh, which is the same behavior the database would have expected anyway.</p>
<p>HTTP also means the PHP client works through every HTTP-aware proxy, load balancer, and API gateway that already exists in your infrastructure, and it means TLS termination happens at the normal layer rather than requiring a special database-facing certificate setup. You are already doing HTTP for every other external service your application calls; the database does not need to be special.</p>
<h2 id="what-you-give-up-and-what-you-do-not">What you give up and what you do not</h2>
<p>You give up the single-digit-microsecond in-process path, because HTTP adds a header processing layer that a raw TCP protocol would not have (a few hundred microseconds of framing overhead per operation, not a category shift), and you give up the ability to operate without a network path to the server, which is the embedded-database use case that MongrelDB handles differently through its Kit SDK. For a PHP web application that talks to a <code>mongreldb-server</code> over a LAN or a VPC, those tradeoffs are not on the critical path; the query execution time inside the database dominates the HTTP overhead by two orders of magnitude.</p>
<p>You do not give up type safety, because the client speaks a typed wire format and deserializes responses into PHP native types with a full exception hierarchy that maps HTTP status codes to domain exceptions, so a <code>404</code> from the server becomes a <code>NotFoundException</code> in your application code rather than a generic runtime error. You do not give up connection pooling in the PHP-FPM context, because the client uses persistent curl handles when PHP 8.5 is available, which means the underlying TCP connection is reused across requests in the same worker process without the TLS handshake cost on every page load. And you do not give up SQL surface area, because the wire format carries the same query plans and constraint semantics as the Kit SDK and the Rust core, so the PHP client is not a reduced-surface wrapper; it is the full client in a different transport.</p>
<h2 id="who-benefits-from-this-in-practice">Who benefits from this in practice</h2>
<p>The obvious answer is the shared-hosting developer who is on a cPanel machine with no root access and no ability to run phpize, and that answer is correct but incomplete. The more interesting case is the CI pipeline: a <code>composer require</code> in a Dockerfile that uses <code>php:cli</code> as its base image does not need any system package installed beyond the PHP runtime and Composer itself, which means your test matrix can cover PHP 8.4 and PHP 8.5 on the same Docker image without an extension compilation step slowing down every build. The pure PHP client makes your database client a first-class Composer dependency rather than a system-level install, and that distinction matters when your CI minutes are metered and your container build time is on the critical path.</p>
]]></content:encoded></item></channel></rss>