PHP - BlogFlockAll things PHPhttps://blogflock.com/list/VqY0J2026-10-20T10:26:02.000ZBlogFlockexakatFind the slowest PHPUnit tests - Rob Allenhttps://akrabat.com/?p=75952026-10-20T10:26:02.000ZRob Allen<p>We have a <a href="https://phpunit.de">PHPUnit</a> test suite that's taking a while, so I wanted to find out the which tests were taking the longest.</p>
<p>With the help of Google, I discovered the <tt>--log-junit phpunit-results.xml</tt> command line option to <tt>phpunit</tt> which creates an XML file with the info I need. Running on <a href="https://www.slimframework.com">Slim</a>'s codebase, we create the XML file with: </p>
<pre lang="bash">
$ ./vendor/bin/phpunit --log-junit phpunit-results.xml
...
Tests: 436, Assertions: 832, PHPUnit Notices: 37.
</pre>
<p>It looks like this (formatted by me):</p>
<pre lang="bash">
$ head -n 7 phpunit-results.xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite
name="/Users/rob/Projects/slim/slimphp/Slim/phpunit.xml"
tests="436"
assertions="832"
errors="0"
failures="0"
skipped="0"
time="0.595106">
<testsuite
name="Slim Test Suite"
tests="436"
assertions="832"
errors="0"
failures="0"
skipped="0"
time="0.595106">
<testsuite
name="Slim\Tests\Routing\RouteContextTest"
file="/Users/rob/Projects/slim/slimphp/Slim/tests/Routing/RouteContextTest.php"
tests="5"
assertions="14"
errors="0"
failures="0"
skipped="0"
time="0.004264">
<testcase
name="testCanCreateInstanceWithoutRoute"
file="/Users/rob/Projects/slim/slimphp/Slim/tests/Routing/RouteContextTest.php"
line="43"
class="Slim\Tests\Routing\RouteContextTest"
classname="Slim.Tests.Routing.RouteContextTest"
assertions="4"
time="0.002566"/>
<testcase
name="testCanCreateInstanceWithoutBasePathAndThrowExceptionIfGetBasePathIsCalled"
file="/Users/rob/Projects/slim/slimphp/Slim/tests/Routing/RouteContextTest.php"
line="57"
class="Slim\Tests\Routing\RouteContextTest"
classname="Slim.Tests.Routing.RouteContextTest"
assertions="4"
time="0.000505"/>
</pre>
<p>Each <tt>testcase</tt> has a <tt>time</tt> attribute, so I asked my LLM to create some Python to sort by time and list the top 5:</p>
<pre lang="bash">
$ python3 -I -c '
import sys, heapq, xml.etree.ElementTree as ET
cases = ET.parse(sys.argv[1]).iter("testcase")
slowest = heapq.nlargest(5, cases, key=lambda c: float(c.get("time", 0)))
for c in slowest:
print(f"{float(c.get("time", 0)):8.3f}s {c.get("class")}::{c.get("name")}")
' phpunit-results.xml
</pre>
<p>For this test, I get:</p>
<pre>
$ python3 -I -c 'import sys, heapq, xml.etree.ElementTree as ET; print(*(f"{float(c.get("time", 0)):8.3f}s {c.get("class")}::{c.get("name")}" for c in heapq.nlargest(5, ET.parse(sys.argv[1]).iter("testcase"), key=lambda c: float(c.get("time", 0)))), sep="\n")' phpunit-results.xml
0.028275 Slim\Tests\AppTest testAnyRoute
0.026482 Slim\Tests\AppTest testInvokeSequentialProcessAfterAddingAnotherRouteArgument
0.020176 Slim\Tests\AppTest testInvokeSequentialProcessToAPathWithOptionalArgsAndWithoutOptionalArgsAndKeepSetedArgs
0.016482 Slim\Tests\MiddlewareDispatcherTest testExecutesMiddlewareLastInFirstOut
0.013571 Slim\Tests\Factory\AppFactoryTest testCreateAppWithContainerUsesContainerDependenciesWhenPresent
</pre>
<p>Obviously, Slim's tests are quick, so there's no much to see here, but for my client's project, the numbers are larger!</p>Every Road Around the Zend Engine - Exakathttps://www.exakat.io/?p=165112026-10-19T15:39:14.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/directions.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16512" src="https://www.exakat.io/wp-content/uploads/2026/10/directions.640-300x300.jpg" alt="Every Road Around the Zend Engine" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/directions.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/directions.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/directions.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/directions.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/directions.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/directions.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>Every Road Around the Zend Engine</h1>
<p>I’ve been using PHP forever, and so I’ve relied on the Zend Engine. After all, it is the core of PHP. The PHP source goes into the Zend Engine, gets parsed into an AST, compiled to opcodes, and executed. One request, one process. That’s just what <code>php</code> does when you run it, and it’s been true in its broad shape since Zend Engine 2 shipped with PHP 5 in 2004. Set in the PHP era, it is roughly the end of antiquity.</p>
<p>Nowadays, as in October 2026, it is no longer the only way to get from a <code>.php</code> file to a run program. Over the last fifteen years, and especially the last three, a whole ecosystem has grown up around the idea that “run this PHP script” doesn’t have to mean “hand it to the C interpreter Rasmus Lerdorf’s successors maintain”. Some projects replace the engine outright. Some keep the real one and put it somewhere unexpected. Some don’t touch it at all and just change what it’s running inside of. They depart from the canonical pipeline at genuinely different points, which turns out to be the more useful way to sort them than by vendor or by benchmark.</p>
<p>None of this is entirely new, either. <a href="https://github.com/jphp-group/jphp">JPHP</a> has been compiling PHP straight to JVM bytecode since the early 2010s, and picked up 1.7k GitHub stars doing it: sadly, its last real target is PHP 7.1, with only pieces of 7.2–7.4 layered on. It is so old that upgrading to it would be error prone in most 2026 codebases. We won’t include it in this timeline of PHP engines, but acknowledge the tremendous courage it needed, back in these ancient times. The projects that follow are the ones still tracking a current PHP version. This means PHP 8.5, because PHP 8.6 is still a month away.</p>
<h2 id="toc_1">The panorama</h2>
<table>
<thead>
<tr>
<th>Project</th>
<th>What it replaces</th>
<th>Departs the pipeline at</th>
<th>Output</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Zend Engine</strong></td>
<td>nothing, this is the baseline</td>
<td>–</td>
<td>opcodes, interpreted/JIT’d per request</td>
</tr>
<tr>
<td><strong>KPHP</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>native binary, via generated C++</td>
</tr>
<tr>
<td><strong>TypePHP</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>native binary, extension, or shared lib</td>
</tr>
<tr>
<td><strong>elephc</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>native binary, assembly directly, no C++ step</td>
</tr>
<tr>
<td><strong>Manticore</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>native binary, via LLVM IR</td>
</tr>
<tr>
<td><strong>Quercus</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>JVM bytecode</td>
</tr>
<tr>
<td><strong>PeachPie</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>.NET IL</td>
</tr>
<tr>
<td><strong>php.rs</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>its own Rust-native VM</td>
</tr>
<tr>
<td><strong>phpr</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>its own Rust-native bytecode VM</td>
</tr>
<tr>
<td><strong>zphp</strong></td>
<td>the compiler and the runtime</td>
<td>before parsing</td>
<td>its own Zig-native runtime</td>
</tr>
<tr>
<td><strong>php-wasm / WordPress Playground</strong></td>
<td>nothing, recompiles the real engine</td>
<td>after the engine is built</td>
<td>the same engine, running in WASM</td>
</tr>
<tr>
<td><strong>FrankenPHP</strong></td>
<td>nothing, links the real engine</td>
<td>after the engine is built</td>
<td>the same engine, embedded in a Go process</td>
</tr>
<tr>
<td><strong>RoadRunner</strong></td>
<td>nothing, supervises the real engine</td>
<td>outside the pipeline entirely</td>
<td>long-lived worker processes, not one-shot</td>
</tr>
</tbody>
</table>
<p>HHVM, one historical ancestor of the “replace the compiler” family, is covered separately below. It no longer belongs on this table, for reasons that are themselves instructive.</p>
<h2 id="toc_2">The canonical path, briefly</h2>
<p>It is worth starting precisely, since everything else in this blog is defined relative to it. <code>php script.php</code> does four things: lex and parse the source into an AST, compile the AST to Zend opcodes, hand those opcodes to the Zend VM, which either interprets them one at a time or, for code the PHP 8.0 JIT decides is worth it, compiles them to machine code on the fly. One process, one request, memory cleaned down at the end. Every alternative below is a departure from some part of that sequence, never all of it invented from nothing; even the projects that discard the C source still have to agree on what <code>array_merge()</code> does.</p>
<h2 id="toc_3">Replace the compiler and its target</h2>
<p>The first one is the most drastic move: write your own front end for PHP syntax, and instead of Zend opcodes, emit something else entirely. Everything in this family shares one constraint: none of them implements all of PHP, because doing so is the whole difficulty of the exercise, and each one draws the line differently.</p>
<h3 id="toc_4">Straight to native code</h3>
<p><a href="https://github.com/VKCOM/kphp"><strong>KPHP</strong></a>, built at VK.com and open-sourced in 2020, compiles a restricted subset of PHP to C++, then to a native binary that serves HTTP itself: it has no interpreter, no opcode cache, no “slow startup” phase, because there’s no startup to be slow.</p>
<p><a href="https://github.com/swoole/typephp"><strong>TypePHP</strong></a> does the same C++ round-trip but is, charmingly, self-hosting: its own compiler is PHP code, compiled by an earlier version of itself into the <code>tpc</code> binary you run. It’ll also emit a loadable extension or a shared library from the same source, if a full standalone binary is more than you need. It’s GPL-3 licensed, and its current Packagist listing wants PHP 8.4 or 8.5 to build.</p>
<p><a href="https://elephc.dev/"><strong>elephc</strong></a> skips the C++ intermediate step and goes straight to assembly, that is parse, type-check, lower to native code, assemble, link, with <code>eval()</code> available through an experimental bridge to a statically linked interpreter, for the one thing you genuinely can’t resolve at compile time. The current release, 0.26.2, targets macOS and Linux.</p>
<p><a href="https://github.com/manticorephp/compiler"><strong>Manticore</strong></a> is a PHP AOT compiler written in PHP, with an LLVM backend: it lowers a large subset of PHP 8.5+ through its own typed intermediate representation to LLVM IR, and produces standalone arm64 or x86_64 binaries that need no interpreter and no <code>php.ini</code>. The compiler compiles itself to a byte-identical fixpoint, and its README reports native binaries beating the Zend interpreter in all 47 comparable benchmark cases. Those are claims worth checking yourself, and the project’s own limitations list, such as integer overflow wraps instead of promoting to float, no <code>extract()</code>, shows exactly where the feature coverage stops.</p>
<div>
<pre><code class="language-bash"># TypePHP: same source, three different outputs
tpc build app.php -o app # standalone native binary
tpc build app.php --ext -o app.so # loadable PHP extension
tpc build app.php --lib -o libapp # shared library, callable from C</code></pre>
</div>
<h2 id="toc_5">Straight to someone else’s VM</h2>
<p><a href="https://github.com/CleverCloud/Quercus"><strong>Quercus</strong></a>, Caucho’s PHP-on-Java implementation from the Resin application server, compiles PHP source to Java source and lets <code>javac</code> finish the job. PHP scripts becoming <code>.class</code> files that run in whatever JVM your Java stack already has.</p>
<p><a href="https://github.com/peachpiecompiler/peachpie"><strong>PeachPie</strong></a> does the .NET equivalent: PHP compiled through Roslyn to CLR IL, JIT’d by the .NET runtime, with the payoff that a PeachPie-compiled Composer package can be <code>new</code>‘d directly from C#.</p>
<p>And then there’s the newest and most crowded corner of this family: reimplementing PHP wholesale outside C entirely, mostly in Rust but not only. <a href="https://github.com/alexisbouchez/php.rs"><strong>php.rs</strong></a> bills itself as the complete interpreter, compiler, and standard library rebuilt from zero, over 100,000 lines in.</p>
<p><strong><a href="https://phprust.com/">phpr</a></strong> takes the more conservative VM-architecture route: parser, IR, bytecode compiler, dispatch loop. It measures itself against something genuinely hard to fake: whether it can install and serve WordPress.</p>
<p><strong><a href="https://github.com/nvms/zphp">zphp</a></strong> is the same wholesale-reimplementation bet made in Zig instead: a from-scratch PHP 8.x runtime with its own HTTP server, database drivers, and a <code>--compile</code> flag for standalone binaries. It is still tiny next to the Rust entrants, with single-digit GitHub stars, and openly built as an AI-maintained experiment rather than a community project. The commit history never sleeps, but it also never argues on the mailing list. Indeed, it is attacking exactly the same problem.</p>
<p>There’s enough activity here that “reimplement PHP outside the Zend C source” has quietly become its own genre, alongside smaller entrants like <code>phprs</code> and <code>phpl</code> that haven’t attempted anything close to full-language coverage.</p>
<h3 id="toc_6">The ancestor of this whole family is Facebook’s HipHop</h3>
<p>It was released in 2010: PHP compiled to C++, then to a binary, on the theory that facebook.com’s PHP was CPU-bound and interpretation was the tax. It worked, roughly double the throughput, until it didn’t scale further, so in 2013 Facebook replaced it with HHVM, a JIT bytecode VM that eventually beat HipHop’s own numbers. The instructive part isn’t the speedup, it’s what happened next: by 2018, HHVM’s maintainers concluded that chasing PHP compatibility was slowing down the language features they actually wanted, and cut the cord. Hack today is PHP’s syntax with the serial numbers filed off, running on a VM that no longer promises to run your Laravel app. Every project in this section is implicitly betting it won’t end up there, a bet HHVM itself already lost.</p>
<h2 id="toc_7">Keep the real Zend engine, change its house</h2>
<p>This section is for a breed of engines with a different style: don’t write a new front end at all. Take the actual Zend Engine C source, the one that ships with PHP, and put it somewhere it wasn’t designed to run initially. Embedding is the name of the game.</p>
<p><a href="https://github.com/seanmorris/php-wasm"><strong>php-wasm</strong></a> and the <a href="https://wordpress.github.io/wordpress-playground/">WordPress Playground</a> project both do this via Emscripten: compile libphp itself to WebAssembly, and you get PHP running client-side in a browser tab, sandboxed, with SQLite standing in for a filesystem. It’s the reason you can now try WordPress at wordpress.org without a server existing anywhere: the “server” is a WASM module your browser downloaded. Playground now ships a separate php-wasm build for each PHP version from 7.2 to 8.5, and it has dropped support for 7.2 and 7.3.</p>
<p><a href="https://frankenphp.dev/"><strong>FrankenPHP</strong></a> does the embedding move instead of the cross-compilation move: it links libphp into a Go binary via cgo, so the actual Zend Engine runs inside the same process as the Caddy web server, one thread per PHP execution (Windows support arrived in 2026), with Go orchestrating the thread pool through a state machine and C handling the SAPI lifecycle. You end up with a single static binary that is your app, your PHP runtime, and your web server, with zero IPC between them. It is one fewer box-and-arrow diagram for the ops team to keep arguing about, but the opcodes running inside it are exactly the ones <code>php-fpm</code> would have produced.</p>
<p>Neither of these projects had to solve PHP-the-language a second time, which is precisely why their compatibility story is so much better than in the first section: they didn’t reimplement anything, they relocated PHP.</p>
<h2 id="toc_8">Leave the engine alone, change what’s around it</h2>
<p>The least drastic and, in production terms, probably the most consequential family: don’t touch the pipeline at all. A normal PHP process runs a normal request exactly as it always has. What moves is everything outside that boundary.</p>
<p><a href="https://roadrunner.dev/"><strong>RoadRunner</strong></a> is a Go process manager that starts a pool of long-lived PHP workers and keeps feeding them requests over a binary protocol (Goridge), instead of the PHP-FPM model of spawning and tearing down a process, or at least its framework bootstrap — on every request, which is PHP-FPM’s way of never quite unpacking its suitcase. The Zend Engine inside each worker does exactly what it always did; what disappeared is the reload cost, paid once at worker startup instead of per request.</p>
<h2 id="toc_9">The diagram</h2>
<figure><a href="https://www.exakat.io/wp-content/uploads/2026/10/php-execution-panorama-1.png"><img decoding="async" class="alignleft wp-image-16516 size-full" src="https://www.exakat.io/wp-content/uploads/2026/10/php-execution-panorama-1.png" alt="" width="815" height="474" srcset="https://www.exakat.io/wp-content/uploads/2026/10/php-execution-panorama-1.png 815w, https://www.exakat.io/wp-content/uploads/2026/10/php-execution-panorama-1-300x174.png 300w, https://www.exakat.io/wp-content/uploads/2026/10/php-execution-panorama-1-500x291.png 500w, https://www.exakat.io/wp-content/uploads/2026/10/php-execution-panorama-1-300x174@2x.png 600w" sizes="(max-width: 815px) 100vw, 815px" /></a></figure>
<figure></figure>
<h2 id="toc_10">What the panorama shows</h2>
<p><strong>The starting point predicts the compatibility story</strong>. Everything in lane 1 has to reimplement PHP’s semantics from scratch, in a foreign environment: references, magic methods, the coercion rules, the standard library’s thousand small inconsistencies, which is exactly why KPHP, TypePHP, and elephc all cover a subset of PHP rather than PHP. Lane 2 sidesteps that problem entirely by never rewriting anything: php-wasm and FrankenPHP inherit the real engine’s compatibility, bugs and all, because it is the real engine. Lane 3 doesn’t even ask the question: RoadRunner runs ordinary PHP because nothing about ordinary changed.</p>
<p><strong>Speed and portability pull in different directions</strong>. The native-compilation projects in lane 1 exist because interpretation has a cost that JIT alone hasn’t eliminated for every workload. KPHP’s own numbers put it at three to ten times faster than plain PHP, which is the kind of numbers you build a compiler for. The JVM/.NET/WASM branch of lane 1 isn’t chasing speed at all; Quercus and PeachPie exist so a PHP codebase can sit inside a Java or .NET deployment, calling and being called by that ecosystem’s own code. Two different problems, sharing only the shape of “write a new compiler.”</p>
<p><strong>Lane 3 is where most production PHP will actually feel this</strong>. Nobody’s rewriting a Laravel app to fit KPHP’s subset, but plenty of teams are already running it inside a RoadRunner worker or a FrankenPHP binary. That is because those get the payoff without rewriting anything at all.</p>
<h2 id="toc_11">The bigger picture</h2>
<p>What ties all twelve of these together, more than any shared codebase or shared goal, is that every single one is still measured against the same yardstick: does it run real PHP. The WordPress install with fifty plugins, the Composer tree with a dependency that leans on <code>eval()</code>, the codebase where someone passed a reference into a closure in 2016 and it’s load-bearing now, and nobody currently employed knows why. phpr’s own benchmark is literally the WordPress torture test, and PeachPie brags about running it. KPHP doesn’t try and says so upfront. That’s a true challenge: it’s the actual hard problem every project on this list is solving a different slice of, and the panorama above is really a map of how much of real PHP each one has chosen to promise.</p>
<p>The yardstick is also moving. PHP 8.6 is due on November 19, 2026, and every project above is chasing a target that keeps moving every year.</p>
<p>Which raises a question: language-level tools already audit a codebase for type coverage, or, as I’ve written about before, for where validation silently doesn’t happen. Nothing audits a codebase for engine coverage: which functions, which reference patterns, which magic-method edge cases a given PHP source tree actually exercises, and therefore which of these eleven routes it could safely take. Until that audit exists, asking if my app run on X stays a question you answer by trying it and watching what breaks.</p>
<p>PHP has a wild new frontier!</p>
<p>The post <a href="https://www.exakat.io/every-road-around-the-zend-engine/">Every Road Around the Zend Engine</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>PHP has a Spoofchecker: A 14-Year-Old Tool You’re Probably Not Using - Exakathttps://www.exakat.io/?p=165062026-10-17T14:34:38.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/spoof.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16507" src="https://www.exakat.io/wp-content/uploads/2026/10/spoof.640-300x300.jpg" alt="PHP has a Spoofchecker: A 14-Year-Old Tool You're Probably Not Using" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/spoof.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/spoof.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/spoof.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/spoof.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/spoof.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/spoof.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>PHP has a <code>Spoofchecker</code>: A 14-Year-Old Tool You’re Probably Not Using</h1>
<p>The <code>Spoofchecker</code> class has shipped with PHP since 5.4, released in March 2012. That’s the same year the iPhone 4S was current, “Gangnam Style” dropped, and PHP 5.3 was still the dominant version on the west. Fourteen years later, most PHP developers, including me, have never heard of it. Yet, it’s still the single best PHP built-in defense against Unicode-based visual spoofing.</p>
<h2 id="toc_1">Start here: when visual traps aren’t a runtime problem</h2>
<p>Before going further, you can read my recent blog <strong><a href="https://www.exakat.io/when-php-reads-something-else-visual-traps-in-source-code/">When PHP reads something else: Visual traps in source code</a></strong>. It’s about a related yet distinct problem: characters in your source code that look identical to other characters, silently breaking comparisons, identifiers, or function calls. The post focuses on the editor side: the developer’s eye can’t tell the difference between an ASCII hyphen <code>-</code> and a Unicode minus <code>−</code>: can you? Or between a Latin <code>o</code> and a Greek <code>ο</code>; between a regular space and a non-breaking space; … there are so many.</p>
<p><code>Spoofchecker</code> is the runtime counterpart to that problem. Where Exakat helps you spot visual traps in your codebase, <code>Spoofchecker</code> helps you spot them in user input: the strings your application accepts from forms, APIs, emails, and URLs. It’s the same family of attack: characters that look the same to a human but are different to a machine. The difference is who’s being fooled, aka a developer vs. an end user, and where the trap is set, aka your repo vs. incoming data.</p>
<p>If Exakat’s article is about keeping your own house clean, <code>Spoofchecker</code> is about recognizing when someone else is tracking dirt across your floor. This is often underestimated.</p>
<h2 id="toc_2">Three configurations worth knowing in detail</h2>
<p>Out of the box, <code>Spoofchecker</code> runs a default set of checks. But the real power is in <code>setChecks()</code>, where you configure exactly the checks you want. Here are three configurations I keep coming back to.</p>
<h3 id="toc_3">1. Mixed-script confusables: the brand-impersonation check</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::MIXED_SCRIPT_CONFUSABLE);
var_dump($checker->areConfusable('раypal.com', 'paypal.com')); // true
var_dump($checker->areConfusable('example.com', 'paypal.com')); // false
?>
</pre>
</div>
<p>The string <code>раypal.com</code> looks identical to <code>paypal.com</code> in almost any font, but the first two characters are Cyrillic: <code>р</code> is U+0440 and <code>а</code> is U+0430, not the Latin <code>p</code> and <code>a</code>. <code>areConfusable()</code> compares two whole strings and tells you whether they look alike, which is the question a brand-impersonation check asks. This is the configuration that catches the classic homograph domain attack.</p>
<h3 id="toc_4">2. Invisible characters: the password and ID hygiene check</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::INVISIBLE);
$spoofed = "ad\xE2\x80\x8Bmin"; // 'ad' + zero-width space + 'min'
var_dump($checker->isSuspicious($spoofed)); // true
?>
</pre>
</div>
<p>Zero-width spaces, zero-width joiners, and certain combining marks are invisible to humans but live in the string. <code>INVISIBLE</code> should catch them. This is the configuration you want for any context where the user might retype or visually verify a string: passwords, 2FA seeds, share codes, short IDs. A zero-width character in a password is a guaranteed support ticket. Copy paste is not your friend.</p>
<h3 id="toc_5">3. Restriction level: the policy-grade check</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$checker = new Spoofchecker();
$checker->setRestrictionLevel(Spoofchecker::ASCII);
var_dump($checker->isSuspicious('café')); // true (non-ASCII)
var_dump($checker->isSuspicious('hello')); // false
?>
</pre>
</div>
<p>The restriction level is the most opinionated setting: it grades the whole string against one of Unicode’s identifier profiles. It’s set with <code>setRestrictionLevel()</code>, not with a check constant, so keep it separate from <code>setChecks()</code>. The levels are:</p>
<table>
<thead>
<tr>
<th>Level</th>
<th>What it allows</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Spoofchecker::ASCII</code></td>
<td>ASCII only</td>
</tr>
<tr>
<td><code>Spoofchecker::SINGLE_SCRIPT_RESTRICTIVE</code></td>
<td>A single script only</td>
</tr>
<tr>
<td><code>Spoofchecker::HIGHLY_RESTRICTIVE</code></td>
<td>ASCII + one other script (e.g., Latin + one CJK)</td>
</tr>
<tr>
<td><code>Spoofchecker::MODERATELY_RESTRICTIVE</code></td>
<td>Allows reasonable mixtures, blocks exotic ones</td>
</tr>
<tr>
<td><code>Spoofchecker::MINIMALLY_RESTRICTIVE</code></td>
<td>All identifier characters</td>
</tr>
<tr>
<td><code>Spoofchecker::UNRESTRICTIVE</code></td>
<td>Anything goes</td>
</tr>
</tbody>
</table>
<p>This is the configuration to reach for when you have a policy about what an identifier should look like, not just a question about spoofing. For machine-generated IDs, URLs, and API tokens, <code>ASCII</code> or <code>HIGHLY_RESTRICTIVE</code> is right. For human-facing display names where multilingual input is legitimate, drop to <code>MODERATELY_RESTRICTIVE</code>.</p>
<h2 id="toc_6">The rest of the checks</h2>
<p><code>Spoofchecker</code> exposes more checks than the three above. PHP 8.4 added <code>IGNORE_SPACE</code>, <code>CASE_INSENSITIVE</code>, <code>ADD_CASE_MAPPINGS</code> and <code>SIMPLE_CASE_INSENSITIVE</code>, which the manual documents. Here’s the full list, briefly:</p>
<ul>
<li><strong><code>SINGLE_SCRIPT_CONFUSABLE</code></strong>: confusable letters within one script (e.g., <code>0</code> and uppercase <code>O</code>, <code>l</code> and <code>1</code>). Useful for short identifiers.</li>
<li><strong><code>WHOLE_SCRIPT_CONFUSABLE</code></strong>: an entire script that visually mimics another. This is the case of Greek, that resembles Latin. This check is broader than mixed-script.</li>
<li><strong><code>ANY_CASE</code></strong>: ICU’s “any case” option. It does <strong>not</strong> make comparisons case-insensitive: on PHP 8.5.11, <code>areConfusable()</code> with it raised an ICU <code>U_INVALID_STATE_ERROR</code> warning. On PHP 8.6-dev there is no warning, but <code>Admin</code> and <code>ADMIN</code> still don’t match. Normalize case yourself, as shown in the username example below.</li>
<li><strong><code>CHAR_LIMIT</code></strong>: a character-limit check. I haven’t verified its exact behavior, so check the manual before relying on it.</li>
<li><strong><code>MIXED_SCRIPT_CONFUSABLE</code></strong>: the brand-impersonation check illustrated above.</li>
<li><strong><code>INVISIBLE</code></strong>: the invisible-character check illustrated above.</li>
<li><strong><code>SINGLE_SCRIPT</code></strong>: the identifier mixes scripts, e.g. <code>ceo@раypal.com</code>. This is the check the egulias/EmailValidator library uses (see the conclusion).</li>
</ul>
<p>Combine them with bitwise OR: <code>$checker->setChecks(Spoofchecker::MIXED_SCRIPT_CONFUSABLE | Spoofchecker::INVISIBLE | Spoofchecker::SINGLE_SCRIPT);</code></p>
<p>There are also two helper methods that don’t involve <code>setChecks</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Does string A look like string B to a human?
$checker->areConfusable('Admin', 'Аdmin', $errorCode); // true
// Restrict allowed scripts to specific locales (affects restriction level checks)
$checker->setAllowedLocales('en_US,ru_RU,el_GR');
?>
</pre>
</div>
<p>Note the third argument. <code>areConfusable()</code> and <code>isSuspicious()</code> both take an <code>int &$errorCode</code>, not a human-readable message. You get a code, not a sentence, so map it to your own message if you need one.</p>
<h3 id="toc_7">New in PHP 8.6: skeletons and bidi comparison</h3>
<p>PHP 8.6 adds <code>getSkeleton()</code>, <code>getBidiSkeleton()</code> and <code>areBidiConfusable()</code>, plus the <code>LTR</code> and <code>RTL</code> constants. I tested them on PHP 8.6.0-dev (built 15 October 2026); none of them exist in PHP 8.5. A skeleton maps each confusable character to a canonical form, which makes it a good way to show someone why a string looks suspicious:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$checker = new Spoofchecker();
// Does Damien looks suspicious enough to you?
$checker->getSkeleton('Damien'); // 'Darnien'
// The Cyrillic letters map to Latin, and `m` maps to `rn`
$checker->getSkeleton('раypal.com'); // 'paypal.corn'
// Bidi-aware skeleton, given the text direction
$checker->getBidiSkeleton(Spoofchecker::LTR, 'Elephpant'); // 'Elephpant'
// Bidi-aware comparison
$checker->areBidiConfusable(Spoofchecker::LTR, 'Аdmin', 'Admin'); // true
?>
</pre>
</div>
<h2 id="toc_8">Various usages of Spoofchecker</h2>
<p>The <code>Spoofchecker</code> class is general-purpose, but in practice it shines in a handful of PHP situations. Here are a few of them.</p>
<h3 id="toc_9">Domain names and URLs</h3>
<p>This is the headline use case. When your application accepts a URL from a user, for link preview, webmention, bookmark, social share, you’re implicitly trusting that the hostname is what it claims to be. IDN domains, aka Punycode, make this worse, because <code>xn--80ak6aa92e.com</code> decodes to <code>аррӏе.com</code>, an all-Cyrillic string that looks like <code>apple.com</code> (Don’t sue me!).</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function isLikelySpoofedUrl(string $url): bool
{
$host = parse_url($url, PHP_URL_HOST) ?? '';
if ($host === '') {
return false;
}
// Decode IDN if intl is available
$decoded = idn_to_utf8($host, IDNA_NONTRANSITIONAL_TO_UNICODE) ?: $host;
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::MIXED_SCRIPT_CONFUSABLE);
$checker->setRestrictionLevel(Spoofchecker::HIGHLY_RESTRICTIVE);
return $checker->isSuspicious($decoded);
}
isLikelySpoofedUrl('https://paypal.com'); // false
isLikelySpoofedUrl('https://раypal.com'); // true
isLikelySpoofedUrl('https://xn--80ak6aa92e.com'); // true (Punycode → Cyrillic apple)
?>
</pre>
</div>
<p>Pair it with <code>idn_to_ascii()</code> for full domain validation: <code>Spoofchecker</code> looks at the visual content, <code>idn_to_ascii()</code> confirms the domain is well-formed.</p>
<h3 id="toc_10">Username and handle registration</h3>
<p>Account impersonation is one of the most common abuse vectors in any platform with user-visible names. Without <code>Spoofchecker</code>, the only way to prevent <code>Аdmin</code>, with a Cyrillic <code>А</code>, from sneaking past a “don’t register as <code>admin</code>” check is to maintain your own confusable mapping. Usually, which you’ll get it wrong. Trust me, bro.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function isUsernameImpersonation(string $newName, array $existingNames): bool
{
$checker = new Spoofchecker();
$checker->setChecks(
Spoofchecker::MIXED_SCRIPT_CONFUSABLE
| Spoofchecker::SINGLE_SCRIPT_CONFUSABLE
);
// Uppercase both sides: `0` looks like uppercase `O`, not lowercase `o`
foreach ($existingNames as $existing) {
if ($checker->areConfusable(mb_strtoupper($newName), mb_strtoupper($existing), $errorCode)) {
return true;
}
}
return false;
}
$existing = ['Admin', 'moderator', 'support'];
isUsernameImpersonation('Аdmin', $existing); // true
isUsernameImpersonation('moderator', $existing); // true (exact match handled by other logic)
isUsernameImpersonation('m0derat0r', $existing); // true (single-script confusable)
isUsernameImpersonation('alice', $existing); // false
?>
</pre>
</div>
<p>Case is the awkward part. Uppercasing both sides, as above, makes the comparison case-insensitive for the letters that matter, and it doesn’t rely on the <code>ANY_CASE</code> option.</p>
<h3 id="toc_11">Email addresses and display names</h3>
<p><code>filter_var($email, FILTER_VALIDATE_EMAIL)</code> checks the syntax, and it rejects raw Unicode in the domain. So the attacker sends the Punycode form instead: <code>ceo@xn--ypal-43d9g.com</code> is <code>ceo@раypal.com</code>, and it passes validation. The syntax check does its job; it just can’t see that <code>раypal</code> is visually <code>paypal</code>. Display names are even worse: they’re free-form text, so they are prone to social engineering tactics.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function isLikelySpoofedEmail(string $email): bool
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false; // let syntax validation handle this elsewhere
}
[$local, $domain] = explode('@', $email, 2);
$decodedDomain = idn_to_utf8($domain, IDNA_NONTRANSITIONAL_TO_UNICODE) ?: $domain;
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::SINGLE_SCRIPT);
return $checker->isSuspicious($local . '@' . $decodedDomain);
}
function isLikelySpoofedDisplayName(string $name, array $trustedNames): bool
{
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::MIXED_SCRIPT_CONFUSABLE);
foreach ($trustedNames as $trusted) {
if ($checker->areConfusable(mb_strtoupper($name), mb_strtoupper($trusted), $errorCode)) {
return true;
}
}
return false;
}
isLikelySpoofedEmail('ceo@xn--ypal-43d9g.com'); // true (decodes to ceo@раypal.com)
isLikelySpoofedEmail('ceo@paypal.com'); // false
isLikelySpoofedDisplayName('Аlex from IT', ['Alex from IT']); // true
?>
</pre>
</div>
<h3 id="toc_12">Password policy: the lesser taken road</h3>
<p>Passwords with zero-width characters are a support nightmare: the user thinks they typed <code>hunter2</code>, but they copy pasted it, and the database stores <code>hunter\xE2\x80\x8B2</code>: the login fails forever. It is recommended to reject these at registration and update time.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function isPasswordSafeToStore(string $password): bool
{
$checker = new Spoofchecker();
$checker->setChecks(Spoofchecker::INVISIBLE);
return !$checker->isSuspicious($password);
}
isPasswordSafeToStore('correct horse battery staple'); // true
isPasswordSafeToStore("correct\xE2\x80\x8B horse"); // false
?>
</pre>
</div>
<p>You probably don’t want to enforce full <code>ASCII</code> restriction on passwords: users legitimately use accented characters and Cyrillic in passphrases. But blocking invisible characters is almost always correct.</p>
<h3 id="toc_13">Content moderation and banned-word filters</h3>
<p>Word filters are a classic spoofing target. If you block the string <code>viagra</code> in a comment, the first thing an attacker does is substitute visually identical Cyrillic characters: <code>vіagra</code>, with a Cyrillic <code>і</code>. The second thing they do is use single-script confusables: <code>v1agra</code>. <code>Spoofchecker</code> catches both.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function containsBannedWord(string $text, array $bannedWords): bool
{
$checker = new Spoofchecker();
$checker->setChecks(
Spoofchecker::SINGLE_SCRIPT_CONFUSABLE
| Spoofchecker::MIXED_SCRIPT_CONFUSABLE
);
foreach ($bannedWords as $word) {
// Compare the whole text against the banned word
// (in practice, tokenize first)
if ($checker->areConfusable(mb_strtoupper($text), mb_strtoupper($word), $errorCode)) {
return true;
}
}
return false;
}
?>
</pre>
</div>
<p>For real production use, tokenize the input first and compare token-by-token rather than the whole string at once. The <code>areConfusable</code> method is a per-string check, not a substring search.</p>
<h3 id="toc_14">Other situations worth a pass</h3>
<p>A few more places where the same pattern pays off, with less code:</p>
<ul>
<li><strong>Payment and financial contexts</strong>: payee names, bank references, invoice descriptions that visually mimic a known legitimate counterparty.</li>
<li><strong>Brand and keyword monitoring</strong>: in marketplaces, app stores, and ad networks, to detect typosquatting and brand squatting in user-submitted product names and store names.</li>
<li><strong>Anti-phishing in messaging and email</strong>: scan incoming sender display names and embedded links against a list of known-internal executive names and vendor domains before display.</li>
</ul>
<h2 id="toc_15">Limitations and gotchas</h2>
<p><code>Spoofchecker</code> is definitely better than nothing. And probably better than anything you can come up with. Yet, there are a few things <code>Spoofchecker</code> is not: let’s review them so you don’t get surprised.</p>
<p><strong><code>Spoofchecker</code> requires the <code>intl</code> extension</strong>. Most modern PHP builds include <code>intl</code> by default, though shared hosting and minimal Docker images sometimes don’t. Check <code>extension_loaded('intl')</code> before relying on it; provide a fallback, or refuse the input otherwise.</p>
<p><strong><code>isSuspicious</code> is heuristic, not authoritative</strong>. It will produce false positives on legitimate multilingual names. A real Greek user named <code>Αλέξ</code> is flagged by <code>setRestrictionLevel(Spoofchecker::ASCII)</code>. Always combine the check with locale context via <code>setAllowedLocales()</code>, and prefer <code>MODERATELY_RESTRICTIVE</code> for human-facing identifiers rather than the strictest levels.</p>
<p><strong>Behavior depends on the ICU version</strong>. The confusable tables are maintained upstream in ICU and updated periodically. The same string may produce different results on PHP 8.1 with ICU 70 vs. PHP 8.3 with ICU 74. Don’t store <code>isSuspicious</code> results as long-lived authoritative flags; re-run the check when needed.</p>
<p><strong><code>areConfusable</code> is a per-string check, not a substring search</strong>. It compares two whole strings and tells you whether they look alike. It will not find <code>раypal</code> inside <code>https://раypal.com/login</code>. For substring detection, tokenize first: that is, split the URL on <code>/</code>, split the email on <code>@</code>, split text on whitespace, etc. and compare per-token.</p>
<p><strong>It does not validate format</strong>. A Punycode string that doesn’t decode, or a domain with illegal characters, will pass <code>Spoofchecker</code> if no confusable pattern matches. Pair with <code>idn_to_ascii()</code> for full domain validation and with <code>Normalizer::normalize()</code> for general Unicode hygiene.</p>
<p><strong>The default check set is conservative</strong>. If you don’t call <code>setChecks()</code>, you get a reasonable starting point but not a strict one. For anything security-sensitive, be explicit about which checks you want and which restriction level applies.</p>
<p><strong>It is not a substitute for output encoding</strong>. <code>Spoofchecker</code> tells you whether input is suspicious. It doesn’t make output safe. Always pair with proper HTML escaping, URL encoding, and CSP headers.</p>
<h2 id="toc_16">Unicode tricks in your tests too</h2>
<p>The same trick works against the output of your test suite. On 8 October 2026, The PHP Foundation published <a href="https://thephp.foundation/blog/2026/10/08/your-tool-has-a-new-reader/">Your tool has a new reader</a>, about PHPUnit. Its argument: a terminal now has two readers. A human sees the rendered output, and a coding agent reads the raw bytes and acts on them. Any gap between the two is a risk.</p>
<p>The post shows how a test name, data set name, or assertion message can carry control characters. An <code>ESC[2K</code> (erase line) or a carriage return can make a failing test look like a pass to a human, while the agent reads the failure. A line feed inside a data set name can forge the header of a record for a test that doesn’t exist. The fix is to show these characters as visible <code>\u{NNNN}</code> escapes rather than strip them. The lesson is the same as for <code>Spoofchecker</code>: the bytes differ from what the eye sees, and your test output is input too.</p>
<h2 id="toc_17">Conclusion: you may already be running Spoofchecker</h2>
<p>Thanks to <a href="https://github.com/egulias/EmailValidator">egulias/EmailValidator</a>, which is included in many applications, <code>Spoofchecker</code> may already run in your code. Its <code>SpoofCheckValidation</code> strategy calls <code>Spoofchecker</code> with <code>SINGLE_SCRIPT</code> on the whole address, and its constructor throws a <code>LogicException</code> if the intl extension is missing. It is not part of the default RFC validation: it runs only when you add it to a validation chain. On the address from the email example above, it flags <code>ceo@xn--ypal-43d9g.com</code> and passes <code>ceo@paypal.com</code>.</p>
<p>To check your own code, run <code>composer show egulias/email-validator</code> and search your codebase, including your framework’s validation layer, for <code>SpoofCheckValidation</code>. If it is there, you’re already using <code>Spoofchecker</code>. If it is not, the three configurations above are a good place to start.</p>
<h1 id="toc_18">Check your Unicode strings a bit more</h1>
<p><code>Spoofchecker</code> is fourteen years old, underused, and still the right tool whenever your PHP application accepts Unicode that other humans will see.</p>
<p>Worth ten minutes of your time to wire in.</p>
<p>The post <a href="https://www.exakat.io/php-has-a-spoofchecker-a-14-year-old-tool-youre-probably-not-using/">PHP has a Spoofchecker: A 14-Year-Old Tool You’re Probably Not Using</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>The 2,000th word: a personal notebook became a map of PHP - Exakathttps://www.exakat.io/?p=164992026-10-16T09:40:31.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640.jpg"><br />
<img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16500" src="https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640-300x300.jpg" alt="" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/dictionary.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>The 2,000th word: a personal notebook became a map of PHP</h1>
<p>Today the <a href="https://php-dictionary.readthedocs.io/en/latest/"><strong>PHP Dictionary</strong></a> reaches its 2,000th definition: bearer token, is the last entry, and, as you can see, it is quite niche subject for a definition and PHP. This is because, over the years, a lot of definitions were added to what was initially a simple knowledge memorisation. So, it is a good moment to look back at how we got here, and at what the dictionary has become.</p>
<h2 id="toc_1">It started as a notebook</h2>
<p>The dictionary began as a way to clear up things I used every day without always understanding them. It is surprising how much words and concepts are used, both heard and pronounced, yet, they are not defined. This is how language (human, not PHP) live: initially, you learn by converting the meaning to what you already know, with a definition. Later, you build that same definition by experience: hear it, see its side effects, form a definition.</p>
<p>Even words that sound familiar are not obvious to pin down, because they were acquired without being defined. Think “dangling reference” or “garbage collection”: who put garbage in my code? Is there a notion of garbage creation? There is spagetti code, but when does it become garbage? And, yes, OOP is POO in French, but that is not the point.</p>
<p>Writing down a definition for each term, with a short illustration, may be a code sample, forced me to understand them properly.</p>
<p>Then, a definition is only useful in a context, so soon, definitions were not enough. So I started adding links: to the PHP manual, to the blog posts that explained a notion best, to the people who coined the terms, to other definitions in the dictionary. 2000 words gave 12000+ links, and they bring the technical reference of PHP together, like a carpet in a room.</p>
<h2 id="toc_2">It grew by listening to the community</h2>
<p>That is where the dictionary changed shape. The PHP community has written an enormous amount of useful material over the last thirty years: articles, blog posts, conference talks, tutorials, videos, tools, release notes, funny things… Hundreds of authors explained concepts in great details, and with lyrism and enthusiasm. Their articles are now linked from the definitions.</p>
<p>The numbers show how wide that reach is:</p>
<table>
<thead>
<tr>
<th>What</th>
<th>Count</th>
</tr>
</thead>
<tbody>
<tr>
<td>Definitions</td>
<td><strong>2,000</strong></td>
</tr>
<tr>
<td>Distinct URLs</td>
<td><strong>4,108</strong></td>
</tr>
<tr>
<td>Distinct websites cited</td>
<td><strong>about 1,000</strong></td>
</tr>
<tr>
<td>Definitions with a PHP code sample</td>
<td><strong>1,452</strong></td>
</tr>
<tr>
<td>Words written across descriptions</td>
<td><strong>193,000+</strong></td>
</tr>
</tbody>
</table>
<p>Every definition points outward. The official PHP manual is the most-linked source, of course, and a large share of the links go to blogs and articles written by practitioners. Each one is a small credit to someone who took the time to write it down: thank you to all of you. You know who you are.</p>
<h2 id="toc_3">It connected the words together</h2>
<p>PHP notions are not isolated. A “trait” leans on “class”, “class” on “object”, “object” on “reference”, “type”, “property”, “inheritance”, “composition”, “case”, and so on. So the dictionary also became a network.</p>
<p>There are <strong>12,381 links between definitions</strong>. The most connected terms are the foundations of the language: <code>array</code>, <code>security</code>, <code>interface</code>, <code>database</code>, <code>type</code>, <code>property</code>, <code>string</code>. Reading one definition naturally leads to the next, which is what I wanted from the start. If you remember “surfing the net with hyperlinks” from the 1990’s, that’s the idea. If you don’t, well, you can catchup with something you definitely missed.</p>
<h2 id="toc_4">It untangled words with several meanings</h2>
<p>Many PHP terms mean more than one thing, depending on who is talking. Over 59 definitions explicitly list multiple concepts they cover. Generally, it is difficult to come up with new words for each concept: as programmer, we don’t always add <code>v2</code> as a suffix, so some words are covering several distinct meanings. It happens like in any natural and human language.</p>
<p>Take <strong>stack</strong>. It can be the call stack that tracks nested function calls, the data structure where the last item in is the first one out, or the LAMP stack of Linux, Apache, MySQL and PHP. Or <strong>unpacking</strong>, which is either the <code>unpack()</code> function that reads binary data, or the <code>...</code> operator that spreads an array into arguments.</p>
<p>Those entries were often an eye opener: not by choosing one meaning, but by putting them side by side so the reader can tell which one a blog post is talking about.</p>
<h2 id="toc_5">Where does the PHP dictionary stands</h2>
<p>Numbers are also a to-do list:</p>
<ul>
<li><strong>30 definitions</strong> are not linked from any other definition: I need to review them.</li>
<li><strong>new blogs</strong> are created everyday, even today, and should be added to this dictionary.</li>
<li><strong>old blogs</strong> decay, fade away, and disappear entirely, in a casino. These need a review.</li>
<li>Some corners of the PHP ecosystem should be brought forward. Which one? I missed them, so you tell me.</li>
</ul>
<p>Each of these is a small, concrete contribution, accessible to everyone.</p>
<h2 id="toc_6">Beyond the book</h2>
<p>The dictionary already exists in several forms:</p>
<ul>
<li>a <a href="https://php-dictionary.readthedocs.io/en/latest/">website</a>, with a sitemap and an RSS feed</li>
<li>a machine readable JSON <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary.json"><code>dictionary.json</code></a>, one structured object per entry. Grab this one in one go, don’t scan web sites.</li>
<li><a href="https://php-dictionary.readthedocs.io/en/latest/llms.txt"><code>llms.txt</code></a> and <a href="https://php-dictionary.readthedocs.io/en/latest/llms-full.txt"><code>llms-full.txt</code></a> for AI tools.</li>
<li>a terminal navigator, <code>tui.php</code>, to read the dictionary from the comfort of the commandline.</li>
</ul>
<p>For the future, a book is not always the most convenient way to use a dictionary. I am looking at other formats: flashcards for learning the vocabulary, a Markdown vault for personal note-taking, an API or MCP, an editor plugin that shows a definition on hover, and a printable reference sheet. If one of those would help you, I want to hear about it.</p>
<h2 id="toc_7">Help build the next 2,000</h2>
<ol>
<li><strong>Fix a broken link, suggest new articles.</strong> Open a file in <a href="https://github.com/exakat/php-dictionary/tree/main/src/index"><code>src/index/</code></a>, check <code>JSON</code>, and fill it in.</li>
<li><strong>Connect an orphan.</strong> Pick one of the 30 definitions nobody links to, and add it to the <code>related</code> list of a relevant entry.</li>
<li><strong>Add a resource.</strong> A good article or a manual page is worth more than a dozen generic links. A whole website.</li>
<li><strong>Propose a missing word.</strong> Terms, acronyms and jargon are the most valuable addition. Send a pull request on the <a href="https://github.com/exakat/php-dictionary">repository</a>; the <a href="https://github.com/exakat/php-dictionary/blob/main/README.md">contributing guide</a> explains the workflow.</li>
</ol>
<p><strong>Let’s talk, now that we have a <a href="https://php-dictionary.readthedocs.io/en/latest/">PHP dictionary</a>.</strong> You can reach me online on the <a href="https://discord.com/invite/RYajXKxuuK">PHPCommunity</a> server, or on <a href="https://phpc.social/@dseguy">Mastodon</a> or <a href="file:///Users/famille/Desktop/php-dictionary/blog/2000-definitions.md">Twitter</a>. Tell me which definition you want next, which one is wrong, or which format would make you actually use this dictionary.</p>
<p>And now, let’s go to 3,000.</p>
<p>The post <a href="https://www.exakat.io/the-2000th-word-a-personal-notebook-became-a-map-of-php/">The 2,000th word: a personal notebook became a map of PHP</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Recently updated PIE extensions #11 (since October 8th, 2026) - Exakathttps://www.exakat.io/?p=164972026-10-15T09:21:24.000ZExakat<h2 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16294" src="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-300x300.png" alt="Recently updated PIE extensions #11" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Recently updated PIE extensions #11 (since October 8th, 2026)</h2>
<p>40 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/flow-php/arrow-ext">flow-php/arrow-ext</a> (0.46.0): Apache Arrow and Parquet bindings for flow-php/parquet (Rust)</li>
<li><a href="https://packagist.org/packages/iliaal/mdparser">iliaal/mdparser</a> (0.8.0): Native C CommonMark + GitHub Flavored Markdown parser for PHP, targeting CommonMark 0.31 + GFM. ~10-20x faster than pure-PHP parsers, zero runtime dependencies.</li>
<li><a href="https://packagist.org/packages/phpolygon/php-vio">phpolygon/php-vio</a> (2.33.0): PHP extension for GPU rendering (OpenGL, Vulkan, Metal), audio, video recording, streaming, and input</li>
<li><a href="https://packagist.org/packages/symfony/deepclone">symfony/deepclone</a> (v0.8.9): Export any serializable PHP values as pure arrays – accelerator for Symfony’s DeepCloner</li>
<li><a href="https://packagist.org/packages/php-debugger/php-debugger">php-debugger/php-debugger</a> (0.4.0): Zero-overhead debugging for PHP</li>
<li><a href="https://packagist.org/packages/iliaal/statgrab">iliaal/statgrab</a> (2.3.1): PHP extension wrapping libstatgrab, the cross-platform system-statistics library. CPU, memory, disk I/O, filesystems, network interfaces, processes, users.</li>
<li><a href="https://packagist.org/packages/mongodb/mongodb-extension">mongodb/mongodb-extension</a> (2.5.4): MongoDB driver extension</li>
<li><a href="https://packagist.org/packages/flow-php/flow-php-ext">flow-php/flow-php-ext</a> (0.46.0): Flow PHP native extension (Rust) – native column backend, CSV/JSON readers, Parquet ETL through arrow-ext</li>
<li><a href="https://packagist.org/packages/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a> (v1.22.2): Pre-compiled tree-sitter grammars for 372 programming languages</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v2.2.3): Universal LLM API client with Rust-powered polyglot bindings.</li>
<li><a href="https://packagist.org/packages/xberg-io/xberg">xberg-io/xberg</a> (v1.3.10): High-performance document intelligence library</li>
<li><a href="https://packagist.org/packages/xberg-io/html-to-markdown">xberg-io/html-to-markdown</a> (v3.18.0): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/iliaal/phonetic">iliaal/phonetic</a> (0.4.4): Native phonetic matching for PHP: Double Metaphone, Beider-Morse Phonetic Matching, Daitch-Mokotoff Soundex, NYSIIS, and Match Rating Approach.</li>
<li><a href="https://packagist.org/packages/thomas-0816/pdo-duckdb-php">thomas-0816/pdo-duckdb-php</a> (2.0.0.0-alpha2): PHP PDO Driver for DuckDB, modern analytics</li>
<li><a href="https://packagist.org/packages/iliaal/fast_uuid">iliaal/fast_uuid</a> (0.8.1): Fast RFC 9562 UUID generation (v1/v2/v3/v4/v5/v6/v7/v8 + nil/max) as a PHP C extension, with a ramsey/uuid-shaped object API and procedural fast-path functions.</li>
<li><a href="https://packagist.org/packages/php-io-extensions/ftdi">php-io-extensions/ftdi</a> (0.10.5): PHP bindings of libftdi1: FTDI USB devices, MPSSE, async transfers and EEPROM</li>
<li><a href="https://packagist.org/packages/goldziher/spikard">goldziher/spikard</a> (v0.17.2): Codegen-first polyglot web toolkit with a Rust core and bindings for 14 languages</li>
<li><a href="https://packagist.org/packages/iliaal/phpser">iliaal/phpser</a> (0.9.0): Fast binary serializer for PHP cache workloads. Decoder-optimized, beats igbinary on packed numerics, deep-nested structures, and same-class DTO batches.</li>
<li><a href="https://packagist.org/packages/iliaal/fastjson">iliaal/fastjson</a> (0.10.0): Fast JSON encode/decode/validate for PHP 8.1+, backed by yyjson. Drop-in alternative to ext/json with namespaced fastjson<em>* functions and json</em>last_error-compatible error reporting.</li>
<li><a href="https://packagist.org/packages/phpstan/turbo">phpstan/turbo</a> (2.3.1): Native acceleration extension for PHPStan</li>
<li><a href="https://packagist.org/packages/php-io-extensions/glfw">php-io-extensions/glfw</a> (v0.10.0): 1:1 PHP bindings of GLFW 3.4 for staged windows: monitors and video modes, windows, hints, attributes and callbacks, events, contexts, Vulkan surfaces and native handles</li>
<li><a href="https://packagist.org/packages/cachewerk/ext-relay">cachewerk/ext-relay</a> (v0.60.0): The fastest Redis client for PHP. 100× faster cache reads, near-zero bandwidth, no code changes required.</li>
<li><a href="https://packagist.org/packages/xberg-io/crawlberg">xberg-io/crawlberg</a> (v1.10.3): High-performance web crawling engine</li>
<li><a href="https://packagist.org/packages/orieg/judy">orieg/judy</a> (v2.8.0): PHP Judy implements sparse dynamic arrays (aka Judy Arrays).</li>
<li><a href="https://packagist.org/packages/php-io-extensions/appkit">php-io-extensions/appkit</a> (v0.10.0): 1:1 PHP bindings of AppKit and the CoreFoundation run loop it runs on, as classes named after their native counterparts</li>
<li><a href="https://packagist.org/packages/php-io-extensions/gtk">php-io-extensions/gtk</a> (v0.10.0): 1:1 PHP bindings of GTK 4 and the GLib main loop it runs on, as functions and classes named after their C counterparts</li>
<li><a href="https://packagist.org/packages/orieg/expanse-extension">orieg/expanse-extension</a> (v0.13.0): Native PHP Zend Engine extension for Expanse: modern Judy arrays in pure Rust (ext-php-rs). Userland API: orieg/expanse.</li>
<li><a href="https://packagist.org/packages/kcs/mjml">kcs/mjml</a> (v1.2.2): No description available.</li>
<li><a href="https://packagist.org/packages/pecl/bitset">pecl/bitset</a> (3.2.1): BITSET extension</li>
<li><a href="https://packagist.org/packages/ptondereau/biscuit-php">ptondereau/biscuit-php</a> (v0.6.0): PHP bindings for Biscuit authorization tokens</li>
<li><a href="https://packagist.org/packages/php-io-extensions/pcurl">php-io-extensions/pcurl</a> (0.10.5): libcurl’s multi socket API bound 1:1 into PHP on top of ext-curl handles</li>
<li><a href="https://packagist.org/packages/php-io-extensions/fb">php-io-extensions/fb</a> (v0.10.5): Pixel storage in C: FbFormat describes a byte layout, FbBuffer holds one surface in it and reads, writes, converts and copies it</li>
<li><a href="https://packagist.org/packages/kpqc/kpqc">kpqc/kpqc</a> (v0.1.1): PHP APIs for AIMer, HAETAE, NTRU+, and SMAUG-T.</li>
<li><a href="https://packagist.org/packages/php-io-extensions/epoll">php-io-extensions/epoll</a> (v0.10.6): Linux epoll(7) bindings as global PHP functions</li>
<li><a href="https://packagist.org/packages/php-io-extensions/kqueue">php-io-extensions/kqueue</a> (v0.10.5): 1:1 PHP bindings of kqueue(2): kqueue(), kevent(), kevent64(), EV<em>SET(), EV</em>SET64()</li>
<li><a href="https://packagist.org/packages/uma/secp256k1-php">uma/secp256k1-php</a> (v0.1.2): Native bindings for libsecp256k1</li>
<li><a href="https://packagist.org/packages/corvus-dotnet/corvus-json-schema">corvus-dotnet/corvus-json-schema</a> (0.1.3): A high-performance JSON Schema evaluator (draft 4, 6, 7, 2019-09 and 2020-12) as a PHP extension, backed by Rust.</li>
<li><a href="https://packagist.org/packages/php-io-extensions/imgdec">php-io-extensions/imgdec</a> (v0.10.5): PNG, JPEG and TIFF into straight RGBA8 in C, through libpng, libjpeg and libtiff</li>
<li><a href="https://packagist.org/packages/php-io-extensions/rasterize">php-io-extensions/rasterize</a> (v0.10.5): Shapes into coverage spans in C: RasterScanner answers paths, ellipses, rings and one-pixel polylines as span bytes, hard-edged or anti-aliased</li>
<li><a href="https://packagist.org/packages/php-io-extensions/box2d">php-io-extensions/box2d</a> (v0.10.5): 1:1 PHP bindings of Box2D 3.1.1, with the library compiled into the extension</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-11-since-october-8th-2026/">Recently updated PIE extensions #11 (since October 8th, 2026)</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>PHP ❤️ Rust: Every Bridge Between Two Worlds - Exakathttps://www.exakat.io/?p=164932026-10-14T14:29:56.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/crab.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16494" src="https://www.exakat.io/wp-content/uploads/2026/10/crab.640-300x300.jpg" alt="" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/crab.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/crab.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/crab.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/crab.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/crab.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/crab.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>PHP <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2764.png" alt="❤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Rust: Every Bridge Between Two Worlds</h1>
<h2 id="toc_1">An Elephant and a Crab Walk Into a Bar</h2>
<p>PHP and Rust have been walking toward each other from across the room for years. One is the elephant: over thirty years old, used by about 70% of the websites whose server-side language <a href="https://w3techs.com/technologies/details/pl-php">W3Techs</a> can identify, and believes <code>==</code> is a perfectly reasonable thing for two values to do. The other is the crab: young, armored, allergic to nulls, and insists you prove your code is correct before it runs, which frankly sounds like homework.</p>
<p>For a long time, the gap between them was a rift between continents. Crossing it meant writing C glue code by hand, at night, alone, without a debugger. That era is over. What changed is not the languages: it’s the sudden ability to produce a PHP VM in Rust with a lot of help from an AI that has read more C code than any human ever will. The results are wildly uneven, sometimes accidental, and occasionally brilliant. The rift is now being covered with bridges. Some are suspension bridges. Some are rope. One is a DOOM renderer.</p>
<p>This post inventories every crossing point between the two languages, in both directions, with notes on each project’s maturity where the sources show it.</p>
<h2 id="toc_2">From PHP, Toward Rust</h2>
<h3 id="toc_3">1. Zend Extensions: The Classic Highway</h3>
<p>This is the obvious move: write a PHP extension, in Rust instead of C. The Zend API stays the reference: the extension still has to speak its C-level interface. In fact, there are currently several ways to do this, including one that writes the extension code for you.</p>
<table>
<thead>
<tr>
<th>Project</th>
<th>Pitch</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ext-php-rs</strong></td>
<td><a href="https://github.com/extphprs/ext-php-rs">Bindings over the Zend API</a>, macros galore. The respectable choice. Its README requires PHP 8.1 or later.</td>
</tr>
<tr>
<td><strong>phper</strong>, aka <a href="https://phper-framework.github.io/">“PHP Enjoy Rust”</a></td>
<td>“Pure and safe Rust whenever possible.” Supports PHP 7.0 to 8.5. Powers the <a href="https://skywalking.apache.org/">Apache SkyWalking</a> PHP agent, which is a serious résumé.</td>
</tr>
<tr>
<td><strong>alef-backend-php</strong></td>
<td><a href="https://lib.rs/crates/alef-backend-php">A code generator</a>, not a compiler: from a Rust library it produces ext-php-rs source for a PHP 8.2+ extension, with classes, methods, free functions and serde conversions. It is the PHP backend of alef, a polyglot binding generator (MIT, version 0.17.35, May 2026).</td>
</tr>
</tbody>
</table>
<p>Async calls in its output run through Tokio <code>block_on</code>, so from PHP’s side they behave synchronously.</p>
<p>These solutions are solid and battle tested. This is the road production code actually takes when it needs Rust speed inside PHP. It is also a great avenue to keep a production monolith in PHP, and offload heavy work to Rust.</p>
<p>Other languages are not so lucky: few have such comfortable tools to merge with PHP.</p>
<h3 id="toc_4">2. FFI: The Drawbridge</h3>
<p>With FFI, no framework is needed. Since PHP 7.4, you can compile a Rust crate as a <code>cdylib</code> (<code>.so</code>, <code>.dylib</code>, <code>.dll</code>), declare the C signatures with <code>FFI::cdef()</code>, and call it. The elephant extends its trunk; the crab extends a claw; a C header mediates because neither trusts the other’s native language.</p>
<p>This solution is light and fast: so much that there is nothing more to explain than the paragraph above. And you own every <code>unsafe</code>decision yourself. FFI is the trust fall of interoperability, and it is common in other languages too.</p>
<h3 id="toc_5">3. WebAssembly: The Neutral Embassy</h3>
<p>Why talk directly when you can meet in a sandbox? Compile Rust to WASM, execute it inside PHP through a runtime extension:</p>
<table>
<thead>
<tr>
<th>Project</th>
<th>Pitch</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>veewee/ext-wasm</strong></td>
<td>The modern take, still experimental: <a href="https://github.com/veewee/ext-wasm">wasmtime via ext-php-rs</a>, component model, WASI preview1/2, Fibers-based async. Runs DOOM, because of course it does.</td>
</tr>
<tr>
<td><strong>wasmer-php</strong></td>
<td><a href="https://github.com/wasmerio/wasmer-php">Wasmer-based</a> runtime, sandboxed. Only PHP 8.0 is supported, on Linux and macOS amd64, with no Windows or arm64 support.</td>
</tr>
<tr>
<td><strong>php-ext-wasm</strong></td>
<td>A <a href="https://github.com/skylei/php-ext-wasm">fork of wasmer-php</a> that runs WASM through Wasmer since v0.2.0 (earlier versions used wasmi), a matryoshka option.</td>
</tr>
</tbody>
</table>
<p>This is the safest way, as a WASM binary can’t touch PHP’s memory. But you pay a toll at each call boundary. veewee’s ext-wasm is experimental, so expect its API to change before 1.0.</p>
<h3 id="toc_6">4. PHP to Rust Transpilation: A Moving Walkway</h3>
<p><strong>php-to-rust</strong>, from <a href="https://github.com/LegacyToRustProject/php-to-rust">LegacyToRustProject</a>, is an agent that converts an entire PHP project to Rust, module by module, with a verification loop: the Rust must compile, run, and produce identical output to the original PHP, or the AI fixes it and tries again. WordPress plugins are the first-class target.</p>
<p>It is not a transpiler; it’s a very patient intern with a diff tool. It is in early development, with a handful of commits so far.</p>
<h3 id="toc_7">Rust powered PHP engine: The Holy Bridge</h3>
<p>Many people try this: rewrite the PHP engine in Rust. The official engine is heading for PHP 8.6, so these remain interesting experiments.</p>
<ul>
<li><a href="https://github.com/ekinertac/Phargo">Phargo</a>: PHP engine, written from scratch in Rust: it is capable of serving WordPress pages, with a SQLite database.</li>
<li><a href="https://github.com/francescotinti/php-rust">php-rust</a>: a rewrite of PHP 8.5 in Rust, including the bytecode, garbage collector and database interfaces. According to its README, it passes 1,748 unit tests, runs WordPress on MySQL, installs Composer packages and produces PHPUnit output identical to PHP’s. Doctrine DBAL and Symfony’s http-kernel tests pass, though Doctrine ORM still has failures. It is called phpr.</li>
<li><a href="https://github.com/rphp-lang/rphp">rphp</a> (rphp-lang): pre-alpha PHP-compatible engine with its own lexer, parser, bytecode compiler and VM. It does not copy the Zend architecture, and it is not yet a drop-in replacement for PHP.</li>
<li><a href="https://github.com/lineCode/phpl/">phpl</a>: PHP interpreter in Rust, in its early stages. It cannot yet execute complete PHP files.</li>
</ul>
<p>Another rewrite comes from JoliCode. <a href="https://jolicode.com/blog/writing-the-php-virtual-machine-in-rust-with-a-lot-of-help-from-ai">Writing the PHP Virtual Machine in Rust (with a lot of help from AI)</a>, from <a href="https://jolicode.com/">Jolicode</a> with a <a href="https://github.com/jolicode/rphp">rphp project</a>, which calls itself a research project, not a production runtime.</p>
<p>Beyond the headline of rewriting PHP engine in Rust, you can follow various ways to do it: some experiments are trying to run subsets of PHP features, others are running it against an ever growing set of tests, whether it be the <code>.phpt</code> or the ones from popular projects. The goal is always the same: identify the parts of PHP that are not possible to run with Rust.</p>
<h3 id="toc_8">The Rumored Bridges: more to come!</h3>
<p>Word of mouth and a <a href="https://x.com/matthieunapoli/status/2109309533558657246">tweet from Matthieu Napoli</a> point to compiling Rust on the fly from within a Laravel application. No public artifact to cite yet: if this bridge exists, you’ll have to wait for publication.</p>
<p>And another experiment is coming from <a href="https://x.com/0x15f/status/2109105401631481904">tweet from Jake Casto</a>, which hits at rewriting the PHP engine with Rust. Still a mysterious endeavour at the moment, but let’s keep an eye on it.</p>
<h3 id="toc_9">7. The Mirror: A Rust Compiler Written in PHP</h3>
<p>Compiling Rust code to PHP would make little sense, but apparently, compiling Rust code with PHP does.</p>
<p><strong><a href="https://github.com/mrconter1/rustc-php">rustc-php</a></strong> is a Rust compiler with ownership checking, borrow checking, generics, traits and closures, written in PHP, emitting x86-64 Linux ELF binaries directly, no LLVM, no linker. Its stated use case: “compile Rust on a shared hosting server from 2008 where the only installed runtime is PHP.” It is the perfect comedic twin of the previous category: while Rust developers rewrite PHP in Rust, PHP developers are compiling Rust in PHP. The circle is not just complete; it is smug about it.</p>
<h2 id="toc_10">From Rust, Toward PHP</h2>
<p>Rust can also be the source of the PHP code, or the host that runs it.</p>
<h3 id="toc_11">php_codegen: Rust Writes the PHP</h3>
<p><a href="https://github.com/php-rust-tools/codegen">php_codegen</a> is a Rust library with a fluent API for writing PHP source code: classes, functions, enums, interfaces, traits, methods and properties. It is not a transpiler: nothing reads Rust code and translates it. You describe PHP code in Rust, and the output is PHP source. The docs.rs page for 0.4.0 reports that none of the crate is documented, so the examples folder is the best entry point. It is dual-licensed MIT or Apache-2.0 and maintained by azjezz.</p>
<h3 id="toc_12">Rust as Plugin Host, Process Front, or Async Engine</h3>
<table>
<thead>
<tr>
<th>Project</th>
<th>Pitch</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>php-tokio</strong></td>
<td><a href="https://github.com/danog/php-tokio">Use any async Rust library from PHP</a>: an extension that plugs into the Revolt event loop, so it works with amphp, PSL and ReactPHP. Apache-2.0, 357 stars, no releases listed.</td>
</tr>
<tr>
<td><strong>Extism</strong></td>
<td>Rust as a plugin language: compile a plugin to WebAssembly, then call it from a <a href="https://packagist.org/packages/extism/extism">PHP host</a>. PHP and Rust are both on Extism’s supported lists. The runtime library must be installed separately, and the Packagist listing I could see was from 2024, so check the current version.</td>
</tr>
<tr>
<td><strong>solder</strong></td>
<td><a href="https://github.com/killertux/solder">A library to build PHP extensions in Rust</a>, started as a fork of php-rs. Early and unstable; its README says it only works with PHP 7.</td>
</tr>
<tr>
<td><strong>php-rpm</strong></td>
<td><a href="https://github.com/hjr3/php-rpm">An experimental process manager</a>: Rust’s hyper server takes the HTTP requests and runs PHP itself, in place of nginx and PHP-FPM. The repository was archived in January 2018.</td>
</tr>
</tbody>
</table>
<h2 id="toc_13">The Type Checker Will See You Now</h2>
<p>For all this engineering exuberance, one truth keeps resurfacing in every project above: the two languages’ compatibility has hard limits, and they are philosophical, not technical.</p>
<p>The bridges work. What doesn’t transfer is the soul. PHP’s genius is forgiveness: <code>==</code> means “eh, close enough,” arrays are arrays are dictionaries are lists, and a <code>null</code> is just a value that hasn’t decided yet. Nobody, PHPersonne, as the folklore might say, wants to strictly type their code. We are all willing to do it, as it promises marvels and wonders. Yet, Rust’s non-negotiable entry fee is exactly that strictness: types proven, ownership proven, no hand-waving. To pass into Rust, PHP code must renounce the very flexibility that made it productive; to host PHP, Rust must build a runtime that simulates a looseness it was designed to reject. Every interpreter above has to model PHP’s loose values somehow: a type-juggling layer, and, likely, a quiet apology.</p>
<p>This is why the pragmatic bridges are partial: extract the static, well-typed, hot modules into Rust, via FFI or extensions, and let the rest of the application keep living its dynamic best life on the PHP side. Business rules are often fuzzy, vague, evolving and leaving no time to enforce maximum verification. The elephant does not need to become a crab. It just needs the crab to carry the heavy boxes.</p>
<p>The experiments keep coming; that is important, and healthy. The AI era is taking care of that. The mature engineering question is no longer can PHP and Rust talk, but how much of each other’s personality they should be asked to adopt. On that, both languages would probably agree: none, thank you.</p>
<p>Now excuse me while I push this pull request: it compiles, it passed the tests, and honestly, that’s the only kind of trust fall either of them respects.</p>
<p>The post <a href="https://www.exakat.io/php-%e2%9d%a4%ef%b8%8f-rust-every-bridge-between-two-worlds/">PHP ❤️ Rust: Every Bridge Between Two Worlds</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>foreach Already Knows: PHP and the Zero-One-Infinity Rule - Exakathttps://www.exakat.io/?p=164902026-10-13T06:38:27.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/crates.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16491" src="https://www.exakat.io/wp-content/uploads/2026/10/crates.640-300x300.jpg" alt="" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/crates.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/crates.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/crates.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/crates.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/crates.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/crates.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>foreach Already Knows: PHP and the Zero-One-Infinity Rule</h1>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
foreach ($recipients as $recipient) {
$mailer->send($recipient, $message);
}
?>
</pre>
</div>
<p>This loop doesn’t ask when or if <code>$recipients</code> a list. It doesn’t ask anything, although, well, it does ask for an array or an iterable. Now, when <code>$recipients</code> holds three users, it runs three times. If it holds one, it runs once. If it’s empty, the body simply never executes: no dispatcher spun up, no mailer touched, no special case written by anyone. PHP’s own engine already treats zero, one, and many as the same problem: it is solved once, at the language level. And you don’t have to pay attention to it in any way.</p>
<p>The rest of the code that we write hasn’t caught up to that idea. Most PHP is written for exactly one thing: one user, one row, one file, one thing. The moment a function has to handle zero of them, or two, or an arbitrary number N, that code usually gets rewritten rather than extended: a new <code>if</code>, a extra loop, a external helper, you name it. The gap between how PHP’s runtime treats cardinality and how PHP programmers treat it is the subject of this piece.</p>
<h2 id="toc_1">Naming the rule</h2>
<p>There’s a decades-old name for the discipline <code>foreach</code> is abiding by: the <strong>Zero-One-Infinity rule</strong>, that I credit to computing pioneer <code>Willem van der Poel</code>, with an independent claim by language designer <code>Bruce MacLennan</code>. The rule says that a well-designed system should never bake in an arbitrary limit, such as “up to 5”, or “at most 3”, on how many of something can exist. Either a thing is forbidden, that is 0, allowed exactly once, that is 1, or allowed without limit, that is many. Nothing in between is a design decision; it’s a bug waiting to be reported as a feature request.</p>
<p>Applied to everyday PHP, the rule reads a little differently than it did for language designers, back then in the previous millenium:</p>
<ul>
<li><strong>Zero</strong> is usually an optimization: don’t do the work, not even prepration, if there’s nothing to do. No preloading before you know it’s needed, no dispatch before you know there’s a recipient, no clean up.</li>
<li><strong>One</strong> is the default case, the one every tutorial teaches, and it needs no introduction.</li>
<li><strong>More</strong>, or 2, or 3, or an undefined N, depending on the situation, is where the real cost shows up: duplicated code, then the itch to centralize it into a function or method, or a loop to drive it, and sometimes both. Sometimes, you don’t even know where to put the lot: inside or outside the method.</li>
</ul>
<p>PHP has plenty of internal gears for the two extremes already. What it’s never quite settled is the syntax for the middle: the moment a script goes from <code>this variable holds one user</code> to <code>this variable holds however many users there are</code>.</p>
<p>Let’s review them, and see how we can transition from one to the other.</p>
<h2 id="toc_2">Zero: be lazy</h2>
<p>Zero rarely gets its own code path in well-written PHP: it gets avoided, short-circuited, omitted, ignored. May be, even dispised. A repository method finds a user or it doesn’t; the code afterward should cost nothing in the doesn’t branch.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$user = $repository->find($id); // ?User
if ($user === null) {
return; // nothing to notify, nothing spent
}
$user->notify($message);
?>
</pre>
</div>
<p>The nullsafe operator collapses that guard into the call itself, which is in fact the same optimization written as one expression instead of two statements:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$repository->find($id)?->notify($message);
?>
</pre>
</div>
<p><code>??=</code> is the same idea applied to work instead of data: don’t build the expensive thing until something actually asks for it:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$cache ??= $this->loadExpensiveThing();
?>
</pre>
</div>
<p>Notice PHP already has two different spellings for nothing, and it chooses between them based on the cardinality the code expected before it turned out to be empty:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function findUser(int $id): ?User { /* ... */ } // expected zero-or-one -> null
function findUsers(array $ids): array { /* ... */ } // expected zero-or-many -> []
?>
</pre>
</div>
<p><code>null</code> is what “zero” looks like when a slot was always meant to hold at most one thing. <code>[]</code> is what “zero” looks like when the slot was always meant to hold any number of things. These are two different absences, which are chosen by the shape of the context. There is no best practice so place the <code>if</code> anywhere.</p>
<h2 id="toc_3">One: same old, same old</h2>
<p>This is the most common case. It is deliberately a short description, because it’s the case PHP needs the least help with. <code>$user->notify($message);</code> is all you need, and it gets used all the time, obviously. Just do it. If it is typed, it’s a safe thing. Every PHP tutorial’s first working example is the one case, and that’s not an accident: it’s the case the language was designed around first, and everything else was retrofitted on top.</p>
<h2 id="toc_4">Arrays, and the container that stopped caring</h2>
<p>Before going further into <code>more than one</code>, it’s worth stopping on a traditional PHP feature, that the platform got right decades before this pattern had a name: the data side of this problem was already solved. PHP is untyped by default: it dissolves the zero/one/many distinction at the array level, long before it ever reaches a function signature. Actually, any effort to force types on arrays, is going against what we are presenting here.</p>
<p>Imagine a moment wrapping every value in an array. A list, if you want, with a steady type, no index.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// classic code
$x = $user; // one
// wrapped code
$x = [$user]; // still one — now wrapped
$x = [$user, $otherUser]; // two
$x = []; // none
?>
</pre>
</div>
<p>The values are of the same type. Here, we see the humble <code>[...]</code> or <code>array()</code> for the old-timers (hi there!), regardless of how many elements are inside it. Nothing converts, nothing changes shape, nothing declares a new type as the count moves from 0 to 1 to N. <code>count($x)</code> doesn’t care which case it’s looking at: it is typeless. Neither does <code>foreach</code>. And it is important to understand that they are working on different levels.</p>
<p>Compare that to a statically-typed language, where the question <code>maybe zero, maybe one, maybe many</code> usually forces three genuinely different types that don’t compose without an explicit conversion step:</p>
<div>
<pre class="brush: java; title: ; notranslate">
Optional<User> zeroOrOne = Optional.empty();
List<User> zeroOrMany = new ArrayList<>();
</pre>
</div>
<p><code>Optional</code> and <code>List</code> share no supertype worth using; going from one to the other is a refactor, not a reassignment. PHP’s array already is the composition those two types are trying to approximate separately. The literal syntax, <code>[]</code>, is simultaneously the empty set, the singleton (the unique value here, not the pattern), and an arbitrary-length collection, with no wrapper to strip off later.</p>
<p>So the noun problem, what container holds <code>however many of these there are</code>, was never PHP’s weak point. What’s left, and what the rest of this piece is actually about, is the verb problem: the syntax you write to act on that container still assumes exactly one, and has to be entirely rewritten the moment it isn’t.</p>
<h2 id="toc_5">Two in a box: it is still fine to write by hand</h2>
<p>Treating two items in th same way is where the <code>more</code> side of the rule start its transition, and it’s still comfortable territory: PHP lets you name both halves and move on:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
[$winner, $runnerUp] = $results;
echo "{$winner->name} beat {$runnerUp->name}";
?>
</pre>
</div>
<p><code>list()</code> destructuring, and its less blog-friendly <code>[$a, $b]</code> shorthand, is PHP’s admission that <code>exactly two, with distinct roles</code> is common enough to deserve its own syntax: a middle ground between <code>one variable</code>and <code>a loop</code>. Nobody reaches for <code>array_map()</code> to handle a winner and a runner-up. Yet, most people should reach for <code>in_array()</code> instead of doing an <code>or</code>, even for at the two values scale: let that sink in. The point is that there are easy work around, that don’t feel like bringing out the big guns.</p>
<h2 id="toc_6">Three: where duplication starts to hurt</h2>
<p>Three is usually still hand-writable, but it’s the point where the transition starts:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$ceo->notify($message);
$cto->notify($message);
$cfo->notify($message);
?>
</pre>
</div>
<p>Three identical lines, differing only in which variable holds <code>$this</code>. Nothing here is wrong, but the pattern is now visible, and the next person to read it, aka the next executive to be added, is going to feel the pull toward a loop before they feel any pull toward more copy-paste.</p>
<h2 id="toc_7">N, the infinite and beyond: where centralization becomes mandatory</h2>
<p>Once the count isn’t known until runtime, hand-written duplication stops being an option and PHP forces a choice: a function or method that takes the collection, and a loop inside it. We all recognize <code>array_map()</code> and <code>foreach()</code> and its cousins.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function notifyAll(array $users, string $message): void
{
foreach ($users as $user) {
$user->notify($message);
}
}
?>
</pre>
</div>
<p>Actually, that notion applies to other PHP features, such as parameters. Variadics are PHP’s syntax for the specific flavor of <code>many</code>where even the caller doesn’t know the count at compile time: it’s baked into the call site itself rather than passed as a collection:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function notifyAll(User ...$users): void
{
foreach ($users as $user) {
$user->notify($message ?? 'Update available');
}
}
notifyAll($ceo, $cto, $cfo); // three, spelled like a normal call
notifyAll(...$allExecutives); // N, unpacked from a collection
?>
</pre>
</div>
<p>Both are still hiding <code>foreach</code> under their vest. Variadics only change how the many things arrive at the function boundary, not how they’re processed once inside it, which is a hint that <code>foreach</code> really is doing all the load-bearing work for more in PHP, everywhere it shows up.</p>
<h2 id="toc_8">PHP’s built-in answers to “more”</h2>
<p>Once <code>more</code> needs handling, PHP hands several tools, each answering a slightly different question about it.</p>
<p><code>array_map</code>, <code>array_filter</code>, and <code>array_reduce</code> are <code>foreach</code> with the loop body extracted and named: the same zero-or-more traversal, but expressed as <code>apply this operation</code> instead of <code>repeat this block</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$names = array_map(fn(User $u) => ucfirst(strolower($u->name)), $users); // get and format each name
$active = array_filter($users, fn(User $u) => $u->isActive()); // keep some, maybe none
$total = array_reduce($orders, fn($sum, $o) => $sum + $o->total(), 0); // collapse to one
?>
</pre>
</div>
<p><code>Countable</code> and <code>IteratorAggregate</code> let a custom type declare that it behaves like <code>zero or more</code> without exposing its internal array at all: typing the cardinality instead of just the storage:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
final class UserCollection implements Countable, IteratorAggregate
{
/** @var User[] */
private array $users = [];
public function add(User $user): void
{
$this->users[] = $user;
}
public function count(): int
{
return count($this->users);
}
public function getIterator(): Iterator
{
return new ArrayIterator($this->users);
}
}
?>
</pre>
</div>
<p>The <code>iterable</code> type hint, union of <code>array</code> and <code>Traversable</code>, is PHP’s way of saying <code>zero or more, and I genuinely don't need to know how you boxed them together</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function notifyAll(iterable $users, string $message): void
{
foreach ($users as $user) {
$user->notify($message);
}
}
?>
</pre>
</div>
<p>Generators push this the furthest, because they can yield zero, one, or an unbounded number of values, lazily, one at a time, without ever materializing the “how many” question as a number:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function activeUsers(iterable $allUsers): Generator
{
foreach ($allUsers as $user) {
if ($user->isActive()) {
yield $user;
}
}
}
?>
</pre>
</div>
<p>If nothing is active, the generator yields nothing and costs nothing beyond the check. If everything is active, it yields everything, one at a time, without ever holding the full set in memory. Zero, one, and infinity, genuinely unified in a single construct: not a bad way to close out the rule’s namesake.</p>
<h2 id="toc_9">The 8.x-era features, seen through this lens</h2>
<p>A few newer additions to PHP line up with this pattern more directly than they’re usually given credit for.</p>
<p>First-class callable syntax is PHP explicitly turning a single-item operation into a value that can be handed to something that applies it across many:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$upper = array_map(strtoupper(...), $names);
// vs. the pre-8.1 spellings:
$upper = array_map('strtoupper', $names);
$upper = array_map(fn($n) => strtoupper($n), $names);
?>
</pre>
</div>
<p><code>strtoupper(...)</code> doesn’t call <code>strtoupper</code>: it becomes a <code>Closure</code> version of it, built specifically to be passed somewhere that will call it repeatedly. That’s the <code>one operation, applied N times</code> idea, made explicit at the syntax level for the first time.</p>
<p><code>match()</code> occasionally gets used to encode this exact rule directly, which is a nice bit of self-reference:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$label = match (true) {
$count === 0 => 'none',
$count === 1 => 'one',
default => 'many',
};
?>
</pre>
</div>
<p>Enums are an interesting middle case: a fixed, compile-time <code>many</code>: a closed set of possibilities known in advance, where every individual case is simultaneously a singleton. Think of it as a boolean that will allow a third leg to sprout when you need it, or a fourth, a fifth (and not six, we don’t want bugs obviously):</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
enum Status
{
case Draft;
case Published;
case Archived;
}
Status::Draft === Status::Draft; // always truem as they are the same one instance
?>
</pre>
</div>
<p>An enum is <code>many</code> at the type level and <code>one</code> at every use site: a preview of the tension the next section runs into on purpose.</p>
<h2 id="toc_10">The idea PHP could finish: applying object syntax onto arrays</h2>
<p>Now, let’s write a piece of science fiction. It’s worth noting that PHP wouldn’t be the first language to get there. Groovy’s spread-dot operator already does this at the language level:</p>
<div>
<pre><code class="language-groovy">def totals = orders*.total()
?>
[/php]
</code></pre>
</div>
<p><code>orders*.total()</code> calls <code>.total()</code> on every element of <code>orders</code> and collects the results: no loop, no <code>map</code>, no proxy object. It reads exactly like calling <code>.total()</code> on a single order, with one extra character marking and do this to all of them.</p>
<p>Interestingly, there are already some experimentations in the PHP syntax with the current level of features. Laravel’s <code>HigherOrderCollectionProxy</code> gets close to the same effect in userland, without any language changes, by leaning on <code>__call()</code> and <code>__get()</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$users->each->notify($message);
?>
</pre>
</div>
<p><code>->each</code> returns a proxy object; the <code>->notify($message)</code> that follows it gets forwarded to every item in the collection. It’s a library trick, not a syntax change, and it’s proof the idiom is desirable enough that people built magic-method plumbing to fake it.</p>
<p>PHP itself has neither. Today, going from one to many always means switching syntax entirely:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$user->notify($message); // one
array_map(fn(User $u) => $u->notify($message), $users); // many
?>
</pre>
</div>
<p>The intent hasn’t changed at all between those two lines: only the cardinality has. Yet the code had to be rewritten from a method call into a function call wrapping a closure. A hypothetical broadcast syntax would let the array itself carry that intent, the same way <code>[]</code> already carries “however many” at the data level:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// hypothetical, not real PHP
$users->notify($message); // $users is an array; call notify() on each element
?>
</pre>
</div>
<p>In this syntax, there is nothing to see. <code>$users</code> contains objects, and each, if any, can process a call to notify(), with the same method and arguments. This is no less readable than making it a foreach loop. No one needs three lines to understand what is happening here.</p>
<h2 id="toc_11">Upgrading some more syntax could be quite fun</h2>
<p>Broadcasting only makes sense where we need to act on each element. There are some situations where we still want to work on the group, not the elements. I see two of them, and you can let your imagination run wild.</p>
<p>The first one is when reaching for class constants, static methods or properties. They would be always the same, as they do not depend on the object, but on the class. Of course, we could optimize this by short circuiting it to a call to <code>array_fill()</code>, unless there are side effects that need to be counted.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
final class NotificationPolicy
{
public const DEFAULT_CHANNEL = 'email';
}
// ['email', 'email'], for sure, but do we need to copy the results or re-run the call?
?>
</pre>
</div>
<p>Generally speaking, a static method or property has no per-element <code>$this</code> to bind to. <code>Logger::record()</code> would stuff the same static values in the same file, again and again. This is quite a trap:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
final class Logger
{
public static function record(string $line): void
{
file_put_contents(self::LOG_PATH, $line, FILE_APPEND);
}
}
?>
</pre>
</div>
<p>The second one is when using the current operators. Does a scalar, which are now virtually banned, auto-transform to an array, with <code>array_fill()</code>, to generate the same operation on each element? Nested arrays may also be worth a look.</p>
<p>Then,</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$var = [1 ,2 , 3];
$var += 2; // [3, 4, 5]?
$var = [[1]];
$var += 2; // [[1, 2]], or [[1], [2]] or an error, as you can add an array and a scalar?
?>
</pre>
</div>
<h1 id="toc_12">A look at the bigger picture</h1>
<p>PHP’s story on cardinality turns out to be further along than it looks at first glance. The container problem, that super-variable that holds zero, one, or many of something, was solved by being untyped: <code>[]</code> absorbs all three without ever needing a wrapper type to convert between them, which is more than most statically-typed languages can say. In fact, a large part of the PHP community tend to ditch this feature away in favor of typing strongly every single piece of data. And that might be a piece of advice I also provide, so, mea culpa.</p>
<p>The traversal problem is solved too, and solved very well: <code>foreach</code> skips empty input for free, Generators can lazily yield nothing, one thing, or an unbounded stream from the same construct, and <code>iterable</code> lets a function stop caring which container it was handed. Let them come to me…</p>
<p>What’s still unfinished is the call-site problem: the fact that <code>do this</code> and <code>do this to each of these</code> have never shared a syntax in PHP, even though the data underneath them always could. Groovy closed that gap at the language level with the star operator <code>*.</code>; Laravel closed a narrower version of it in userland with proxy objects. PHP has the container and the traversal machinery to make the same move: what’s missing is the verb. And a bit of syntax modernization or science fiction.</p>
<p>The post <a href="https://www.exakat.io/foreach-already-knows-php-and-the-zero-one-infinity-rule/">foreach Already Knows: PHP and the Zero-One-Infinity Rule</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>A Hard Memory Ceiling for PHP Scripts That Run for Hours - Exakathttps://www.exakat.io/?p=164872026-10-09T10:31:39.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/cap.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16488" src="https://www.exakat.io/wp-content/uploads/2026/10/cap.640-300x300.jpg" alt="" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/cap.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/cap.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/cap.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/cap.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/cap.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/cap.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>A Hard Memory Ceiling for PHP Scripts That Run for Hours</h1>
<p>You inherit a legacy table full of auto-incrementing integers and a new architecture that wants UUIDs. You write a CLI worker, build an in-memory lookup array mapping old IDs to new ones, and let it run. Forty thousand rows in, the terminal stops mid-scroll and hands you the line every PHP developer eventually meet one day: <em>Allowed memory size exhausted</em>.</p>
<p>The instinctive fix is to raise <code>memory_limit</code>. The correct fix is to notice that you built an unbounded array and called it a lookup table. And once you’ve fixed that, a second problem is waiting right behind it: this migration runs for hours, across a table nobody trusts, on infrastructure nobody promised wouldn’t reboot. If the fix for memory doesn’t also make the script resumable, you haven’t solved anything: you’ve just delayed the crash until row two million, at which point restarting from zero stops being an inconvenience and starts being your weekend.</p>
<p>One structure fixes both problems, because they’re the same problem wearing two hats: an LRU cache backed by SQLite, doing double duty as a checkpoint log. Let’s see how one can do that.</p>
<h2 id="toc_1">Why WeakMap doesn’t apply here</h2>
<p>PHP 8.0 gave us <a href="https://www.php.net/manual/en/class.weakmap.php"><code>WeakMap</code></a>, and it’s tempting to reach for it the moment “memory” and “cache” appear in the same sentence: the garbage collector drops an entry the moment nothing else references its key, no manual eviction required. It’s a real improvement over <code>SplObjectStorage</code>, which has been quietly holding strong references to its keys since PHP 5.3 and leaking memory in exactly the scenario you’d use it to avoid.</p>
<p>But <code>WeakMap</code> keys must be objects. In our case, the lookup table is keyed on database IDs, that is strings or integers, and <code>WeakMap</code> won’t accept them at all. It responds with <code>WeakMap key must be an object</code>, which is basically the exact contrary of the reaction of an array. Even granting yourself the trouble of wrapping every ID in a throwaway object, you’d still be trusting PHP’s generational garbage collector, incrementally improved since 7.4, but never deterministic, to decide <em>when</em> to reclaim memory. You want to keep control. You want a hard cap of exactly <code>$maxItems</code> entries, enforced on every write, not a random moment, sometime after PHP notices the cycle. <code>WeakMap</code> solves a reference-tracking problem. You have a capacity problem. Different tool.</p>
<h2 id="toc_2">Hot data in RAM, everything else on disk</h2>
<p>The actual shape you want is a two-tier cache: a small PHP array holds the most recently touched entries, and SQLite holds all of them, unbounded, on disk. Reads check the array first; on a miss, they pull from SQLite and promote the entry into the array. Writes go to both, immediately.</p>
<p>PHP arrays are ordered by insertion, which makes LRU bookkeeping nearly free: <code>unset()</code> a key and reassign it, and it moves to the end. <code>array_key_first()</code> hands you the eviction candidate without a separate tracking structure. The only work SQLite does is being reliable, fast enough, and, crucially, still there after the process dies.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class MemoryCappedSQLiteCache
{
private PDO $db;
private int $maxMemoryItems;
private array $memoryCache = [];
private PDOStatement $selectStmt;
private PDOStatement $insertStmt;
public function __construct(string $dbPath, int $maxMemoryItems = 100)
{
$this->db = new PDO('sqlite:' . $dbPath);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->maxMemoryItems = $maxMemoryItems;
$this->db->exec('
CREATE TABLE IF NOT EXISTS data_store (
key TEXT PRIMARY KEY,
value TEXT,
created_at INTEGER
)
');
$this->selectStmt = $this->db->prepare('SELECT value FROM data_store WHERE key = ?');
$this->insertStmt = $this->db->prepare(
'INSERT OR REPLACE INTO data_store (key, value, created_at) VALUES (?, ?, ?)'
);
}
public function get(string $key): ?string
{
if (array_key_exists($key, $this->memoryCache)) {
$value = $this->memoryCache[$key];
unset($this->memoryCache[$key]);
$this->memoryCache[$key] = $value; // promote to most-recently-used
return $value;
}
$this->selectStmt->execute([$key]);
$value = $this->selectStmt->fetchColumn();
if ($value === false) {
return null;
}
$this->remember($key, $value);
return $value;
}
public function set(string $key, string $value): void
{
$this->insertStmt->execute([$key, $value, time()]);
$this->remember($key, $value);
}
private function remember(string $key, string $value): void
{
unset($this->memoryCache[$key]);
$this->memoryCache[$key] = $value;
if (count($this->memoryCache) > $this->maxMemoryItems) {
unset($this->memoryCache[array_key_first($this->memoryCache)]);
}
}
}
?>
</pre>
</div>
<p>It is worth pointing at the <code>remember()</code> extraction, because its absence is exactly where a first draft of this class would broke. <code>set()</code> on an existing key looks harmless, <code>$this->memoryCache[$key] = $value</code>, but PHP does not reorder an array on reassignment to an existing key. It only moves to the end on <code>unset()</code> + insert. If you sSkip that step, you have a structure calling itself an LRU cache that never promotes a re-set key. Eventually, it evicts values based on first-write order instead of last-touch order. Cheap bug, and one you’d only notice under load, exactly when you can least afford to.</p>
<h2 id="toc_3">The checkpoint you already own</h2>
<p><code>set()</code> writes to SQLite before it writes to the array. That means the database on disk is never more than one call behind the true migration state, which is a checkpoint mechanism, whether you asked for one or not. Use it deliberately: keep a <code>progress</code> table, read <code>last_id</code> on startup, resume from there.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$db = new PDO('sqlite:./migration_state.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('
CREATE TABLE IF NOT EXISTS progress (
id INTEGER PRIMARY KEY CHECK (id = 1),
last_id INTEGER NOT NULL
)
');
$db->exec('INSERT OR IGNORE INTO progress (id, last_id) VALUES (1, 0)');
$lastProcessedId = (int) $db->query('SELECT last_id FROM progress WHERE id = 1')->fetchColumn();
echo "Resuming from ID {$lastProcessedId}\n";
$fetchBatch = $oldDb->prepare('SELECT * FROM users WHERE id > ? ORDER BY id LIMIT 1000');
$saveCheckpoint = $db->prepare('UPDATE progress SET last_id = ? WHERE id = 1');
$cache = new MemoryCappedSQLiteCache('./migration_state.db');
do {
$fetchBatch->execute([$lastProcessedId]);
$rows = $fetchBatch->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$cache->set((string) $row['id'], generateUuid());
$lastProcessedId = (int) $row['id'];
$saveCheckpoint->execute([$lastProcessedId]);
}
} while (count($rows) === 1000);
?>
</pre>
</div>
<p>That <code>INSERT OR IGNORE</code> seeding line is doing more work than its size suggests. An empty <code>progress</code> table with only an <code>UPDATE</code> statement pointed at it is a checkpoint that silently never checkpoints. <code>UPDATE</code> against zero matching rows succeeds, reports nothing wrong, and the resumable script will restart from ID 0. It’s the kind of bug that passes every manual test, because you only ever run it interrupted-and-resumed by accident once, in production, at 2am. I would recommend grepping your own scripts for that exact pattern: any <code>UPDATE ... WHERE</code> used as a substitute for an upsert, on a table, should be reviewed. Yes, this is me giving you homework.</p>
<p>The <code>do...while</code> around the fetch matters too. A single <code>LIMIT 1000</code> query with no outer loop processes one batch and exits, which quietly turns a resumable migration into a migration you have to manually re-invoke a few thousand times. This is all fine if that’s what your process supervisor is for; but it is worth being honest with yourself about which one you want to built.</p>
<table>
<thead>
<tr>
<th>What broke</th>
<th>Why it passed a glance</th>
<th>The fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>set()</code> doesn’t reorder existing keys</td>
<td>Looks identical to a fresh insert</td>
<td>Route both paths through one <code>remember()</code> that always <code>unset()</code>s first</td>
</tr>
<tr>
<td><code>progress</code> table starts empty</td>
<td><code>UPDATE</code> on zero rows raises no error</td>
<td>Seed the row with <code>INSERT OR IGNORE</code>before ever updating it</td>
</tr>
<tr>
<td>Single <code>LIMIT 1000</code>fetch, no loop</td>
<td>Runs fine, just stops after 1000 rows</td>
<td>Wrap the fetch/checkpoint pair in a <code>do...while</code> on batch size</td>
</tr>
</tbody>
</table>
<h2 id="toc_4">The trade off of this approach</h2>
<p>Reading through SQLite is slower than reading through a PHP array, for sure. But, what it buys back isn’t just a memory ceiling; it’s the automated checkpoint, because durability and eviction turned out to be the same write. The cost worth naming explicitly: every <code>set()</code> here is its own implicit transaction, so PDO commits to disk on every single row. For a migration touching millions of rows, that’s millions of fsyncs. Wrapping each 1000-row batch in <code>$db->beginTransaction()</code> / <code>$db->commit()</code> buys real throughput. This is the price to pay for the risk of losing up to one batch’s progress on a hard crash mid-transaction, instead of losing at most one row. Decide which of those numbers you can live with before you’re the one explaining the choice in an incident channel.</p>
<h2 id="toc_5">The bigger picture</h2>
<p>Once a structure is durable, resumable, and survives its own process dying mid-write, it is tempting to call it a cache. In fact, a cache is allowed to lose data. In this case, it is deliberately built so it doesn’t and can’t. Every property that makes it safe for a nine-hour migration is a property a database has, not a property a cache has. The eviction policy is the only thing left distinguishing <code>data_store</code> from a table you’d just call your database.</p>
<p>In the end, we have now set up a robust system can both limit the amount of memory it uses to process even more data, and can even restart. This is a feature that rest on the shoulders of the data organisation, rather than PHP. Yet, with generators, and tools that walk datasets rather than process then in one time, it is not difficult to prepare PHP to be reborn after dying. Again, and again, and again…</p>
<p>The post <a href="https://www.exakat.io/a-hard-memory-ceiling-for-php-scripts-that-run-for-hours/">A Hard Memory Ceiling for PHP Scripts That Run for Hours</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Recently updated PIE extensions #10 (since October 1st, 2026) - Exakathttps://www.exakat.io/?p=164852026-10-08T05:10:18.000ZExakat<h2 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16294" src="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-300x300.png" alt="Recently updated PIE extensions #10" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a></h2>
<p>26 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/flow-php/arrow-ext">flow-php/arrow-ext</a> (0.45.0): Apache Arrow and Parquet bindings for flow-php/parquet (Rust)</li>
<li><a href="https://packagist.org/packages/phpolygon/php-vio">phpolygon/php-vio</a> (2.30.0): PHP extension for GPU rendering (OpenGL, Vulkan, Metal), audio, video recording, streaming, and input</li>
<li><a href="https://packagist.org/packages/symfony/deepclone">symfony/deepclone</a> (v0.8.8): Export any serializable PHP values as pure arrays – accelerator for Symfony’s DeepCloner</li>
<li><a href="https://packagist.org/packages/phalcon/cphalcon">phalcon/cphalcon</a> (v5.22.1): Phalcon is an open source web framework delivered as a C extension for the PHP language providing high performance and lower resource consumption.</li>
<li><a href="https://packagist.org/packages/flow-php/flow-php-ext">flow-php/flow-php-ext</a> (0.45.0): Flow PHP native extension (Rust) – native column backend, CSV/JSON readers, Parquet ETL through arrow-ext</li>
<li><a href="https://packagist.org/packages/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a> (v1.21.0): Pre-compiled tree-sitter grammars for 372 programming languages</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v2.1.4): Universal LLM API client with Rust-powered polyglot bindings.</li>
<li><a href="https://packagist.org/packages/xberg-io/xberg">xberg-io/xberg</a> (v1.3.6): High-performance document intelligence library</li>
<li><a href="https://packagist.org/packages/xberg-io/html-to-markdown">xberg-io/html-to-markdown</a> (v3.17.2): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/thomas-0816/pdo-duckdb-php">thomas-0816/pdo-duckdb-php</a> (1.5.6.4): PHP PDO Driver for DuckDB, modern analytics</li>
<li><a href="https://packagist.org/packages/extport/grpc">extport/grpc</a> (1.84.0.1): Unofficial PIE-compatible mirror of grpc/grpc</li>
<li><a href="https://packagist.org/packages/extport/protobuf">extport/protobuf</a> (36.2.0.1): Unofficial PIE-compatible mirror of protocolbuffers/protobuf</li>
<li><a href="https://packagist.org/packages/php-io-extensions/ftdi">php-io-extensions/ftdi</a> (0.10.0): The PHP FTDI Extension – bindings for libftdi1</li>
<li><a href="https://packagist.org/packages/php-io-extensions/posi">php-io-extensions/posi</a> (0.10.0): PHP-Controllable UNIX POSIX Extension</li>
<li><a href="https://packagist.org/packages/webpatser/php-resp3">webpatser/php-resp3</a> (v0.2.0): PECL extension that parses RESP3 wire-protocol bytes into PHP values, plus PHP adapters for amphp/redis and Fledge.</li>
<li><a href="https://packagist.org/packages/phpstan/turbo">phpstan/turbo</a> (2.3.0): Native acceleration extension for PHPStan</li>
<li><a href="https://packagist.org/packages/flow-php/pg-query-ext">flow-php/pg-query-ext</a> (0.45.0): PostgreSQL query parser PHP extension using libpg_query</li>
<li><a href="https://packagist.org/packages/xberg-io/crawlberg">xberg-io/crawlberg</a> (v1.10.2): High-performance web crawling engine</li>
<li><a href="https://packagist.org/packages/orieg/expanse-extension">orieg/expanse-extension</a> (v0.11.1): Native PHP Zend Engine extension for Expanse: modern Judy arrays in pure Rust (ext-php-rs). Userland API: orieg/expanse.</li>
<li><a href="https://packagist.org/packages/bangbangda/wecomarchive">bangbangda/wecomarchive</a> (v1.4.0): PHP extension for WeCom (WeChat Work) Chat Archive functionality. Provides object-oriented interface to fetch chat messages, decrypt content, and download media files.</li>
<li><a href="https://packagist.org/packages/frostlake/pdo_frostlake">frostlake/pdo_frostlake</a> (v0.2.0): PDO driver for the Frostlake SQL engine over its HTTP protocol, with pdo_snowflake’s surface</li>
<li><a href="https://packagist.org/packages/rubix/tensor_ext">rubix/tensor_ext</a> (4.1.0): A PHP extension that provides objects for scientific computing in PHP.</li>
<li><a href="https://packagist.org/packages/php-io-extensions/fb">php-io-extensions/fb</a> (0.10.x-dev): Pixel storage in C: FbFormat describes a byte layout, FbBuffer holds one surface in it and reads, writes, converts and copies it</li>
<li><a href="https://packagist.org/packages/resvg-php/resvg">resvg-php/resvg</a> (v0.3.0): Render SVG to PNG in-process, backed by a statically linked build of resvg</li>
<li><a href="https://packagist.org/packages/usearch-php/usearch">usearch-php/usearch</a> (v0.2.0): In-process vector similarity search for PHP, backed by a statically linked build of USearch</li>
<li><a href="https://packagist.org/packages/lcmialichi/php-gpu-tensors">lcmialichi/php-gpu-tensors</a> (v0.1.0-beta.5): Native PHP extension for NVIDIA GPU tensors, CUDA-accelerated computing, and machine-learning workloads.</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-10-since-october-1st-2026/">Recently updated PIE extensions #10 (since October 1st, 2026)</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Discover PHP 8.6 Through Its New Error Messages - Exakathttps://www.exakat.io/?p=164782026-10-05T20:16:58.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16479" src="https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640-300x300.jpg" alt="Discover PHP 8.6 Through Its New Error Messages" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/uneven_steps.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>Discover PHP 8.6 Through Its New Error Messages</h1>
<p><em>A guided tour of what will bark at your code when you move, and how you should care now</em></p>
<p>PHP 8.6 lands in November 2025. You have a few weeks left to run your test suite on the release candidate, spot the deprecation notices, and fix them while nobody watches. Migrating now costs you an afternoon, and even if partially applied, it might improve your code anyway.</p>
<p>With the introduction of new features come error messages: after all, without experience yet, you might very well trespass the extend of the features. So, let’s see these new possible error messages, so you can react faster when you encounter them. If you ever do.</p>
<h2 id="toc_1">No more magic numbers: use the constants</h2>
<p>PHP has always shipped named constants for function arguments. It has also always accepted raw integers instead, without a single complaint. That free ride stops in PHP 8.6 for a growing list of functions.</p>
<p>Take <code>pathinfo()</code>. Before 8.6, you could combine <code>PATHINFO_DIRNAME | PATHINFO_EXTENSION</code> and get… one of them. Which one? The answer depended on internal flag values abd black magic, not on logic. Now PHP throws a <code>ValueError</code>:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-only-one-of-the-pathinfo_*-constants.html">pathinfo(): Argument #2 ($flags) must be only one of the PATHINFO_* constants</a></p></blockquote>
<p>The same treatment applies to <code>array_filter()</code>. Passing <code>42</code> as the mode used to default silently to <code>ARRAY_FILTER_USE_VALUE</code>. Now it rages at you:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-array_filter_use_value,-array_filter_use_key,-or-array_filter_use_both.html">arrayfilter(): Argument #3 ($mode) must be one of ARRAY_FILTER_USE_VALUE, ARRAY_FILTER_USE_KEY, or ARRAY_FILTER_USE_BOTH</a></p></blockquote>
<p>And <code>scandir()</code> follows the same path, refusing invented sorting orders:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">scandir(): Argument #2 ($sortingorder) must be one of the SCANDIR_SORT</a><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">_</a>ASCENDING, SCANDIR<a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">_</a><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">SORT</a><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">_</a>DESCENDING, or SCANDIR<a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">_</a><a href="https://php-errors.readthedocs.io/en/latest/messages/must-be-one-of-the-scandir_sort_ascending,-scandir_sort_descending,-or-scandir_sort_none-constants.html">SORT_NONE constants</a></p></blockquote>
<p>This trend deserves attention. PHP keeps replacing silently-accepted literal values with strict constant checks. Each time, the old behavior was to pick a default and hope nobody notices. The new behavior tells you exactly what went wrong and what to use instead. If you grep your codebase for raw integers passed to these functions, fix them now. The constants existed all along; PHP just stopped being polite about ignoring them.</p>
<h2 id="toc_2">Partial application arrives with guardrails</h2>
<p>PHP 8.6 introduces partial function application, one of the most anticipated features in years. You can write <code>add(1, ?)</code> and get a <code>Closure</code> that remembers the <code>1</code> and waits for the rest. It is elegant, concise, and is also guarded by two brand-new error messages.</p>
<p>The first fires when you supply too many arguments during the partial call, just like any other function call, but with a reminder that you are using PFA:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/partial-application-of-%25s()-expects-at-most-%25d-arguments,-%25d-given.html">Partial application of add() expects at most 2 arguments, 4 given</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function add($a, $b) {
return $a + $b;
}
$partial = add(1, 2, 3, ?); // too many slots
?>
</pre>
</div>
<p>And a partial application cannot sneak extra arguments past the function signature. The <code>?</code> placeholder reserves a slot for later; it does not magically create new parameters. What would they be used for? The body of the function is not changed by PFA, so they would be lost in translation.</p>
<p>The second fires when you supply too few:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/partial-application-of-%25s()-expects-%25s-%25d-arguments,-%25d-given.html">Partial application of add() expects exactly 3 arguments, 2 given</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function add($a, $b, $c) {
return $a + $b + $c;
}
$partial = add(1, ?); // one slot short
?>
</pre>
</div>
<p>Every required parameter needs either a value or a <code>?</code> placeholder. If you want to defer all remaining parameters at once, use <code>...</code> instead: <code>add(1, ...)</code> captures everything still missing. PHP checks the argument count at partial-creation time, not at call time. Better to fail early than to hand you a <code>Closure</code> that would blow up the moment you try to use it.</p>
<h2 id="toc_3">Constructors and destructors get their act cleaned up</h2>
<p>Two long-standing PHP oddities finally get the deprecation notice they deserved for a decade. Until now, they were mostly reported by static analysis.</p>
<p>First, the generator constructor. Yes, you could put <code>yield</code> inside <code>__construct()</code>. Yes, it compiled. No, it never actually ran as a generator, because a generator only starts executing when iterated, and nothing iterates a constructor. The object was born with its initialization silently skipped.</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/making-a-constructor-a-generator-is-deprecated.html">Making a constructor a Generator is deprecated</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class X {
public function __construct() {
yield 1; // this never runs
}
}
new X; // empty object, no warning... until 8.6
?>
</pre>
</div>
<p>Who thought this was a good idea? Nobody did. The feature emerged as a side effect of how generators work: any function with <code>yield</code> becomes a generator, and <code>__construct()</code> was no exception. PHP 8.6 recognizes this as an elephpant trap. The same deprecation applies to destructors, by the way. Geez…</p>
<p>Second, returning a value from a constructor. The <code>new</code> keyword always returns the fresh object, regardless of what <code>__construct()</code> hands back. Any <code>return $value</code> inside a constructor was dead code wearing a disguise:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/returning-a-value-from-a-constructor-is-deprecated.html">Returning a value from a constructor is deprecated</a></p></blockquote>
<p>A bare <code>return;</code> to exit the constructor early remains perfectly fine. Only <code>return $something;</code>triggers the deprecation, because that <code>$something</code> vanishes into the void every single time. And <code>$something</code> could have been as nothing as <code>null</code>.</p>
<h2 id="toc_4">Returning from finally, a silent killer</h2>
<p>Here lies one of the most treacherous patterns in PHP. A <code>return</code> inside a <code>finally</code> block silently replaces whatever the <code>try</code> or <code>catch</code> was about to return. Worse, it swallows any exception that was propagating, without a trace.</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/returning-from-a-finally-block-is-deprecated.html">Returning from a finally block is deprecated</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function getConfig(): array {
try {
return loadConfig(); // may throw
} finally {
return []; // exception? what exception?
}
}
?>
</pre>
</div>
<p>PHP already forbids <code>break</code>, <code>continue</code>, and <code>goto</code> out of a <code>finally</code> block. A <code>throw</code> inside <code>finally</code> at least chains the discarded exception as <code>$previous</code>. But <code>return</code> got a free pass all these years, quietly eating exceptions and return values with no indication anything happened. That era ends now. Move the <code>return</code> out of the <code>finally</code> block, and make the control flow visible.</p>
<h2 id="toc_5">Named variadics versus internal functions</h2>
<p>Internal functions written in C handle their variadic parameters differently from user-land functions. When you spread an associative array into <code>array_merge()</code>, PHP tries to match each string key to a parameter name. Since internal variadic functions only have one name for their catch-all parameter, every other key gets rejected:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/internal-function-%25s%25s%25s()-does-not-accept-named-variadic-arguments.html">Internal function array_merge() does not accept named variadic arguments</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$parts = ['x' => [1], 'y' => [2]];
array_merge(...$parts); // string keys become named arguments
?>
</pre>
</div>
<p>The fix takes one line: wrap the array in <code>array_values()</code> before spreading it. This strips the string keys and turns them into plain numeric indexes, which the variadic parameter happily accepts. If your data comes from a database query or a JSON decode, string keys might lurk where you expected none. Apply <code>array_values()</code> as a habit, just in case.</p>
<h2 id="toc_6">NAN walks into clamp</h2>
<p>PHP 8.6 ships the new <code>clamp()</code> function, which restricts a value to a given range. Simple, useful, and immediately confronted with the eternal troublemaker of floating-point arithmetic: <code>NAN</code>.</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/must-not-be-nan.html">clamp(): Argument #2 ($min) must not be NAN</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
clamp(5, NAN, 10); // NAN as a lower bound makes no sense
?>
</pre>
</div>
<p>The fundamental problem with <code>NAN</code> is that any comparison involving it returns <code>false</code>. Ask “is 5 greater than NAN?” and the answer is no. Ask “is 5 less than NAN?” and the answer is also no. A clamping function built on comparisons cannot do anything meaningful with a bound that defeats all comparisons.</p>
<p>Note that <code>NAN</code> is technically a float. It passes type checks, it slips through type declarations, and it arrives in your code disguised as a perfectly normal number. The <code>range()</code> function already rejects it with “must be a finite number, NAN provided”. Now <code>clamp()</code> does the same. Validate your floats with <code>is_nan()</code> when they come from untrusted sources.</p>
<p>Note that <code>range()</code> had a similar problem with <code>NAN</code>, and <code>INF</code> for that matter. You could get that nice error message:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
range(0, NAN); // range(): Argument #2 ($end) must be a finite number, NAN provided
?>
</pre>
</div>
<p>So, PHP is able to handle infinite numbers. That should attract attention of mathematicians, I guess.</p>
<h2 id="toc_7">Virtual properties resist lazy introspection</h2>
<p>PHP 8.4 introduced property hooks and lazy objects. PHP 8.6 discovers that these two features do not always get along.</p>
<p>A virtual property, that is declared with only a <code>get</code> hook and no backing storage, has no memory slot for storage. The lazy objects API, specifically <code>ReflectionProperty::setRawValueWithoutLazyInitialization()</code>, tries to write directly to that memory slot. When the slot does not exist, the method has nothing to write to:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/cannot-use-%25s()-on-virtual-property-%25ps::$%25ps.html">Cannot use setRawValueWithoutLazyInitialization() on virtual property Foo::$bar</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class Foo {
public int $bar {
get => 5; // virtual: no storage
}
}
$prop = new ReflectionProperty(new Foo, 'bar');
$prop->setRawValueWithoutLazyInitialization(new Foo, 2);
?>
</pre>
</div>
<p>Only properties with actual backing storage, those whose hooks reference <code>$this->propName</code>, support raw value manipulation. Virtual properties compute their value on the fly and have nothing to bypass. If your lazy-object initialization code touches property hooks, check which properties are virtual before calling the raw setter.</p>
<h2 id="toc_8">The readonly function reaches end of life</h2>
<p>When PHP 8.1 introduced <code>readonly</code> as a property modifier, the team carved out a compatibility exception: a global function literally named <code>readonly()</code> could still be declared and called, because WordPress shipped one. That exception kept the peace for four versions. In PHP 8.6, the moratorium ends:</p>
<blockquote><p><a href="https://php-errors.readthedocs.io/en/latest/messages/calling-a-function-%22readonly%22-is-deprecated.html">Calling a function “readonly” is deprecated</a></p></blockquote>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function readonly() {
return 'not a property modifier';
}
echo readonly(); // deprecated in 8.6
?>
</pre>
</div>
<p>The deprecation notice fires on every call, not on the declaration. Declaring the function still works, for now: it is only a deprecation. And here is the fun part: <code>readonly</code> as a method name remains perfectly legal. You can call <code>$obj->readonly()</code> all day long without a single warning. The deprecation targets only bare function calls, as part of the broader effort to fully reserve <code>readonly</code> as a language keyword.</p>
<p>If you maintain a library that defines a <code>readonly()</code> function, rename it. Pick <code>isReadonly()</code>, pick <code>asReadonly()</code>, pick anything that will not collide with the language in PHP 9.0.</p>
<h2 id="toc_9">The takeaway</h2>
<p>PHP 8.6 cleans house. It replaces silent fallbacks with explicit errors, deprecates patterns that never worked as intended, and sets boundaries around its newest features. None of these changes will surprise you if you run your test suite against the RC today.</p>
<p>Every error message mentioned in this article links to its full documentation at <a href="https://php-errors.readthedocs.io/en/latest/">php-errors.readthedocs.io</a>, with code samples, explanations, and migration advice. When PHP hands you a new error message, look it up there. The answer already exists.</p>
<p>Go test your code. November gets closer every day.</p>
<p>The post <a href="https://www.exakat.io/discover-php-8-6-through-its-new-error-messages/">Discover PHP 8.6 Through Its New Error Messages</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Apidays London 2026 - Rob Allenhttps://akrabat.com/?p=75922026-10-05T10:00:00.000ZRob Allen<p>Last week I attended <a href="https://joinfost.io/events/london">FOST London 2026</a>, mostly I was there for <a href="https://apidays.global/events/london">apidays</a> and the <a href="https://events.openapis.org/events/api-days-london-2026/">OpenAPI Initiative track</a>. As ever, there was a set of excellent talks and I particularly liked Abby Bangser's opening keynote, Steven Willmott's talk on how to think about software systems as AI is added to them, and Kristof Van Tomme's thoughts on where developer portals fit into today's AI world.</p>
<p>The OpenAPI track was excellent. We started with Frank Kilcommins covering API workflows before Lorna Mitchell talked about upgrading your OpenAPI spec to the latest 3.2. She really put it into perspective on how old version 3.0 is, and the benefits of updating to the latest version. Kin Lane followed with an interesting talk on OpenAPI extensions and how the help expand your specification, particularly when tied together with Overlays. He was followed by Chris Wood explaining the proposals around security that are in-flight at the moment.</p>
<p>There's a lot going on in the OpenAPI world and I will be paying more attention than I have. In particular, the security proposals look interesting and worth helping out on if I can. I also want to bring the OpenAPI story at one of my clients up to the latest version and see what we can leverage when we're there.</p>
<p>Another talk I was particularly happy to attend was Afsha H.'s talk on accountability in systems where agents work more autonomously. It's important that humans are responsible and do not hide behind "the agent did it", and Afsha's talk provided the things to think about. It was Afsha's first in-person technical conference talk and she knocked it out of the park.</p>
<p>In addition to the talks, the hallway track was good and I met new people starting out in the industry and caught up with old friends. The conversations were interesting and thought-provoking and I find it a very good way to learn new things from people who are actually doing and thinking about them.</p>
<p>It was fantastic to catch up with Phil Sturgeon too, which included a visit to the pub! Donate to his <a href="https://protect.earth/">charity</a> as he's doing amazing work.</p>
<p>If you're into APIs, I recommend attending a conference to find out where the industry is today. As Mehdi Medjaoui said at the start, conferences are where the humans connect (but we won't mention the SOAP joke!).</p>
<p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2026/10/2026IMG_1915-web-1.jpg" alt="Mehdi Medjaoui on stage at apidays." title="IMG_1915-web.jpg" border="0" width="600" /></p>Partial Function Application : Building the Call Along the Way - Exakathttps://www.exakat.io/?p=164752026-10-02T10:18:50.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/10/partial.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16476" src="https://www.exakat.io/wp-content/uploads/2026/10/partial.640-300x300.jpg" alt="Partial Function Application : Building the Call Along the Way
" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/10/partial.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/10/partial.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/10/partial.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/10/partial.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/10/partial.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/10/partial.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>Building the Call Along the Way</h1>
<p>A function call is always atomic with PHP. The target and every argument must converge at the same place, at the same moment, or nothing happens. This doesn’t sound like a limitation, rather a cooking recipe: you’re not going to start your strawberry pie in Winter, with the pie crust, and wait for the fruits to be in season a few months later. Yet, the some ingredient may be mixed, and completed later with the rest of it. Sometimes, the kitchen counter is too small.</p>
<p>In comparison with source code, it means that every call site carries the full weight of assembly. When three arguments are known at boot time and the fourth arrives with the HTTP request, the developer bridges that gap with structure: a closure, a class, a wrapper function, or a re-fetch of values that were perfectly available ten layers up. The bridge costs code estate, and the writing stays hardcoded long after everyone forgets why it exists.</p>
<p><a href="https://wiki.php.net/rfc/partial_function_application_v2">Partial Function Application</a>, landing in PHP 8.6, removes the constraint, at least partially. A call can start where the first arguments live:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$underscore = str_replace(' ', '_', ?);
?>
</pre>
</div>
<p>and finish wherever the last argument finally shows up:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$slug = $underscore($title);
?>
</pre>
</div>
<p>The <code>?</code> placeholder marks an open slot. What travels between the two lines is a <a href="https://www.php.net/manual/en/class.closure.php">Closure</a> and not a hand-written one. PHP derives it from the target function, preserving names, types, defaults, and attributes. Reflection can read it. Static analysis can follow it. The half-made call is a first-class value.</p>
<p>Now, a value that nobody calls is a value that does nothing. PFA closures can be assembled patiently, one argument at configuration time, another at wiring time, a third when the request arrives, and then never be executed. The call was built; the actual call may well never be made. Between now and execution, there might be other condition that apply, and lead to the cancellation for the call. In other times, the arguments would have been grouped and moved, but not used at any level. Similar waste, if you ask me.</p>
<p>Whether that trade-off matters depends on how the partial function applications travel through the codebase, and on the patterns they replace. Let’s see what we can read from the current source code, before the new features gets activated.</p>
<h2 id="toc_1">The closure, by hand</h2>
<p>Before architecture, there is instinct. When some arguments are known and others are not, the PHP developer reaches for a closure:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// since PHP 5.3: capture what is known, leave the rest open
$legal = config('allowed_tags');
$isLegal = function (string $s) use ($legal) {
return in_array($s, $legal, true);
};
// since PHP 7.4: arrow functions, shorter, same idea
$isLegal = fn(string $s): bool => in_array($s, $legal, strict: true);
?>
</pre>
</div>
<p>This closure is partial application, performed manually. The known value enters through <code>use</code>, or through automatic capture in arrow functions, the open slot becomes a parameter, and execution waits for the caller. The <a href="https://wiki.php.net/rfc/partial_function_application_v2">RFC</a> calls the hand-written form “cumbersome” because it forces the author to redeclare every parameter name and type: this is information the target function already carries.</p>
<p>PFA replaces the ceremony:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$isLegal = in_array(?, $legal, strict: true);
?>
</pre>
</div>
<p>One expression. No parameter list, no type annotations, no <code>use</code> clause. The resulting closure inherits its signature from <a href="https://www.php.net/manual/en/function.in-array.php"><code>in_array</code></a>, and in particular, the subset of parameters that <code>?</code> left open.</p>
<h2 id="toc_2">A Class’s job: the closure, objectified, again</h2>
<p>When the open slots need names, discoverability, validation, or collection across many steps and many files, the closure grows into a class. A class whose entire purpose is storing arguments until someone calls <code>run()</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$report = (new ReportBuilder())
->withDataSource($db) // known at wiring time
->withFormat('csv') // known at configuration time
->withDateRange($range) // known at request time
->run(); // the actual call, finally
?>
</pre>
</div>
<p>Query builders work this way. <a href="https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html">Doctrine’s QueryBuilder</a> chains <code>select()</code>, <code>where()</code>, <code>orderBy()</code>, then <code>getResult()</code>. Fluent option resolvers, mock builders in test frameworks, HTTP request builders: the pattern occurs everywhere a call’s arguments arrive in stages.</p>
<p>Each of these classes is a partially applied function written as a type: one field per open slot, one setter per argument, one terminal method for the deferred invocation. The cost scales with the number of arguments: fields, setters, validation, a terminal method, a class per call shape.</p>
<p>PFA collapses the pure-plumbing cases into an expression:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$publish = $book->publish(DateTime::now(), ...);
if ($shouldPublish) {
$publish();
}
?>
</pre>
</div>
<p>The <code>...</code> after the last provided argument signals that everything else remains open, producing a zero-argument closure — a delayed call. No class needed.</p>
<p>Builder classes that carry domain logic, cross-slot validation, conditionals, negotiated defaults, remain classes. PFA replaces argument warehouses, not argument processors.</p>
<h2 id="toc_3">Code inverted around the last argument</h2>
<p>The subtlest workaround, and the most pervasive. Since all arguments must exist at the call site, code migrates to the location of the scarcest argument. The late value becomes the organizing principle:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function makeSlug(string $title): string {
$legal = config('slug.characters'); // available since boot
$locale = LocaleService::current(); // available since request start
return strtolower(str_replace($legal, '_', $title));
}
?>
</pre>
</div>
<p>The function lives here because <code>$title</code> lives here. Everything else, <code>$legal</code>, <code>$locale</code>, gets fetched on the spot, re-derived at every call, because there was nowhere to put it earlier. The shape of the code follows data availability, not logic.</p>
<p>PFA inverts the organization. Stable arguments bind where they originate; only the volatile one stays open:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Boot time: stable arguments applied at their source
$slugifier = str_replace(config('slug.characters'), '_', ?);
// Request time: the last argument arrives, the call completes
$slug = strtolower($slugifier($title));
?>
</pre>
</div>
<p>Larry Garfield, who authored the RFC, <a href="https://thephp.foundation/blog/2025/12/08/partial-application/">notes on the PHP Foundation blog</a> that most uses in practice will follow this shape: reducing a function down to a single remaining argument, applied when the callback’s single parameter arrives. Currying, in everything but name. No, not the basketball player.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$result = array_map(in_array(?, $legal, strict: true), $input);
?>
</pre>
</div>
<h2 id="toc_4">The grandmother of all partial applications</h2>
<p>The oldest workaround predates closures, predates <a href="https://wiki.php.net/rfc/first_class_callable_syntax">first-class callables</a>, predates everything. When the same function keeps appearing with the same leading arguments, the developer promotes it to a named wrapper:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function underscore(string $s): string {
return str_replace(' ', '_', $s);
}
?>
</pre>
</div>
<p>This is a hand-compiled partial application, fossilized into a function definition. Codebases are full of them. Entire CMS ecosystems run on stacks of such wrappers, passed as string callables, or called as a global function, always available. In a way, PFA are hunting down global functions, the way we ostracized the global variables:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
array_map('myproject_sanitize', $arr);
?>
</pre>
</div>
<p>The wrapper is static and greppable, so any IDE finds it, any code search locates every call site. PFA replaces the wrapper with a runtime value:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$underscore = str_replace(' ', '_', ?);
?>
</pre>
</div>
<p>The gain is dynamism: <code>str_replace($from, $to, ?)</code> adapts to its inputs, and no signature gets duplicated. The trade-off is indirection: the call shape lives one step away from the function name. Both forms coexist in PHP 8.6. The named wrapper becomes a choice, not the only option.</p>
<p>But the repeated call without a wrapper remains the pattern most vulnerable to drift:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$a = str_replace(' ', '_', $title);
// ... 40 lines later, in another file
$b = str_replace(' ', '_', $name);
?>
</pre>
</div>
<p>The day the rule changes, every site needs editing. PFA consolidates the rule into one configuration point:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$slug_chars = config('slug.chars');
$underscore = str_replace($slug_chars, '_', ?);
?>
</pre>
</div>
<p>One place to change. Every completion site just calls <code>$underscore()</code>.</p>
<h2 id="toc_5">The trio</h2>
<p>These three features form a long story arc, across PHP versions.</p>
<p><a href="https://wiki.php.net/rfc/first_class_callable_syntax">First-Class Callable syntax</a>, since PHP 8.1, proved that a call can be held before being made. <code>trim(...)</code> wraps the function into a Closure with all arguments pending.</p>
<p>The <a href="https://thephp.foundation/blog/2025/07/11/php-85-adds-pipe-operator/">pipe operator</a>, since PHP 8.5, gave the held call a canonical place to complete. <code>$input |> trim(...)</code> feeds the value and executes in one step: no intermediate variable, no nested parentheses.</p>
<p>PFA, with PHP 8.6, fills the gap between the two: the call can be assembled gradually, one argument at a time, between first-class capture and pipe completion. Garfield <a href="https://stitcher.io/blog/php-86-partial-function-application">calls pipes and PFA</a> “the twins, reunited.”</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$input
|> str_replace(' ', '-', ?)
|> strtolower(?)
|> trim(?);
?>
</pre>
</div>
<h2 id="toc_6">Spotting PFA candidates in today’s code</h2>
<p>Let’s switch our hat, and take the static code analysis one. The workaround family doubles as an audit map. Every workaround still in a codebase marks a spot where PFA can slot in.</p>
<p><strong>Relay closures</strong>: the highest-frequency target. Any arrow function whose body is a single call, whose parameters appear verbatim among the call’s arguments, and whose body contains no logic beyond the call itself:</p>
<table>
<thead>
<tr>
<th>Hand-written form</th>
<th>PFA form</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>fn($x) => f($x, $a, $b)</code></td>
<td><code>f(?, $a, $b)</code></td>
</tr>
<tr>
<td><code>fn($x) => f($a, $x, $b)</code></td>
<td><code>f($a, ?, $b)</code></td>
</tr>
<tr>
<td><code>fn($x) => $this->repo->save($x, $this->tenant)</code></td>
<td><code>$this->repo->save(?, $this->tenant)</code></td>
</tr>
<tr>
<td><code>fn() => $svc->flush($em)</code></td>
<td><code>$svc->flush($em, ...)</code></td>
</tr>
<tr>
<td>wrappers inside pipe chains</td>
<td><code>$x \|> f($const, ?)</code></td>
</tr>
</tbody>
</table>
<p>Closures that contain conditions, transformations, null-coalescing, or multiple statements stay closures. PFA replaces the relay, not the logic.</p>
<p><strong>Repeated literal calls</strong>: <code>str_replace(' ', '_', ...)</code> scattered across files, <code>$date->format('d/m/Y')</code> copy-pasted through a template layer. Grep finds them. Each cluster is one PFA closure built at configuration time, completed at each call site.</p>
<p><strong>Argument-warehouse classes</strong>: the vanishing test settles these. If PHP had always had PFA, would this class exist? If the class stores arguments and forwards them to a single real call, it converts. If it validates, negotiates, or branches on its stored values, it stays.</p>
<h2 id="toc_7">Waste by absence</h2>
<p>PFA closures are cheap to create and carry no side effects until called. That is the feature, and here is a related risk.</p>
<p>A closure built at boot time and stored in a container costs memory for the lifetime of the request, whether or not any code path reaches it. A builder class had the same cost, but the class was visible: it had a name, a file, an import statement. The PFA closure is a value in a variable. Nothing in the code advertises that it was prepared but never used.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Boot: three closures built, all three carried through the request
$normalize = str_replace(' ', '_', ?);
$sanitize = htmlspecialchars(?, ENT_QUOTES, 'UTF-8');
$truncate = mb_substr(?, 0, 255);
// Request: only $normalize is ever called
$slug = $normalize($title);
// $sanitize and $truncate travel the whole pipeline and vanish at shutdown
?>
</pre>
</div>
<p>The preparation cost is small in isolation. Multiplied across a container that eagerly builds PFA closures the way it eagerly builds services, it accumulates. And it does it silently, because the closures leave no trace of non-execution, and no naming conflict in case of repetition.</p>
<p>Static analysis could flag this: a PFA closure assigned but never invoked on any reachable path. That pattern is detectable. Now, imagine that the execution is conditional: cannot get the 4th parameter? well, we’ll throw an exception, and forget it.</p>
<p>PFA will impact the code organisation by linking several methods in a series of steps to build the final call. Either the closure is completed with a new argument, or it is passed to the next step intact, and it is eventually executed. It is now a lot easier to follow how a specific function is executed, and it is always good to see how conditions are applied to data.</p>
<p>I also wonder if PFA will generate a family of function with long list of arguments. After all, a PFA make more sense the more arguments there are.</p>
<p>The post <a href="https://www.exakat.io/partial-function-application-building-the-call-along-the-way/">Partial Function Application : Building the Call Along the Way</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Recently updated PIE extensions #9 (since September 24th, 2026) - Exakathttps://www.exakat.io/?p=164732026-10-01T09:50:31.000ZExakat<h2 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16294" src="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-300x300.png" alt="Recently updated PIE extensions #9" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Recently updated PIE extensions #9 (since September 24th, 2026)</h2>
<p>27 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/iliaal/mdparser">iliaal/mdparser</a> (0.7.0): Native C CommonMark + GitHub Flavored Markdown parser for PHP, targeting CommonMark 0.31 + GFM. ~10-20x faster than pure-PHP parsers, zero runtime dependencies.</li>
<li><a href="https://packagist.org/packages/symfony/deepclone">symfony/deepclone</a> (v0.8.7): Export any serializable PHP values as pure arrays – accelerator for Symfony’s DeepCloner</li>
<li><a href="https://packagist.org/packages/mongodb/mongodb-extension">mongodb/mongodb-extension</a> (2.5.3): MongoDB driver extension</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v2.1.2): Universal LLM API client with Rust-powered polyglot bindings.</li>
<li><a href="https://packagist.org/packages/xberg-io/xberg">xberg-io/xberg</a> (v1.3.0): High-performance document intelligence library</li>
<li><a href="https://packagist.org/packages/xberg-io/html-to-markdown">xberg-io/html-to-markdown</a> (v3.15.1): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/thomas-0816/pdo-duckdb-php">thomas-0816/pdo-duckdb-php</a> (1.5.6.2): PHP PDO Driver for DuckDB, modern analytics</li>
<li><a href="https://packagist.org/packages/iliaal/fast_uuid">iliaal/fast_uuid</a> (0.8.0): Fast RFC 9562 UUID generation (v1/v2/v3/v4/v5/v6/v7/v8 + nil/max) as a PHP C extension, with a ramsey/uuid-shaped object API and procedural fast-path functions.</li>
<li><a href="https://packagist.org/packages/php-io-extensions/ftdi">php-io-extensions/ftdi</a> (0.9.0): The PHP FTDI Extension – bindings for libftdi1</li>
<li><a href="https://packagist.org/packages/php-io-extensions/posi">php-io-extensions/posi</a> (0.9.0): PHP-Controllable UNIX POSIX Extension</li>
<li><a href="https://packagist.org/packages/iliaal/phpser">iliaal/phpser</a> (0.7.0): Fast binary serializer for PHP cache workloads. Decoder-optimized, beats igbinary on packed numerics, deep-nested structures, and same-class DTO batches.</li>
<li><a href="https://packagist.org/packages/iliaal/fastjson">iliaal/fastjson</a> (0.9.0): Fast JSON encode/decode/validate for PHP 8.1+, backed by yyjson. Drop-in alternative to ext/json with namespaced fastjson<em>* functions and json</em>last_error-compatible error reporting.</li>
<li><a href="https://packagist.org/packages/prateekbhujel/php-terminal">prateekbhujel/php-terminal</a> (v1.1.1): Native terminal primitives for PHP CLI on Unix and Windows</li>
<li><a href="https://packagist.org/packages/cachewerk/ext-relay">cachewerk/ext-relay</a> (v0.50.2.1): The fastest Redis client for PHP. 100× faster cache reads, near-zero bandwidth, no code changes required.</li>
<li><a href="https://packagist.org/packages/xberg-io/crawlberg">xberg-io/crawlberg</a> (v1.8.0): High-performance web crawling engine</li>
<li><a href="https://packagist.org/packages/goopil/rabbit-rs-native">goopil/rabbit-rs-native</a> (v0.3.10): High-performance RabbitMQ transport for PHP and Laravel, powered by Rust</li>
<li><a href="https://packagist.org/packages/orieg/expanse-extension">orieg/expanse-extension</a> (v0.9.2): Native PHP Zend Engine extension for Expanse: modern Judy arrays in pure Rust (ext-php-rs). Userland API: orieg/expanse.</li>
<li><a href="https://packagist.org/packages/pecl/timezonedb">pecl/timezonedb</a> (2026.5): This extension is a drop-in replacement for the builtin timezone database that comes with PHP.</li>
<li><a href="https://packagist.org/packages/ptondereau/biscuit-php">ptondereau/biscuit-php</a> (v0.5.4): PHP bindings for Biscuit authorization tokens</li>
<li><a href="https://packagist.org/packages/swoole/swoole">swoole/swoole</a> (v6.3.0-rc1): Swoole is an event-driven, asynchronous, coroutine-based concurrency library with high performance for PHP.</li>
<li><a href="https://packagist.org/packages/uma/secp256k1-nostr">uma/secp256k1-nostr</a> (v1.0.0): secp256k1_nostr extension for PHP</li>
<li><a href="https://packagist.org/packages/win32service/win32service">win32service/win32service</a> (1.2.0alpha1): The win32service extension is a Windows-specific extension that allows PHP to communicate with the Service Control Manager to start, stop, register and unregister the services, and even allows your PHP scripts to run as a service.</li>
<li><a href="https://packagist.org/packages/vitaliyboyko/pcov">vitaliyboyko/pcov</a> (v2.1.2): PCOV coverage driver optimized for large PHP test suites</li>
<li><a href="https://packagist.org/packages/php-io-extensions/pcurl">php-io-extensions/pcurl</a> (0.10.0): libcurl’s multi socket API bound 1:1 into PHP on top of ext-curl handles</li>
<li><a href="https://packagist.org/packages/mosuka/litsea">mosuka/litsea</a> (v0.14.3): PHP extension for Litsea, a compact word segmentation and POS tagging library (no embedded models)</li>
<li><a href="https://packagist.org/packages/rubix/tensor_ext">rubix/tensor_ext</a> (4.0.0-rc3): A PHP extension that provides objects for scientific computing in PHP.</li>
<li><a href="https://packagist.org/packages/greatcode/gcurl">greatcode/gcurl</a> (v0.1.2): Native PHP C extension and client wrapping libcurl-impersonate for browser fingerprint impersonation (TLS/HTTP2).</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-9-since-september-24th-2026/">Recently updated PIE extensions #9 (since September 24th, 2026)</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>ulimit and PHP: When the OS Says No - Exakathttps://www.exakat.io/?p=164692026-09-29T08:54:28.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/limit.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16470" src="https://www.exakat.io/wp-content/uploads/2026/09/limit.640-300x300.jpg" alt="ulimit and PHP: When the OS Says No" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/limit.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/limit.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/limit.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/limit.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/limit.640.jpg 540w" sizes="(max-width: 300px) 100vw, 300px" /></a>ulimit and PHP: When the OS Says No</h1>
<p>You set <code>memory_limit</code> and <code>max_execution_time</code> in <code>php.ini</code> and feel in control. You are, in fact, but you are also not the only one in control. Underneath PHP, the kernel keeps its own ledger of what a process is allowed to do, and it does not negotiate. That ledger is <code>ulimit</code>, and it has opinions PHP never gets to hear about, even as PHP has limits in place to mimic these same resources.</p>
<p>After running into one too many error because my PHP application opened too many files on Mac OSX, I learned about ulimit and its opened file soft limit. There were many others, that are worth learning about.</p>
<h2 id="toc_1">Two Rooms, Two Atmospheres</h2>
<p>PHP’s directives are the polite layer. When you hit <code>memory_limit</code>, you get a catchable fatal error with a message telling you exactly what happened. When you hit a <code>ulimit</code>, and the kernel sends a signal, usually <code>SIGKILL</code> or <code>SIGXCPU</code> or <code>SIGXFSZ</code>, and your process is simply gone. No stack trace, no log line, no <code>register_shutdown_function</code> call in most cases. One process, formerly running: a ghost that never was.</p>
<table>
<thead>
<tr>
<th>Concern</th>
<th>PHP directive</th>
<th>ulimit flag</th>
<th>What happens when you cross it</th>
</tr>
</thead>
<tbody>
<tr>
<td>CPU time</td>
<td><code>max_execution_time</code></td>
<td><code>-t</code></td>
<td>Catchable timeout, then <code>SIGXCPU</code>/<code>SIGKILL</code></td>
</tr>
<tr>
<td>Memory</td>
<td><code>memory_limit</code></td>
<td><code>-v</code> (address space), <code>-d</code>(data segment)</td>
<td>Catchable fatal, then process killed</td>
</tr>
<tr>
<td>Upload size</td>
<td><code>upload_max_filesize</code></td>
<td><code>-f</code></td>
<td><code>SIGXFSZ</code>, then killed on repeat</td>
</tr>
<tr>
<td>Stack depth</td>
<td>none (until 8.3)</td>
<td><code>-s</code></td>
<td>Catchable <code>Error</code> since 8.3, <code>SIGSEGV</code> before</td>
</tr>
<tr>
<td>Open files</td>
<td>none</td>
<td><code>-n</code></td>
<td><code>fopen()</code>/<code>fsockopen()</code>return <code>false</code></td>
</tr>
<tr>
<td>Child processes</td>
<td>none</td>
<td><code>-u</code></td>
<td><code>pcntl_fork()</code> fails, <code>EAGAIN</code></td>
</tr>
<tr>
<td>Core dumps</td>
<td>none</td>
<td><code>-c</code></td>
<td>Core file written or suppressed</td>
</tr>
</tbody>
</table>
<p>That table is the short version of this article. Everything below is what happens in the gap between the second and third column.</p>
<p>This is most useful in three places: shared hosting, where one tenant’s runaway script cannot be allowed to starve the others; containers, where the orchestrator enforces limits from outside PHP entirely; and development, where I’d rather discover a resource assumption on my laptop than in production. A dev environment that is more permissive than prod is a dev environment making you way softer than you should.</p>
<h2 id="toc_2">CPU Time: A Warning Shot, Then None</h2>
<p><code>max_execution_time</code> throws a catchable fatal error, and PHP still runs your registered shutdown functions afterward. You get a chance to log the timeout, flush a buffer, close a connection. <code>ulimit -t</code> gives you none of that by default: cross the soft limit and the kernel sends <code>SIGXCPU</code>; cross the hard limit and it’s <code>SIGKILL</code>, uncatchable. On a Docker container running a worker script, this is the difference between “we logged a timeout” and the container just restarted, for no apparent reason.</p>
<p>The one point to note: <code>SIGXCPU</code> is catchable, if you bother.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Register a handler for the soft CPU-time warning before the hard kill lands
pcntl_async_signals(true);
pcntl_signal(SIGXCPU, function () {
error_log('CPU soft limit reached, cleaning up before SIGKILL arrives');
// you get one shot at this; make it count
});
while (true) {
// burn CPU on purpose for this illustration
}
?>
</pre>
</div>
<div>
<pre>ulimit -t 5 # soft limit hit at 5s, SIGXCPU fires; hard kill follows shortly after
php burn.php</pre>
</div>
<h2 id="toc_3">File Size: Not as Silent as It Looks</h2>
<p><code>upload_max_filesize</code> caps what PHP accepts over HTTP. <code>ulimit -f</code> caps what the process may write to disk, and it is stricter than the “fopen just returns false” story usually told about it. A <code>write()</code> that starts past the limit fails with <code>EFBIG</code> and delivers <code>SIGXFSZ</code> to the process, which terminates it by default, same as any other unhandled signal. Only a write that starts below the limit and crosses it gets the gentler treatment: a short write, fewer bytes than requested, no signal at all.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$fh = fopen('/tmp/big.log', 'w');
$written = fwrite($fh, str_repeat('x', 50 * 1024 * 1024));
// under ulimit -f 10240 (10MB), this either short-writes and returns
// an integer smaller than you asked for, or the process is gone —
// there's rarely a clean false to check
?>
</pre>
</div>
<p>If you want the false-return behavior people expect, ignore the signal first:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
pcntl_async_signals(true);
pcntl_signal(SIGXFSZ, SIG_IGN);
// now fwrite() past the limit returns false instead of killing you
var_dump(file_put_contents('/tmp/big.log', str_repeat('x', 50 * 1024 * 1024)));
?>
</pre>
</div>
<p>The writing basically limit the total size of the file. If the application proceeds with bite-size appends, it will still stop once the file pass the threshold set by the operating system.</p>
<p>On the other hand, <code>ulimit -f</code> does not affect reading from files.</p>
<h2 id="toc_4">Memory: Two Different Ceilings Wearing the Same Name</h2>
<p><code>memory_limit</code> tracks what the Zend engine allocates for PHP values: arrays, objects, strings, resources. <code>ulimit -d</code>, for data segment, and <code>ulimit -v</code>, for virtual address space, are OS-level ceilings on the whole process: PHP binary, extensions, shared libraries, everything. Hit <code>memory_limit</code> and you get <code>Allowed memory size exhausted</code>. Hit <code>-v</code> and the kernel kills the process outright.</p>
<p>One nuance worth knowing before you rely on <code>-d</code>: modern glibc <code>malloc()</code> routes large allocations through <code>mmap()</code> rather than growing the classic data segment, so <code>ulimit -d</code> can undercount what your process is actually holding. <code>ulimit -v</code>, which bounds total address space regardless of how it got mapped, is the one that reliably catches PHP’s big allocations.</p>
<div>
<pre><# belt and suspenders: PHP warns first, the OS enforces the hard ceiling
ulimit -v 524288 # 512MB in KB
php -d memory_limit=256M script.php
</pre>
</div>
<h2 id="toc_5">Stack Depth: PHP Caught Up in 8.3</h2>
<p>Deep recursion used to mean a segfault once you hit <code>ulimit -s</code>. This is not a PHP error, just a dead process, often mid-request. PHP 8.3 added <code>zend.max_allowed_stack_size</code>, and now the engine checks its own call stack against that budget and throws a catchable <code>Error</code> before the OS ever gets involved:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function recurse(int $n): int
{
return recurse($n + 1);
}
try {
recurse(0);
} catch (\Error $e) {
// "Maximum call stack size of X bytes ... reached"
echo $e->getMessage(), PHP_EOL;
}
?>
</pre>
</div>
<p><code>ulimit -s</code> is still there underneath as the hard backstop, set it too low and you can still starve the Zend stack before PHP’s own guard has room to react, but for the ordinary “someone forgot a final condition” bug, PHP now catches its own mistake before the kernel has to.</p>
<h2 id="toc_6">File Descriptors: The Most Popular Of All</h2>
<p><code>ulimit -n</code> is the limit you will meet first, because macOS ships a default soft limit of 256, and any script that opens a handful of files, a couple of database connections, and a cURL handle or two can get there faster than seems reasonable.</p>
<div>
<pre>ulimit -n # check the current soft limit
ulimit -n 10000 # raise it for this shell session
</pre>
</div>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$handles = [];
for ($i = 0; $i < 1000; $i++) {
$fh = fopen("/tmp/fd-test-$i", 'w');
if ($fh === false) {
printf("fopen failed after %d successful opens\n", count($handles));
break;
}
$handles[] = $fh;
}
?>
</pre>
</div>
<p>No exception is thrown. <code>fopen()</code>, <code>fsockopen()</code>, a fresh PDO connection: they all just hand back <code>false</code> or an exception you may not be catching. Sometimes, there is also a warning message telling you the file could not be opened. That is true, and that is misleading. It produces the most confused bug reports. The fix is almost always that someone forgot to check a return value, and the person debugging it wasn’t the person who wrote that line. At best, it was himself, six months prior.</p>
<h2 id="toc_7">Processes and Core Dumps, Briefly</h2>
<p><code>ulimit -u</code> caps how many processes your user may run at once, relevant the moment your app forks workers or shells out repeatedly <code>pcntl_fork()</code>, <code>proc_open()</code>, a queue worker spawning children. Cross it and you get <code>EAGAIN</code>, “resource temporarily unavailable,” which reads like a transient network error and isn’t.</p>
<p><code>ulimit -c</code> controls core dump size. Core dumps are gold for debugging a segfault and a liability if they land on disk in production with a database password sitting in memory. Set it to <code>0</code> outside of a debugging session:</p>
<div>
<pre>ulimit -c 0 # no core dumps
ulimit -c unlimited # full core dumps, for a debugging box only
</pre>
</div>
<h2 id="toc_8">Asking the OS From Inside PHP</h2>
<p><code>posix_getrlimit()</code> reads every limit at once; <code>posix_setrlimit()</code> also exists, to set the same limits, within whatever ceiling the hard limit already allows. So you can’t break free from them, but make them even tighter: it reminds me of certain knots.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$limits = posix_getrlimit();
foreach (['Max open files', 'Max processes', 'Max data size'] as $key) {
echo $key, ': ', $limits[$key]['soft limit'], "\n";
}
?>
</pre>
</div>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// lower the soft file-descriptor limit for a subprocess that should
// misbehave in a contained way, without touching the parent's limit
posix_setrlimit(POSIX_RLIMIT_NOFILE, 64, 64);
?>
</pre>
</div>
<p>An unprivileged process can only move its soft limit up to its hard limit, and can only ever lower the hard limit. If you need a higher ceiling than the hard limit allows, that change happens outside PHP, in <code>/etc/security/limits.conf</code>, the systemd unit, or the <code>--ulimit</code> flag on <code>docker run</code>.</p>
<h2 id="toc_9">Breaking It on Purpose</h2>
<p>The most useful thing you can do with <code>ulimit</code> has nothing to do with production. Set <code>-n</code> to 10 and run your test suite. Set <code>-v</code> to something your app should never approach. Watch what falls over.</p>
<div>
<pre>( ulimit -n 10 && ulimit -v 65536 && php vendor/bin/phpunit )</pre>
</div>
<p>The subshell keeps the tightened limits from leaking into your regular terminal. What breaks under this is usually not a bug in the strict sense: it’s every place your code assumed a resource would always be there, written down as a stack trace instead of as a comment.</p>
<h2 id="toc_10">Watching What PHP Has Open</h2>
<p>PHP has no built-in inventory of its own open file descriptors. <code>get_included_files()</code> tells you what got <code>require</code>d; <code>php_ini_loaded_file()</code> and <code>php_ini_scanned_files()</code> tell you which <code>.ini</code> files were read. None of that is the same as the list of files descriptors currently open, and extensions open handles of their own that never surface in any PHP-level list. Actually, exceptions may also be an opened file by themselves. For the real picture, ask the kernel:</p>
<div>
<pre>lsof -p $(pgrep -f 'php-fpm: pool')</pre>
</div>
<p>May be we could request such a function as a native PHP one. It could be useful to improve performances, by reducing the number of opened files.</p>
<h2 id="toc_11">Where the Two Resource Constraints Actually Meet</h2>
<p>Use <code>ulimit</code> for the limit that must never be crossed, no matter what PHP thinks it’s doing, including bugs or by-pass: a worker script that must die rather than eat the box, a shared host where one tenant cannot be allowed to take the others down. Use PHP directives for the limit you want a chance to react to. Although, in practice, it is rare to take time to climb out of such holes.</p>
<p>In practice you want both, tuned so the soft one fires first:</p>
<div>
<pre>; php.ini — the polite warning
memory_limit = 256M
</pre>
</div>
<div>
<pre># the hard backstop, set where the process actually starts
ulimit -v 524288 # 512MB, comfortably above 256M
php-fpm
</pre>
</div>
<p>The gap between the two numbers is your reaction time. Make it wide enough to matter and narrow enough that the OS limit isn’t just decorative.</p>
<p>The pitfalls, in short: don’t assume <code>fopen()</code>, <code>file_get_contents()</code>, or <code>PDO</code>‘s constructor will always succeed. Check what they return, especially in code that runs under a tightened <code>ulimit</code>. Don’t tune limits only in staging and assume production behaves the same under real load. And close what you open; a script that never calls <code>fclose()</code> is a script slowly negotiating its own file-descriptor limit downward.</p>
<h2 id="toc_12">Conclusion</h2>
<p><code>ulimit</code> is the layer PHP’s error messages were never going to warn you about, because PHP doesn’t get informed before the kernel acts. It finds out the same way you do, by no longer running. Treat the two layers as complementary: PHP’s directives for the failure you want to observe, <code>ulimit</code>for the one you want to guarantee.</p>
<p>The open question is the one from the “breaking it on purpose” section, generalized: if a resource assumption is worth catching in production, why is <code>ulimit -n 10</code> not part of the test matrix already, next to the PHP version and the database driver?</p>
<h2 id="toc_13">Further Reading</h2>
<p>The PHP manual’s <a href="https://www.php.net/manual/en/function.posix-getrlimit.php"><code>posix_getrlimit()</code></a> and <a href="https://www.php.net/manual/en/function.posix-setrlimit.php"><code>posix_setrlimit()</code></a> pages cover the full set of resource constants. <code>man getrlimit</code> (section 2) is the actual kernel contract everything above is built on, signals included. Docker’s <code>--ulimit</code> flag documentation and systemd’s <code>LimitNOFILE=</code>/<code>LimitCPU=</code>-style unit directives cover the two places most PHP processes inherit their limits from without anyone setting them explicitly.</p>
<p>The post <a href="https://www.exakat.io/ulimit-and-php-when-the-os-says-no/">ulimit and PHP: When the OS Says No</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>How to lint PHP code within the source - Exakathttps://www.exakat.io/?p=164662026-09-28T08:53:11.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/inside.640.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16467" src="https://www.exakat.io/wp-content/uploads/2026/09/inside.640-300x300.jpg" alt="How to lint PHP code within the source" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/inside.640-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/inside.640-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/inside.640-500x500.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/inside.640-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/inside.640.jpg 640w, https://www.exakat.io/wp-content/uploads/2026/09/inside.640-300x300@2x.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" /></a>How to lint PHP code within the source</h1>
<p>Ahead-of-time languages check a closed world. Compile a Rust crate, a Go module, a C project with two functions named <code>parse_config</code>, and you never get to run anything at all: the compiler builds the whole symbol table before it hands anything to a linker, and a duplicate name is a compile error you read on a screen that has executed exactly zero lines of your program. Everything gets loaded, every symbol gets resolved, every collision gets flagged, and only then does a single instruction run. The checking and the running are two different eras, and the first one is not allowed to end quietly.</p>
<p>PHP was never built this way. There is no linker, and there is no moment where the language looks at your whole codebase and pronounces it sound and complete. It compiles and runs file by file, as <code>include</code> and <code>require</code> pull them in, in whatever order your own conditionals decide today. A “PHP program” isn’t a fixed graph of symbols the way a Rust crate is: it’s whatever subset of your files happened to get pulled into this particular execution, given this particular input. So the question “is this PHP code valid?” is not a single question. It’s actually three distinct questions. Let’s see what we can understand of these.</p>
<h2 id="toc_1">Lint PHP like a human being</h2>
<p>The obvious move, from inside a running PHP process, is to treat PHP as a black box and interrogate it from outside:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$output = shell_exec('php -l ' . escapeshellarg($file));
?>
</pre>
</div>
<p>This totally works. It’s also faintly absurd: you are already running an interpreter, and your answer to “does this code parse” is to fork a second one, wait for it to boot, and scrape its stdout. It’s the “turn it off and on again” of static checking: dependable, and a small admission that you don’t trust the interpreter you’re standing inside of. It’s also the wrong tool the moment you want this per-keystroke in a language server rather than once per CI run: a process fork per file doesn’t scale to that, and it hands you a string to grep instead of a tree to walk.</p>
<p>The only valid use case is when you want to test the code with a different PHP versions than the one you’re currently running. But that is a specific use case for static analysis authors, so that doesn’t apply to a lot of us.</p>
<p>PHP can answer the same question about itself, natively, three times, at three different depths. The hidden issue is that all these answers cover different grounds. They don’t check the same thing than the previous one, but more. They check three non-overlapping categories of rules, and the deepest one only agrees to check anything if you also let it run.</p>
<h2 id="toc_2">All your tokens are belong to us</h2>
<p><a href="https://php-dictionary.readthedocs.io/en/latest/index/token.ini.html"><code>token_get_all()</code></a>, or its object-oriented sibling <code>PhpToken::</code><a href="https://php-dictionary.readthedocs.io/en/latest/index/phptoken.ini.html"><code>tokenize()</code></a>, added in PHP 8.0, is a pure <a href="https://php-dictionary.readthedocs.io/en/latest/index/tokenizer.ini.html">tokenizer</a>. It pattern-matches its way through a string and hands back an array of tokens. It has no concept of grammar, no concept of balance, no memory of what it saw two tokens ago.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$code = '<?php function ( $a = + ) ;';
var_dump(token_get_all($code));
// T_OPEN_TAG, T_FUNCTION, "(", T_VARIABLE, "=", "+", ")", ";"
// Not a single complaint.
?>
</pre>
</div>
<p>That is not a bug in the tokenizer, it’s the entire point of it: a syntax highlighter has to colour code you haven’t finished typing yet, and a formatter has to touch files that don’t compile so it can hand them back still broken but reindented. Rejecting anything would make it useless for the one job it’s actually built for.</p>
<p>In fact, it is quite possible to clean these tokens, or invent pre-processing rules on these tokens, that could produce valid PHP code.</p>
<h2 id="toc_3">Tokens, meet grammar rules</h2>
<p><code>token_get_all($code, TOKEN_PARSE)</code>, added in PHP 7.2, feeds the same tokens through PHP’s real <a href="https://php-dictionary.readthedocs.io/en/latest/index/parser.ini.html">parser</a>, that is the one that builds the <a href="https://php-dictionary.readthedocs.io/en/latest/index/ast.ini.html">Abstract Syntax Tree</a> PHP itself compiles from. Now grammar actually matters, and a real <a href="https://php-dictionary.readthedocs.io/en/latest/index/parseerror.ini.html"><code>ParseError</code></a> shows up, catchable this time:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
try {
token_get_all('<?php echo "hi"', TOKEN_PARSE); // no closing semicolon
} catch (ParseError $e) {
echo $e->getMessage();
// syntax error, unexpected end of file, expecting "," or ";"
}
?>
</pre>
</div>
<p>The same string handed to plain <code>token_get_all()</code> returns four tokens and no complaint at all. Such a dangling statement isn’t a lexical fact, it’s a grammatical one, and the default tokenizer doesn’t have grammar. <code>TOKEN_PARSE</code> does, but a parser only builds a tree; it has no symbol table. You can ask it something that requires remembering a name across two declarations rather than reading a declaration’s shape, and it goes quiet. This is because it isn’t a grammar fact either:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$code = <<<'PHP'
<?php
class Response {}
class Response {}
PHP;
token_get_all($code, TOKEN_PARSE);
// Nothing. Two syntactically impeccable class declarations, one after the other.
?>
</pre>
</div>
<p>Grammatically, nothing is wrong here. The parser checks each declaration in isolation against a rule that checks if this looks like a class, or an interface or else; it was never asked to remember which names it has already seen. Naming is difficult, and it it the next step.</p>
<h2 id="toc_4">The Only Judge That Counts</h2>
<p>Run the same string instead of parsing it, and the <a href="https://php-dictionary.readthedocs.io/en/latest/index/compiler.ini.html">compiler</a> finally objects:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
eval($code);
// PHP Fatal error: Cannot redeclare class Response
// (previously declared in ... on line 2)
?>
</pre>
</div>
<p>Note that is not a catchable error. Try to wrap it in <code>try</code>/<code>catch (\Throwable)</code> and this still ends the script. It requires PHP to actually compile the file into <a href="https://php-dictionary.readthedocs.io/en/latest/index/opcode.ini.html">opcodes</a> and register each <a href="https://php-dictionary.readthedocs.io/en/latest/index/declaration.ini.html">declaration</a> into the runtime’s symbol tables as it goes. That pass lives in the execution phase of PHP, not only the parsing side.</p>
<p>Here PHP has one more surprise, and it part of the folklore of PHP syntax. Forward references work! You can call a function or instantiate a class defined further down the same code, because unconditional top-level declarations get bound early, during compilation, before the first opcode runs. So does that mean a duplicate class is caught the same way, as a fact about the whole file, before anything executes? Put an <code>exit()</code> between two unconditional declarations and ask PHP directly:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class A {}
echo "reached\n";
exit("stopped\n");
class A {} // never reached
?>
</pre>
</div>
<div>
<pre><code class="language-none">reached
stopped
?>
[/php]
</div>
No fatal error, ever. The second <code>class A {}</code> needed to actually execute to be judged, and <code>exit()</code> denied it the chance. The redeclaration simply never happened. Do the identical experiment with two <code>function f() {}</code> instead, and the file dies before printing a single character:
<div>
<pre><code class="language-none">PHP Fatal error: Cannot redeclare function f() (previously declared on line 2) in ... on line 5
?>
[/php]
</div>
<code>echo "reached\n"</code> never fires. The conflict was caught during compilation of the whole file, exactly the way an AOT linker would, before execution had a chance to reach line 2. Traits and interfaces side with classes, not with functions: believe me, bro, I checked all three. So of PHP's three kinds of user declaration, exactly one gets the ahead-of-time treatment, and it's the one nobody would have bet on. Class, trait, and interface collisions are genuinely runtime facts, dependent on which lines actually execute, and if autoload is triggered or not; function collisions are compile-time facts about the file as a whole, indifferent to whether execution ever gets there. The same keyword, <code>eval</code>, enforces two different eras of checking depending on which kind of name you handed it.
Now, you can put a class inside a conditional, and this runtime dependency stops being a curiosity and starts being a way to hide a collision entirely:
<div>
<pre class="brush: php; title: ; notranslate">
<?php
<?php
if ($legacyMode) {
class Formatter { public function run(): string { return 'legacy'; } }
} else {
class Formatter { public function run(): string { return 'modern'; } }
}
?>
</pre>
</div>
<p>Two declarations named <code>Formatter</code>. Neither <code>TOKEN_PARSE</code> nor a hundred ordinary runs of this file will ever complain, because a single run only ever walks one branch — one <code>Formatter</code> gets declared, no collision, forever, as far as that execution is concerned. The clash isn't a property of the text. It isn't even a property of a single run. It only exists across runs that disagree with each other about <code>$legacyMode</code>, which is a sentence that has no meaning until you fix an execution and then fix another one to compare it against.</p>
<h2 id="toc_5">What Each One Actually Sees</h2>
<table>
<thead>
<tr>
<th></th>
<th>tokenizes text</th>
<th>checks grammar</th>
<th>catches <code>ParseError</code></th>
<th>catches a class/trait/interface collision</th>
<th>catches a function collision</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>token_get_all()</code></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><code>token_get_all(TOKEN_PARSE)</code></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><code>include</code> / <code>eval</code></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td>only if that line executes</td>
<td>always, whole file, before anything runs</td>
</tr>
</tbody>
</table>
<p>When I wrote this table, I realized it looks like a ladder: each string of checks is strictly stronger or longer than the last. But I had to stop using emojis for the last last two columns: <code>include</code> doesn't have one uniform "now I also check declarations" superpower, it has two different policies depending on what kind of name is colliding, and only one of those policies is the AOT-style, whole-file, checked-before-anything-runs guarantee the opening of this piece described. The other is a genuinely runtime fact. It is true only when a specific execution reaches the conflicting line, silent about every execution that didn't. You can't know before execution starts, and, even at that, long time after it has started.</p>
<p>Under an ordinary <code>php-fpm</code> request, that silence is almost cosmetic: each request is a fresh process, one branch wins, the process dies, nothing ever compares notes with the branch that didn't run. Under a persistent worker, such as RoadRunner, Swoole, FrankenPHP, or any of the newer tools that makes PHP run for a long time, that isolation is exactly what's gone. An early request can take the legacy branch, a later request B can take the modern one, in the same worker, and the two class declarations that a linter, a parser, and every previous request found perfectly innocent finally meet, weeks after the branch was written. Kaboom.</p>
<p>Static analyzers exist to close this gap without paying for an execution: Psalm, PHPStan, exakat's own engine walk both arms of every branch at once, something no single run of <code>include</code> will ever do. That's real ground gained over the three built-in tools above. But it's an approximation of running every path, not a report from having run one, and it inherits every hazard of predicting a program instead of asking it. Which leaves an uncomfortable question to answer: is this PHP code valid? The lexer won't answer completely, the parser won't answer completely and it is still down to the PHP engine, during execution, to check how something is running or not.</p>
<p>Unless you write the code without any possible branching such as the one we covered above.</p>
<p>The post <a href="https://www.exakat.io/how-to-lint-php-code-within-the-source/">How to lint PHP code within the source</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Four ways to count to four: PHP strings, iconv, mbstring and grapheme - Exakathttps://www.exakat.io/?p=164612026-09-25T16:48:05.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/four.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16462" src="https://www.exakat.io/wp-content/uploads/2026/09/four.320-300x300.jpg" alt="Four ways to count to four: PHP strings, iconv, mbstring and grapheme" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/four.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/four.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/four.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/four.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Four ways to count to four: PHP strings, iconv, mbstring and grapheme</h1>
<p>Nothing like starting the day with a nice café, in a Parisian café, pun intended. Now, when you ask PHP how long the word <code>"Café"</code> is, you get four answers from four families of functions. Two of them agree, one says 5, and the fourth is waiting for you to add an emoji before it gets interesting. A croissant 🥐 for sure.</p>
<p>PHP is blessed with no more than four string toolboxes. Until recently, I was only aware of three of them, so I’ll start by a review of them all:</p>
<ul>
<li><strong>Native string functions</strong>, such as <code>strlen()</code>, <code>substr()</code>, <code>strtoupper()</code>…, which count <strong>bytes</strong>. They’re fast, always available, and they have no idea what a character is. Did I say they count <strong>bytes</strong>?</li>
<li><strong>iconv</strong>, with <code>iconv_strlen()</code>, <code>iconv_substr()</code>… count <strong>code points</strong>. Code points are identifier for characters in an encoding, such as Unicode, but not only. They cover a huge number of characters, beyond the 256 range of bytes. iconv is a small extension with a narrow job: converting between encodings.</li>
<li><strong>mbstring</strong>, with such function as <code>mb_strlen()</code>, <code>mb_substr()</code>… also counts <strong>code points</strong>. It’s the big extension, with more than 60 functions. They overlap native functions, and deal with multi-bytes strings.</li>
<li><strong>grapheme</strong> functions from intl, with <code>grapheme_strlen()</code>…, count <strong>what a human would call a character</strong>. That solves the problem of characters build with different code points. This is a nuance, and it pays to know where it brings value to the code.</li>
</ul>
<p>Let’s see where all these functions and extension agree, and where they don’t. Obviously, they will.</p>
<h2 id="toc_1">Round 1: how long is a string?</h2>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$nfc = "Café"; // é as one code point (U+00E9)
$nfd = "Cafe\u{0301}"; // é as two code points, e + combining acute accent
$fam = "👨👩👧"; // family emoji (no relationship with the above)
$flag = "🇫🇷"; // a flag, obviously
// strlen mb_strlen iconv_strlen grapheme_strlen
// $nfc 5 4 4 4
// $nfd 6 5 5 4
// $fam 18 5 5 1
// $flag 8 2 2 1
?>
</pre>
</div>
<p>A whole table of figures to count a string does require some explanations:</p>
<ul>
<li><code>strlen()</code> counts bytes. That’s not a bug, it’s the job description. Use it for buffers, binary protocols and <code>Content-Length</code>.</li>
<li><code>mb_strlen()</code> and <code>iconv_strlen()</code> agree on everything: they both count Unicode code points.</li>
<li>Only <code>grapheme_strlen()</code> sees what your users see. The accented <code>é</code> has two spellings in Unicode, called precomposed and decomposed, and only grapheme gives both the same length. The family emoji is five code points glued together with zero-width joiners. The flag is two “regional indicator” letters, F and R, which fonts merge into a flag.</li>
</ul>
<p>If you validate a “max 20 characters” username field with <code>mb_strlen()</code>, a user with a family emoji in their nickname gets charged 5 characters for it. If you use <code>strlen()</code>, they get charged 18. That’s steep for a family.</p>
<h2 id="toc_2">Round 2: how to slice a string</h2>
<p>Measuring is harmless. Cutting is where things get serious and break:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
echo substr("Café", 0, 4); // "Caf\xC3": half an é, so you get �
echo mb_substr("Cafe\u{0301}", 0, 4); // "Cafe": the accent was left behind
echo iconv_substr("Cafe\u{0301}", 0, 4); // "Cafe": same
echo grapheme_substr("Cafe\u{0301}", 0, 4); // "Café"
echo mb_substr("👨👩👧", 0, 1); // "👨": dad, alone
echo grapheme_substr("👨👩👧", 0, 1); // "👨👩👧": the whole family
?>
</pre>
</div>
<p><code>substr()</code> cuts in the middle of a multibyte sequence and leaves invalid UTF-8 behind. Your database may reject it, or quietly save a lovely <code>?</code>. <code>mb_substr()</code> never breaks a code point, but it can still separate a letter from its accent, or a dad from his family. Only <code>grapheme_substr()</code> keeps a grapheme together.</p>
<p>Negative offsets work everywhere. Three families agree on the result, and the fourth brings its own:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
substr("Crème", -3); // "\xA8me": the second half of è, then "me"
mb_substr("Crème", -3); // "ème"
iconv_substr("Crème", -3); // "ème"
grapheme_substr("Crème", -3); // "ème"
?>
</pre>
</div>
<p>Half a crème is still less than you ordered.</p>
<h2 id="toc_3">Round 3: how to search in a string</h2>
<p>This is where the families are closest, and also where the surprises hide.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$s = "Déjà vu";
strpos($s, 'vu'); // 7 (bytes)
mb_strpos($s, 'vu'); // 5
iconv_strpos($s, 'vu'); // 5
grapheme_strpos($s, 'vu'); // 5
?>
</pre>
</div>
<p>Here’s the useful bit: for <strong>valid UTF-8</strong>, <code>strpos()</code> is correct for <em>“is this in there?”</em> A UTF-8 sequence can never match in the middle of another one, so <code>str_contains()</code>, <code>str_starts_with()</code> and <code>str_ends_with()</code> are safe. This is lucky, but it works.</p>
<p>You only need the <code>mb_</code> version when you want the <strong>position</strong> as a character offset, for example to hand it to <code>mb_substr()</code>. Mixing byte positions from <code>strpos()</code> with <code>mb_substr()</code> is a classic bug.</p>
<p>Case-insensitive search is a different story:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
stripos("CAFÉ", "é"); // false: É and é are different bytes, no recognized as upper and lower case
mb_stripos("CAFÉ", "é"); // 3
grapheme_stripos("CAFÉ", "é"); // 3
// iconv_stripos()? The function doesn't exist.
?>
</pre>
</div>
<p>And grapheme has opinions about what counts as a match:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$nfd = "Cafe\u{0301}";
strpos($nfd, 'e'); // 3
mb_strpos($nfd, 'e'); // 3
grapheme_strpos($nfd, 'e'); // false: that 'e' belongs to 'é'
mb_strpos($nfd, "é"); // false: precomposed é isn't in there, byte-wise
grapheme_strpos($nfd, "é"); // 3: grapheme finds it anyway
?>
</pre>
</div>
<p>Depending on your use case, that’s either exactly right or a nasty surprise. Search engines and word filters love it. Code that searches with <code>grapheme_strpos()</code> and then cuts with <code>substr()</code> does not.</p>
<h3 id="toc_4">Some more edge cases</h3>
<p>Out-of-range offsets are one area where all four agree: they throw the same <code>ValueError</code>.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
strpos("abc", "a", 10);
// ValueError: strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)
// same for mb_strpos(), iconv_strpos() and grapheme_strpos()
?>
</pre>
</div>
<p>Empty needles are where they split:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
strpos("abc", ""); // 0
mb_strpos("abc", ""); // 0
grapheme_strpos("abc", ""); // 0
iconv_strpos("abc", ""); // false
?>
</pre>
</div>
<p>iconv never got the PHP 8.0 memo about empty needles. It’s been busy converting charsets.</p>
<h2 id="toc_5">Round 4: upper, lower and the case of longer uppercase strings</h2>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
strtoupper("café"); // "CAFé"
mb_strtoupper("café"); // "CAFÉ"
mb_strtoupper("straße"); // "STRASSE": full case mapping
mb_convert_case("straße", MB_CASE_UPPER_SIMPLE); // "STRAßE"
ucfirst("élan"); // "élan"
mb_ucfirst("élan"); // "Élan" (PHP 8.4+)
ucwords("l'été à paris"); // "L'été à Paris"
mb_convert_case("l'été à paris", MB_CASE_TITLE); // "L'été À Paris"
?>
</pre>
</div>
<p>Since PHP 8.2, <code>strtoupper()</code> and friends ignore the locale configuration of the system and only handle ASCII. That way, th feature is more predictable, and it’s also wrong for anything past the letter z. mbstring handles Unicode case mapping, including the German ß, which turns into two letters when uppercased. So that universal truth that “uppercased string has the same length” assumption just failed an exam.</p>
<p>iconv has <strong>no case conversion functions at all</strong>.</p>
<p>Grapheme also has no case conversion, but that is handled by the rest of the <a href="file:///Users/famille/Desktop/php-strings-iconv-mbstring-grapheme.md">intl</a> extension, which is the host of Grapheme: the <a href="https://www.php.net/manual/en/intlchar.toupper.php">IntlChar</a>has the methods you need. Grapheme’s case-insensitive search does case folding internally, so it won’t give you the result.</p>
<h2 id="toc_6">Round 5: how to deal with invalid strings</h2>
<p>Now let’s feed them invalid strings. By that, we mean strings containing invalid UTF-8 since PHP string are always valid as the byte level. Yet, invalid UTF-8 is the the way of real life:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$bad = "abc\xFFdef";
strlen($bad); // 7
mb_strlen($bad); // 7: the bad byte counts as one character
iconv_strlen($bad); // false + Notice: Detected an illegal character in input string
grapheme_strlen($bad); // null (!), intl_get_error_message() says U_INVALID_CHAR_FOUND
?>
</pre>
</div>
<p>Four functions, four attitudes:</p>
<ul>
<li>native doesn’t care, as expected. It’s valid.</li>
<li>mbstring keeps going,</li>
<li>iconv complains,</li>
<li>grapheme returns <code>null</code>, which isn’t even in the traditional <code>int|false</code> return value.</li>
</ul>
<p>So <code>if (grapheme_strlen($s) === false)</code> will never catch that error. Validate first with <code>mb_check_encoding($s, 'UTF-8')</code>, or clean up with <code>mb_scrub()</code>, which gives <code>"abc?def"</code>.</p>
<h2 id="toc_7">Round 6: all together now, splitting, padding, trimming, reversing</h2>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
str_split("été"); // ["\xC3", "\xA9", "t", "\xC3", "\xA9"]: confetti
mb_str_split("été"); // ["é", "t", "é"]
grapheme_str_split("👨👩👧🇫🇷"); // ["👨👩👧", "🇫🇷"] (PHP 8.4+)
str_pad("été", 6, "*"); // "été*": it thinks été is already 5 long
mb_str_pad("été", 6, "*"); // "été***" (PHP 8.3+)
trim("\u{00A0}été"); // untouched: the non-breaking space isn't ASCII
mb_trim("\u{00A0}été\u{3000}"); // "été" (PHP 8.4+)
strrev("été"); // broken UTF-8
// mb_strrev()? Doesn't exist. Neither does grapheme_strrev().
?>
</pre>
</div>
<p>mbstring has been catching up quickly: <code>mb_str_pad()</code> in 8.3, then <code>mb_trim()</code>, <code>mb_ltrim()</code>, <code>mb_rtrim()</code>, <code>mb_ucfirst()</code> and <code>mb_lcfirst()</code> in 8.4. There’s still no <code>mb_strrev()</code> though, but it may come some day. The usual workaround is <code>implode('', array_reverse(grapheme_str_split($s)))</code>, and yes, it should be grapheme. Reversing by code points moves accents onto the wrong letters, which is a creative way to write French.</p>
<h2 id="toc_8">Round 7: converting strings</h2>
<p>iconv has 10 only functions, and the most important one is the eponymous one: <code>iconv()</code>. This is where it earns its place.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
iconv('UTF-8', 'ASCII//TRANSLIT', 'Crème brûlée'); // "Creme brulee"
iconv('UTF-8', 'ASCII//IGNORE', 'Crème brûlée'); // "Crme brle": not even a desert
iconv('UTF-8', 'ASCII', 'Crème brûlée'); // false + notice
mb_convert_encoding('Crème brûlée', 'ASCII', 'UTF-8'); // "Cr?me br?l?e"
?>
</pre>
</div>
<p><code>//TRANSLIT</code> is great for slugs, but there’s a catch: <strong>the result depends on the underlying C library</strong>. The output above is from glibc, so check <code>ICONV_IMPL</code>. On Alpine’s musl, or on older libiconv builds, you may get <code>?</code> or <code>false</code> instead. Your Docker image just became part of your business logic. If you need a slug that’s reproducible everywhere, intl’s <code>Transliterator::transliterate('Any-Latin; Latin-ASCII', $s)</code> is the portable choice.</p>
<p>For plain conversions like UTF-8 <=> ISO-8859-1, both work:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
bin2hex(iconv('UTF-8', 'ISO-8859-1', 'é')); // "e9"
bin2hex(mb_convert_encoding('é', 'ISO-8859-1', 'UTF-8')); // "e9"
?>
</pre>
</div>
<p>mbstring also has encoding detection, with <code>mb_detect_encoding()</code>, to be taken with a large grain of salt, HTML entities, Japanese kana conversions with <code>mb_convert_kana()</code>, and display width:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
mb_strwidth("日本"); // 4: CJK characters are double-width
mb_strimwidth("Hello 日本語の世界", 0, 10, "…"); // "Hello 日…"
?>
</pre>
</div>
<p>This is handy for aligning console tables, and nobody else offers it.</p>
<h2 id="toc_9">What the string families have in common</h2>
<p>Before the scoreboard, here’s what’s shared, because it’s more than you’d expect:</p>
<ul>
<li><strong>Same naming pattern</strong>, <code>strlen</code> -> <code>mb_strlen</code> / <code>iconv_strlen</code> / <code>grapheme_strlen</code>. The same goes for <code>substr</code>, <code>strpos</code>, <code>strrpos</code>. Swapping one family for another is mostly a search-and-replace, which makes it tempting to do without thinking. Just remember some behavior and return values may change.</li>
<li><strong>Same signatures and conventions</strong>. Negative offsets, <code>false</code> when nothing is found, and <code>ValueError</code> on out-of-range offsets since PHP 8.0.</li>
<li><strong>Same answer on simple text</strong>. On ASCII, all four agree. That’s exactly why the bugs survive testing: your test suite is written in English.</li>
<li><strong>Configurable encoding</strong> for mbstring and iconv: <code>mb_strlen($s, 'ISO-8859-1')</code>, <code>iconv_strlen($s, 'ISO-8859-1')</code>. Both default to <code>default_charset</code>, which is UTF-8. Grapheme is UTF-8 only. By the way, <code>mb_strlen($s, '8bit')</code> is a well-known alias for <code>strlen()</code>.</li>
<li><strong>None of them normalize</strong>. <code>"Café" === "Cafe\u{0301}"</code> is <code>false</code>, whichever family you pick. Use <code>Normalizer::normalize()</code> from intl before comparing, hashing or storing unique keys.</li>
</ul>
<h2 id="toc_10">The scoreboard</h2>
<table>
<thead>
<tr>
<th></th>
<th>native</th>
<th>iconv</th>
<th>mbstring</th>
<th>grapheme</th>
</tr>
</thead>
<tbody>
<tr>
<td>Unit</td>
<td>byte</td>
<td>code point</td>
<td>code point</td>
<td>grapheme cluster</td>
</tr>
<tr>
<td>Functions</td>
<td>~100</td>
<td>10</td>
<td>65</td>
<td>10</td>
</tr>
<tr>
<td>Always available</td>
<td>✅</td>
<td>usually</td>
<td>usually</td>
<td>needs intl</td>
</tr>
<tr>
<td>Case conversion</td>
<td>ASCII only</td>
<td>❌</td>
<td>✅ full Unicode</td>
<td>❌</td>
</tr>
<tr>
<td>Case-insensitive search</td>
<td>ASCII only</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>Split / pad / trim</td>
<td>✅ (bytes)</td>
<td>❌</td>
<td>✅ (8.3/8.4)</td>
<td>split only (8.4)</td>
</tr>
<tr>
<td>Invalid UTF-8</td>
<td>ignores</td>
<td><code>false</code> + notice</td>
<td>counts bytes</td>
<td><code>null</code></td>
</tr>
<tr>
<td>Empty needle</td>
<td>0</td>
<td><code>false</code></td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>Encoding conversion</td>
<td>❌</td>
<td>✅ (libc dependent)</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>Speed</td>
<td>🚀</td>
<td>🚗</td>
<td>🚗</td>
<td>🚲</td>
</tr>
</tbody>
</table>
<p>I just could not resist using emojis in an article about unicode and string manipulation. Sorry, not sorry.</p>
<h2 id="toc_11">So, which to use in what situation?</h2>
<ul>
<li><strong>Bytes</strong>, for storage, protocols, hashing, <code>str_contains()</code> on valid UTF-8: native functions. They’re not wrong, just literal. And fast.</li>
<li><strong>Text processing</strong> for case, trimming, padding, code-point offsets: mbstring. It’s the most popular for multilingual code in PHP codes, so it is safe. If you want to prepare for the future, consider moving to intl and grapheme functions.</li>
<li><strong>Encoding conversion and quick transliteration</strong>: <code>iconv()</code>, as long as you pay attention to which libc you’re running on. For the <code>iconv_str*</code> functions, there’s nothing mbstring doesn’t also do, with fewer notices.</li>
<li><strong>Anything a human will read, count or cut</strong>: truncating titles, length limits in forms, cursor positions, reversing: grapheme. It’s slower and returns odd values on bad input, but it’s the only one that knows a family is one thing.</li>
</ul>
<p>And the golden rule: <strong>never mix units</strong>. A position from <code>strpos()</code> fed to <code>mb_substr()</code>, or from <code>mb_strpos()</code> fed to <code>grapheme_substr()</code>, will work perfectly until the first customer called Zoë or 李.</p>
<p>Happy coding, and may all your strings be exactly as long as they look! May be I’ll take a second 🥐 today…</p>
<p>The post <a href="https://www.exakat.io/four-ways-to-count-to-four-php-strings-iconv-mbstring-and-grapheme/">Four ways to count to four: PHP strings, iconv, mbstring and grapheme</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Recently updated PIE extensions #8 - Exakathttps://www.exakat.io/?p=164572026-09-24T10:59:13.000ZExakat<h2 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16294" src="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-300x300.png" alt="Recently updated PIE extensions #8" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/08/php-pie.320.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Recently updated PIE extensions #8 (since September 17th, 2026)</h2>
<p>18 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/carthage-software/ext-typst">carthage-software/ext-typst</a> (0.4.0): PHP extension for the Typst typesetting engine</li>
<li><a href="https://packagist.org/packages/hmennen90/php-steamworks">hmennen90/php-steamworks</a> (v0.16.0): PHP extension providing access to the Valve Steamworks SDK via a flat C API</li>
<li><a href="https://packagist.org/packages/phpolygon/php-vio">phpolygon/php-vio</a> (2.27.0): PHP extension for GPU rendering (OpenGL, Vulkan, Metal), audio, video recording, streaming, and input</li>
<li><a href="https://packagist.org/packages/symfony/deepclone">symfony/deepclone</a> (v0.8.5): Export any serializable PHP values as pure arrays – accelerator for Symfony’s DeepCloner</li>
<li><a href="https://packagist.org/packages/kreuzberg/kreuzberg">kreuzberg/kreuzberg</a> (4.10.4): High-performance document intelligence for PHP. Extract text, metadata, and structured information from PDFs, Office documents, images, and 75 formats. Powered by Rust core for 10-50x speed improvements.</li>
<li><a href="https://packagist.org/packages/mailmug/php_dlib">mailmug/php_dlib</a> (v2.0.1): PHP extension for the Dlib C++ library (face detection, landmarks, recognition, and clustering).</li>
<li><a href="https://packagist.org/packages/phalcon/cphalcon">phalcon/cphalcon</a> (v5.22.0): Phalcon is an open source web framework delivered as a C extension for the PHP language providing high performance and lower resource consumption.</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v2.0.3): Universal LLM API client with Rust-powered polyglot bindings.</li>
<li><a href="https://packagist.org/packages/xberg-io/xberg">xberg-io/xberg</a> (v1.2.8): High-performance document intelligence library</li>
<li><a href="https://packagist.org/packages/xberg-io/html-to-markdown">xberg-io/html-to-markdown</a> (v3.14.3): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/extport/protobuf">extport/protobuf</a> (36.2): Unofficial PIE-compatible mirror of protocolbuffers/protobuf</li>
<li><a href="https://packagist.org/packages/phpstan/turbo">phpstan/turbo</a> (2.2.15): Native acceleration extension for PHPStan</li>
<li><a href="https://packagist.org/packages/prateekbhujel/php-terminal">prateekbhujel/php-terminal</a> (v1.0.1): Native terminal primitives for PHP CLI on Unix and Windows</li>
<li><a href="https://packagist.org/packages/open-telemetry/ext-opentelemetry">open-telemetry/ext-opentelemetry</a> (1.4.2): Auto-instrumentation extension for OpenTelemetry</li>
<li><a href="https://packagist.org/packages/orieg/expanse-extension">orieg/expanse-extension</a> (v0.8.1): Native PHP Zend Engine extension for Expanse: modern Judy arrays in pure Rust (ext-php-rs). Userland API: orieg/expanse.</li>
<li><a href="https://packagist.org/packages/osmanov/pecl-ev">osmanov/pecl-ev</a> (1.2.4): PECL extension providing interface to libev library</li>
<li><a href="https://packagist.org/packages/swoole/swoole">swoole/swoole</a> (v6.2.3): Swoole is an event-driven, asynchronous, coroutine-based concurrency library with high performance for PHP.</li>
<li><a href="https://packagist.org/packages/win32service/win32service">win32service/win32service</a> (1.1.1beta20): The win32service extension is a Windows-specific extension that allows PHP to communicate with the Service Control Manager to start, stop, register and unregister the services, and even allows your PHP scripts to run as a service.</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-8/">Recently updated PIE extensions #8</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Continuing to Move PHP Open Source Forward - PhpStorm : The IDE that empowers PHP developers | The JetBrains Bloghttps://blog.jetbrains.com/?post_type=phpstorm&p=7423742026-09-24T09:46:38.000ZPhpStorm : The IDE that empowers PHP developers | The JetBrains Blog<p>A year ago, we introduced <a href="https://blog.jetbrains.com/phpstorm/2025/10/moving-php-open-source-forward/">the first wave of PhpStorm’s open-source sponsorships</a>. We truly believe in the power of open source and the importance of it to the PHP community, and so we want to do our bit, too. </p>
<p>Each year, we pick five open-source projects to sponsor for a whole year. Our goal is not only to financially support these projects but also to raise awareness of the crucial role open source plays in PHP development and to encourage others to pitch in as well.</p>
<p>Before announcing the new projects, I want to thank everyone for sending in their suggestions. We know there are many more projects to sponsor, and we had to make some tough decisions. Eventually, we decided on projects we thought would have the biggest impact and the greatest need for financing.</p>
<p>So here goes, the new projects on our sponsor list:</p>
<ul class="wp-block-list">
<li><a href="https://github.com/sponsors/sebastianbergmann" target="_blank" rel="noopener">Sebastian Bergmann</a>, the creator and maintainer of PHPUnit, the most impactful test suite for PHP. </li>
<li><a href="https://github.com/sponsors/geodro" target="_blank" rel="noopener">George Dumitrescu</a>, who’s creating Lerd, an open source tool for managing local PHP installations and servers.</li>
<li><a href="https://github.com/sponsors/crazywhalecc" target="_blank" rel="noopener">Jerry Ma</a>, who’s creating static-php-cli; a tool used for example by FrankenPHP and Native PHP.</li>
<li><a href="https://github.com/sponsors/composer" target="_blank" rel="noopener">Composer</a>, which is basically the backbone of modern PHP.</li>
<li><a href="https://github.com/sponsors/larabelles" target="_blank" rel="noopener">Larabelles</a>, because open source is about more than code; inclusivity is crucial.</li>
</ul>
<p></p>
<p>If you or your company are in a position to pitch in as well, we would highly recommend doing so. Every bit and piece becomes a substantial difference when added together. Apart from sponsoring individual projects, we also stay committed to sponsoring the PHP Foundation, and encourage as many organizations to do so as well.</p>
<div class="buttons">
<div class="buttons__row">
<a href="https://thephp.foundation/sponsor/" class="btn " target="" rel="noopener">Sponsor the PHP Foundation</a>
</div>
</div>
<p>Let’s work together to continue to make PHP the best it can be!</p>
<p></p>PHPenomenal 8.6: A Challenge to For PHP’s Next Release - Exakathttps://www.exakat.io/?p=164542026-09-24T07:33:04.000ZExakat<figure id="attachment_16455" aria-describedby="caption-attachment-16455" style="width: 300px" class="wp-caption alignleft"><a href="https://www.exakat.io/wp-content/uploads/2026/09/sunrise.320.jpg"><img fetchpriority="high" decoding="async" class="wp-image-16455 size-medium" src="https://www.exakat.io/wp-content/uploads/2026/09/sunrise.320-300x300.jpg" alt="PHPenomenal 8.6: A Challenge to For PHP's Next Release" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/sunrise.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/sunrise.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/sunrise.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/sunrise.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a><figcaption id="caption-attachment-16455" class="wp-caption-text">PHPenomenal 8.6: A Challenge to For PHP’s Next Release</figcaption></figure>
<h1 id="toc_0">PHPenomenal 8.6: A Challenge to For PHP’s Next Release</h1>
<p>PHP 8.6 lands on November 19th, 2026, and with it comes a handful of features that have been simmering in RFCs for a while: partial function application, <code>clamp()</code>, a new <code>Time\Duration</code>class, <code>#[\Override]</code> on constants, writes on const-held objects, readonly properties with defaults, and a <code>SortDirection</code> enum. Reading a changelog is one thing. Actually reaching for these features in real code is another — and that gap is exactly what <strong>PHPenomenal 8.6</strong> is built to close.</p>
<h2 id="toc_1">What It Is</h2>
<p>PHPenomenal is a community challenge, open until PHP 8.6’s release, with a deceptively simple brief: write a useful PHP application that actually uses the new PHP 8.6 features. Not a toy snippet demonstrating syntax — a real program that takes input, runs, and produces output within a reasonable time, on a single machine.</p>
<p>“Useful” is left deliberately loose. Fizz-buzz with algebraic numbers counts. So does a calculator that echoes its arguments. The point isn’t to build the next killer app — it’s to get hands-on with language features before they ship, and to see how other developers are interpreting them.</p>
<h2 id="toc_2">The Feature List</h2>
<p>There’s a core list every submission is expected to touch:</p>
<ul>
<li>Partial Function Application</li>
<li><code>Time\Duration</code></li>
<li><code>clamp()</code></li>
<li><code>#[\Override]</code> on a constant</li>
<li>Writes on const-held objects</li>
<li>Readonly property defaults</li>
<li><code>SortDirection</code> enum</li>
</ul>
<p>And a secondary, optional list for extra credit — things like the polling API, <code>trim()</code>‘s new Form-Feed handling, the reworked stream error management, the <code>array_map()</code> optimization, doc comments in function parameters, and <code>__debugInfo()</code> on enums. This secondary list is explicitly open to growing as more PHP 8.6 quirks get discovered and written up.</p>
<h2 id="toc_3">The Rules (There’s Basically One)</h2>
<p>Share at least one application, with its source, by the end of the event. It has to compile under PHP 8.6 — compiling under 8.5 isn’t required and, for some features, won’t even be possible. Beyond that: architect it however you like, add assets, pull in Composer, write tests, run static analysis. Nobody’s stopping you from going overboard.</p>
<p>The one hard constraint is scope: keep the application self-contained on one machine. No downloading a remote dataset just to demonstrate <code>clamp()</code> on an integer.</p>
<h2 id="toc_4">How to Join</h2>
<p>Participation happens through the repo’s issue tracker — open an issue to declare your intent, and update it as you go. A README explaining what you’re building is encouraged, and so are dev diaries and sample output along the way. Feeling ambitious? Open a second issue for a second idea. And browsing other participants’ issues to comment and compare notes is very much part of the fun.</p>
<h2 id="toc_5">Checking Your Work</h2>
<p>The repo ships a small tool for this exact purpose: <code>scripts/check-php86-features.php</code>. It’s not a naive string search — it tokenizes your source with <code>PhpToken::tokenize()</code> and checks the actual token stream, so a feature name sitting in a comment or a string literal won’t falsely count as “used.” Point it at your file:</p>
<div>
<pre><code class="language-none">php scripts/check-php86-features.php path/to/your-file.php</code></pre>
</div>
<p>and it reports, feature by feature, what it found and where — plus what’s still missing.</p>
<p>You’ll also want PHP 8.6 itself on hand to lint against (<code>php8.6 -l yourfile.php</code>). The README lists several ways to get there: pre-release builds from php.net, Docker images, Homebrew on macOS, or compiling from source.</p>
<h2 id="toc_6">Why Bother</h2>
<p>New language features only prove their worth once people try to break them, combine them in unexpected ways, and hit the edges the RFC didn’t anticipate. PHPenomenal turns that process into something social and a little competitive — and yes, there’s a prize: the best (especially the funniest) submissions earn an elePHPant.</p>
<p>The event is run by <strong>Damien Seguy</strong> (@dseguy) and <strong>Jon Purvis</strong> (<a href="https://x.com/JonPurvis_">https://x.com/JonPurvis</a>), reachable on the repo or the <a href="https://discord.phpc.chat/">PHP community Discord</a>.</p>
<p><strong>Deadline</strong>: November 19th, 2026, 00:00 UTC, aka the moment PHP 8.6 officially exists.</p>
<p>If you’ve been meaning to actually understand what <code>Time\Duration</code> or partial function application look like in practice, rather than just reading about them, this is a good excuse.</p>
<p>The post <a href="https://www.exakat.io/phpenomenal-8-6-a-challenge-to-for-phps-next-release/">PHPenomenal 8.6: A Challenge to For PHP’s Next Release</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>Lazy class constants in PHP - Exakathttps://www.exakat.io/?p=164512026-09-22T07:19:50.000ZExakat<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/lazy.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16452" src="https://www.exakat.io/wp-content/uploads/2026/09/lazy.320-300x300.jpg" alt="Lazy class constants in PHP" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/lazy.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/lazy.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/lazy.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/lazy.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Lazy class constants in PHP: fast by design, sometimes surprising</h1>
<p>PHP has class constant, which are named literals. It also supports static constant expressions, where the value is not always a literal. Rather, it’s an expression that PHP doesn’t evaluate immediately. PHP evaluates it the first time the code needs it. This is the concept of lazy execution. It is good for performance, and it also means some bugs only show up at runtime, in the one request that touches the broken constant.</p>
<p>So, power and macros come at a risk of eventual error. Let’s see how this works.</p>
<h2 id="toc_1">How it works</h2>
<p>When PHP compiles a class, it tries to fold each constant expression into a plain value. <code>const A = 2;</code> is an obvious case. <code>const A = 2 * 21;</code> becomes <code>42</code> at compile time. And then, some expressions can’t be folded, because they depend on something that doesn’t exist yet, now or ever:</p>
<ul>
<li>global constants: <code>const PATH = APP_ROOT . '/var';</code></li>
<li>constants from other classes: <code>const B = Dep::VALUE;</code> They must be loaded first.</li>
<li>enum cases: <code>const DEFAULT = Status::On;</code>. They must be accessed first.</li>
<li>expressions that would throw: <code>const X = 1 % 0;</code>. This fails, but it the exception is defered to execution phase.</li>
</ul>
<p>For these, PHP stores the expression as an AST, the famous syntax tree, and marks the constant as “not updated yet”. The first time the constant is read, PHP evaluates the tree, replaces it with the result and uses the stored value after that.</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
class Config {
const OK = 'fine';
const BROKEN = UNDEFINED_THING . '/cache';
}
echo "compiled and running\n";
echo Config::OK, "\n";
echo Config::BROKEN, "\n";
?&amp;amp;gt;
</pre>
</div>
<div>
<pre><code class="language-none">compiled and running
fine
Error: Undefined constant "UNDEFINED_THING"
?>
[/php]
</code></pre>
</div>
<p>The class compiles, <code>php -l</code> reports no syntax errors, and <code>Config::OK</code> works. The error only appears when something reads <code>Config::BROKEN</code>.</p>
<h2 id="toc_2">Why it helps performance</h2>
<p>Evaluating constants lazily saves work in three ways.</p>
<p><strong>1. You don’t pay for constants you never read</strong>. A class may define dozens of constants while a given request uses two of them. PHP doesn’t resolve the rest. It does some compilation work and allocate memory for the constant, its value and the AST. But these are usually cheap, in terms of memory and execution time.</p>
<p><strong>2. Dependencies aren’t autoloaded early</strong>. A constant that points to another class only triggers that class’s autoloader when it’s read. This is the same problem with entity hydration or deep cloning, where one element depends on n elements, which dependes on even more elements and dependencies…</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
spl_autoload_register(function ($c) {
echo "[autoload($c)] ";
// eval() usage is tolerated to keep the illustration simple.
// imagine this is a file inclusion, not a dynamic piece of code
if ($c === 'Dep') { eval('class Dep { const V = 42; }'); }
});
class Svc {
const A = 1;
const B = Dep::V;
}
echo "Svc::A = ", Svc::A, "\n";
echo "Svc::B = ", Svc::B, "\n";
echo "Svc::B again = ", Svc::B, "\n";
?&amp;amp;gt;
</pre>
</div>
<div>
<pre><code class="language-none">Svc::A = 1
Svc::B = [autoload(Dep)] 42
Svc::B again = 42
?>
[/php]
</code></pre>
</div>
<p>Reading <code>Svc::A</code> doesn’t load <code>Dep</code>. Only <code>Svc::B</code> does, and only once. In a large codebase, constants refer to other classes all the time. Resolving them eagerly would pull in large parts of the class graph on every request.</p>
<p><strong>3. Declaration order doesn’t matter</strong>. A constant can refer to a global constant defined later during bootstrap. It works as long as the value exists when the constant is first read.</p>
<p>With OPcache, classes are cached in shared memory with their unresolved expressions. Each request resolves only the constants it uses.</p>
<h2 id="toc_3">Bugs waiting for the right request</h2>
<p>Lazy evaluation delays value building, so it also delays errors. A mistake in a constant expression doesn’t fail when the file is loaded. It fails when some code first reads that constant, which may be rare or only happen in production.</p>
<h3 id="toc_4">Typos and missing dependencies</h3>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
enum Status: string {
case On = 'on';
case Off = 'off';
const DEFAULT = self::Of; // typo
}
?&amp;amp;gt;
</pre>
</div>
<p>This file loads without complaint. Reading <code>Status::On</code> works. Reading <code>Status::Off</code> works. Reading <code>Status::DEFAULT</code> fails with:</p>
<div>
<pre><code class="language-none">Undefined constant self::Of
?>
[/php]
</code></pre>
</div>
<p>A missing class behaves the same way: <code>const B = Missing::VALUE;</code> raises <code>Class "Missing" not found</code>, but only when it’s evaluated.</p>
<h3 id="toc_5">Math that fails at runtime</h3>
<p>PHP doesn’t detect it at compile time when an expression would throw an exception. It saves it for later:</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
class Math {
const X = 1 % 0;
const Y = 10 / 0;
}
echo "loaded\n"; // prints fine
echo Math::X; // DivisionByZeroError: Modulo by zero
?&amp;amp;gt;
</pre>
</div>
<p>Now, real code rarely divides by a literal zero. It does divide by other constants, which can be zero in some configuration or an empty string. Dependencies may hide some errors under several layers of camouflage.</p>
<h3 id="toc_6">Typed constants checked late</h3>
<p>PHP 8.3 added typed class constants. When the value comes from an expression, PHP can only check the type once it computes the value:</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
class T {
const int LIMIT = SOME_ENV_VALUE;
}
define('SOME_ENV_VALUE', '100 items');
echo "loaded\n";
var_dump(T::LIMIT);
// TypeError: Cannot assign string to class constant T::LIMIT of type int
?&amp;amp;gt;
</pre>
</div>
<p>Note the stange situation where PHP report the assignation of a value to a constant.</p>
<h3 id="toc_7"><code>new</code> evaluates every constant</h3>
<p>Reading one constant evaluates only that one. Calling a static method also works. And then, instantiating the class, or reading a static property, evaluates all of the class’s constants:</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
class Foo {
const A = 'a';
const B = Missing::VALUE;
public static $s = 1;
public static function m() { return 'm'; }
}
Foo::m(); // works
Foo::A; // works
Foo::$s; // Error: Class "Missing" not found
new Foo; // Error: Class "Missing" not found
?&amp;amp;gt;
</pre>
</div>
<p>A broken constant that nobody reads can still break <code>new Foo</code> in code that has nothing to do with that constant. Calling a static method works, then reading a static property on the same class fails. And the stack trace points to where the class was used, not where the mistake is.</p>
<h3 id="toc_8">Results that depend on execution order</h3>
<p>A failed evaluation isn’t cached as a failure. If the global constant gets defined later, the next read succeeds:</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
class Late {
const PATH = APP_ROOT . '/var';
}
try {
echo Late::PATH; // Error: Undefined constant "APP_ROOT"
} catch (Error $e) {
// Catch constant errors with this clause
print $e-&amp;amp;gt;getMessage();
}
define('APP_ROOT', '/srv/app');
echo Late::PATH; // /srv/app/var
?&amp;amp;gt;
</pre>
</div>
<p>So the outcome depends on execution order. The code works through the web front controller, where bootstrap has run, and fails in a CLI command, a queue worker or a unit test that skips bootstrap. It’s the kind of bug that gets reported as “only happens sometimes”.</p>
<h2 id="toc_9">Recommendations</h2>
<p><strong>1. Keep constant expressions self-contained</strong>. Prefer literals and constants within the same class, or to classes that are always available. Every reference to a global constant or another class is a dependency that is only checked at runtime.</p>
<p><strong>2. Don’t use constants for environment values</strong>. Constants built from <code>APP_ROOT</code>, <code>define()</code>d settings or config values create the timing problem above. Put that data in a config object, or in a static method that reads configuration explicitly and fails with a clear message.</p>
<p><strong>3. Evaluate every constant in your test suite</strong>. <code>ReflectionClass::getConstants()</code> forces all of a class’s constants to be evaluated, so one test can check the whole codebase:</p>
<div>
<pre class="brush: php; title: ; notranslate">
&amp;amp;lt;?php
public function testAllClassConstantsResolve(): void
{
foreach ($this-&amp;amp;gt;allProjectClasses() as $class) { // e.g. from Composer's classmap
(new ReflectionClass($class))-&amp;amp;gt;getConstants(); // throws if anything is broken
}
$this-&amp;amp;gt;addToAssertionCount(1);
}
?&amp;amp;gt;
</pre>
</div>
<p>This moves errors from “the first request that happens to read the constant” to CI. Run it after your bootstrap, and a second time without it, to catch constants that depend on bootstrap.</p>
<p><strong>4. Use static analysis</strong>. PHPStan, Psalm and Exakat resolve constant expressions without running the code. They report undefined constants and classes, missing enum cases and type mismatches that <code>php -l</code> doesn’t catch. At high enough strictness levels, most of the problems above get flagged before anything runs.</p>
<p><strong>5. Use typed constants</strong>. A typed constant can’t silently turn into the wrong type. Static analyzers can check the type ahead of time, and at runtime a mismatch throws a <code>TypeError</code> where the constant is declared, not further down the line.</p>
<p><strong>6. Remember that <code>new</code> checks every constant</strong>. When <code>new SomeClass</code> throws an error that mentions a class or constant you’ve never heard of, look at that class’s constants first. Many “impossible” instantiation errors come from there.</p>
<p><strong>7. Preload in production for an early warning</strong>. With <code>opcache.preload</code>, classes are compiled when the server starts, and constants that can be resolved at that point are resolved then. It isn’t a complete check, because anything that depends on runtime state stays unresolved. It does move some failures from the first unlucky request to startup.</p>
<h2 id="toc_10">Summary</h2>
<p>Lazy class constants are offer the same advantages and drawbacks than lazy objects. They are a good trade-off: requests don’t pay for constants they don’t use or for class graphs they don’t need. The cost is that constants are no longer validated when the file compiles. An error in a constant expression shows up only when the right code path reads it, or when <code>new</code> runs on its class. Check your constants in CI with reflection and static analysis so those errors don’t reach production.</p>
<p>The post <a href="https://www.exakat.io/lazy-class-constants-in-php/">Lazy class constants in PHP</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>