PHP - BlogFlock
All things PHP
https://blogflock.com/list/VqY0J
2026-09-20T08:38:14.000Z
BlogFlock
exakat
Vale Reviews Your Comments In The Code - Exakat
https://www.exakat.io/?p=16447
2026-09-20T08:38:14.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/vale.png"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16448" src="https://www.exakat.io/wp-content/uploads/2026/09/vale-300x300.png" alt="Vale Reviews Your Comments In The Code" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/vale-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/09/vale-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/09/vale-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/09/vale.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Vale Reviews Your Comments In The Code</h1>
<p>Static analysis will tell you a function’s return type is wrong down to the last nullable. It doesn’t tell you that the docblock two lines above it opens with “This class is responsible for,” restates the method signature it’s sitting on, or, this happened, in a project with genuinely careful code review, is written half in French. <a href="https://vale.sh/">Vale</a> will. It pulls the prose out of source code and runs style rules over it, the same way a linter runs rules over syntax. Nobody pay designs a codebase’s comments the way they design its types, which is exactly why nobody’s checking them.</p>
<p>I pointed it at <a href="https://cecil.app/">Cecil</a>, a mature, well-documented static site generator, 126 PHP files. Its docblocks are better than most projects’. They are full sentences, bullet lists, fenced code samples. Vale ran on it, and found the following, in <code>src/Asset.php</code>, <code>src/Converter/Parsedown.php</code>, <code>src/Generator/VirtualPages.php</code> and <code>src/Step/Menus/Create.php</code>:</p>
<table>
<thead>
<tr>
<th>Location</th>
<th>Finding</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>src/Asset.php:790</code></td>
<td>Returns image size <strong><code>informations</code></strong>.</td>
<td>“information” has no plural in English</td>
</tr>
<tr>
<td><code>src/Asset.php:853</code></td>
<td>Remove <strong><code>redondant</code></strong> ‘/thumbnails/…/’ in the path.</td>
<td>French spelling of “redundant”</td>
</tr>
<tr>
<td><code>src/Converter/Parsedown.php:439</code></td>
<td><strong><code>abord</code></strong> if InlineImage is an animated GIF</td>
<td>“abort”</td>
</tr>
<tr>
<td><code>src/Generator/VirtualPages.php:66</code></td>
<td><strong><code>abord</code></strong> if the page id already exists</td>
<td></td>
</tr>
<tr>
<td><code>src/Step/Menus/Create.php:129</code></td>
<td><strong><code>abord</code></strong> if entry is not enabled</td>
<td></td>
</tr>
</tbody>
</table>
<p>Same French leak, three separate files: I recognize the consistency as, in French, information may be plural: there is no such thing as a <code>pièce d'information</code>. I’m not going to pretend I’m neutral about a project’s comments quietly code-switching into French, I do that too. The leak isn’t the point. The point is that it survived a genuinely good review process for as long as the project has existed, because nothing in that process was built to catch it. Static analysis checks types. Code prettifier checks formatting. None of them opens a comment and reads it. Comments are for human consumption.</p>
<p>Vale finding range from simple case typo to actual intent. For example, it found ten places where the project’s own docs write <em>Twig</em> and the docblocks write <em>twig</em>. And alter, forty class docblocks that open with “This class is responsible for…” instead of saying what the class does. This is the docblock equivalent of a job interview answer that restates the question before answering it. Let’s see how it can be applied to a code base.</p>
<h2 id="toc_1">Getting Vale ready</h2>
<p>Vale is a single Go binary, which means installing it has nothing to do with Composer:</p>
<div>
<pre># macOS
brew install vale
# Debian / Ubuntu — see pkg.haus for the archive setup
sudo apt install vale
# Arch
sudo pacman -S vale
# Windows
winget install -e --id errata-ai.Vale</pre>
</div>
<p>For a PHP project you’ll want the version pinned alongside everything else, so your laptop and CI agree on which rules exist: Vale has versions every week, so there might be evolution within a short time range. Vale ships through the usual registries too. Each one just downloads the same release binary and puts <code>vale</code> on your path:</p>
<div>
<pre># any project, any language
mise use vale@3.22.0
# or via npm / PyPI, if you already have one of them
npm install --save-dev @vvago/vale
pip install vale</pre>
</div>
<div>
<pre>$ vale --version
vale version 3.22.0</pre>
</div>
<p>The one place Vale genuinely isn’t packaged is Composer, and it isn’t an oversight: it’s a Go binary, not a PHP library, and PHP’s package manager has no business fetching it. Install it beside Composer, not through it.</p>
<h2 id="toc_2">Pointing it at real code</h2>
<p>For a life size test, we’re going to use <a href="https://cecil.app/">Cecil.app</a>, the famous PHP static website generator. For a full disclosure, it is authored by <a href="https://phpc.social/@arnaud@gazuji.com">Arnaud Ligny</a>, and available in open source.</p>
<p>There are 126 PHP files under <code>src/</code>, with docblocks rich enough to make this worth doing: they have full sentences, bullet lists, fenced code samples, in English. For example:</p>
<div>
<pre>/**
* The main Cecil builder class.
*
* This class is responsible for building the website by processing various steps,
* managing configuration, and handling content, data, static files, pages, assets,
* menus, taxonomies, and rendering.
* It also provides methods for logging, debugging, and managing build metrics.
*
* ```php
* $config = [
* 'title' => "My website",
* 'baseurl' => 'https://domain.tld/',
* ];
* Builder::create($config)->build();
* ```
*/
class Builder implements BuildContextInterface, LoggerAwareInterface</pre>
</div>
<p>That docblock is already a small preview of everything below: a summary that restates its own class name, a weasel word <code>various</code>, and a fenced code sample that had better not get treated as prose.</p>
<h2 id="toc_3">Teaching Vale that this is Markdown wearing a PHP hat</h2>
<p>Vale reads PHP natively: <code>//</code>, <code>#</code> and <code>/* … */</code> comments come back as scoped blocks, and everything that isn’t a comment is discarded before any rule runs. That part needs no configuration. The two lines that do all the real work are the format association and the file glob:</p>
<div>
<pre>StylesPath = .vale
MinAlertLevel = suggestion
Packages = write-good, proselint, Microsoft
[formats]
php = md
[*.php]
BasedOnStyles = Vale, write-good, proselint, Microsoft</pre>
</div>
<p><code>php = md</code> is the line that makes docblocks legible, and for PHP specifically it isn’t a nicety. It’s the difference between a usable run and a useless one. With the association in place, Vale strips the leading <code>*</code> from every line of a block comment, so a docblock reads as paragraphs and lists instead of one giant bullet point; treats fenced <code></code><code>php</code><code>samples inside docblocks as code and skips them, so</code>Builder::create($config)->build();<code>doesn't get flagged for "using 'is'"; and unlocks</code>TokenIgnores<code>and</code>BlockIgnores`, which are otherwise unavailable on source files at all — both of them earn their keep two sections from now.</p>
<div>
<pre>$ mkdir -p .vale
$ vale sync
SUCCESS Synced 3 package(s) to '/home/you/cecil/.vale'.</pre>
</div>
<p><code>StylesPath</code> has to exist before Vale will run at all. It will not create the directory on your behalf, and a missing one fails with <code>E201 Invalid value</code> pointing at <code>.vale.ini</code>, which reads exactly like a syntax error in your config and is, in fact, a missing folder.</p>
<h2 id="toc_4">The first vale run is a firehose, and that’s normal</h2>
<div>
<pre>$ vale src/
…
✖ 765 errors, 431 warnings and 1036 suggestions in 126 files.
real 0m1.517s</pre>
</div>
<p>2,232 alerts, almost all of them noise, produced in a second and a half. At least, it tells you Vale’s speed was never the problem. A single file shows why:</p>
<div>
<pre>$ vale src/Builder.php
src/Builder.php
4:14 suggestion Try to avoid using 'is'. write-good.E-Prime
6:4 error Consider using the '©' symbol instead of… proselint.Typography
6:8 error Did you really mean 'Arnaud'? Vale.Spelling
9:14 warning 'was distributed' may be passive voice. write-good.Passive
27:15 warning 'is responsible for' is too wordy. write-good.TooWordy
27:69 warning 'various' is a weasel word! write-good.Weasel
51:46 error Use 'aren't' instead of 'are not'. Microsoft.Contractions
112:13 error Did you really mean 'bool'? Vale.Spelling</pre>
</div>
<p>There are three genuinely different problems, sitting in that list, and each needs a different fix:</p>
<table>
<thead>
<tr>
<th>Rule</th>
<th>Hits</th>
<th>Why it fires</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>write-good.E-Prime</code></td>
<td>588</td>
<td>Bans the verb “to be,” full stop. It is a stylistic experiment some writer once picked a fight with, not a standard. It is useless for API docs, which exist to state facts, several of which are facts about what things are.</td>
</tr>
<tr>
<td><code>Vale.Spelling</code></td>
<td>525</td>
<td>This error reports mostly identifiers, such as <code>$baseurl</code>, <code>getFile()</code>, <code>min_word_count</code>, and not prose. Vale doesn’t yet know the difference between a sentence and a variable name. May be the <code>$</code> sign is not obvious enough?</td>
</tr>
<tr>
<td><code>Microsoft.Passive</code> + <code>write-good.Passive</code></td>
<td>271 + more</td>
<td>Two packages independently policing the passive voice, so every real hit gets counted, and complained about, twice.</td>
</tr>
<tr>
<td><code>proselint.Typography</code></td>
<td>139</td>
<td>The <code>(c)</code> in the license header, once per file, 126 times over.</td>
</tr>
<tr>
<td><code>Microsoft.Contractions</code></td>
<td>49</td>
<td>Demands “aren’t” over “are not.” That’s Microsoft’s house style leaking into your codebase’s opinions, not yours.</td>
</tr>
</tbody>
</table>
<p>Vale is not telling you the comments are bad. Vale is telling you it hasn’t been told what this project is yet. It is the same conversation every static analyzer has with a codebase on day one, just conducted in English instead of PHPDoc types.</p>
<h2 id="toc_5">Four moves, in the order that pays off fastest</h2>
<h3 id="toc_6">Turn off the rules that were never arguing with you</h3>
<p>E-Prime alone is a quarter of the noise; contractions and the spell-out-your-acronyms rules are house-style opinions a docblock has no obligation to hold.</p>
<div>
<pre>[*.php]
BasedOnStyles = Vale, write-good, Microsoft
write-good.E-Prime = NO
Microsoft.Contractions = NO
Microsoft.GeneralURL = NO
Microsoft.Acronyms = NO
Microsoft.QuestionMarks = NO</pre>
</div>
<p>This new configuration drops <code>proselint</code> from <code>BasedOnStyles</code> and <code>Packages</code> entirely. These are typography rules, aimed at published essays, and once the license header is excluded, it has nothing left worth saying about a codebase.</p>
<h3 id="toc_7">Give the project a vocabulary</h3>
<p><code>Vocab</code> names a folder under <code>StylesPath/config/vocabularies/</code>. <code>accept.txt</code> lists terms the spellchecker should already know; <code>reject.txt</code> lists terms that should be flagged wherever they appear. Both take one case-sensitive regex per line.</p>
<p><code>.vale/config/vocabularies/Cecil/accept.txt</code></p>
<div>
<pre>Cecil
Symfony
Twig
Parsedown
Imagick
libvips
Phar
Composer
frontmatter
baseurl
slugify
[Pp]aginator
[Cc]onfig
[Bb]ool(ean)?
[Hh][Tt][Mm][Ll]
[Uu][Rr][Ll]
deduplicat(e|ion)</pre>
</div>
<p>And the inverse list, where, satisfyingly, the French spelling leak gets caught for good:</p>
<p><code>.vale/config/vocabularies/Cecil/reject.txt</code></p>
<div>
<pre>abord
informations
redondant</pre>
</div>
<p><strong>One gotcha worth knowing before you write this list yourself</strong>: every entry in <code>accept.txt</code> doubles as a canonical-spelling rule via <code>Vale.Terms</code>. Write <code>html</code> in the list, lowercase, and Vale will start insisting you write <code>HTML</code> as <code>html</code> everywhere else. 386 new alerts appeared the first time I tried it, including <code>Use 'url' instead of 'URL'</code>, forty-six separate times, from a spellchecker that had until that point been perfectly reasonable. Either write the entry in the casing you actually want, or spell it case-insensitively as <code>[Hh][Tt][Mm][Ll]</code>. Used on purpose, this is the exact feature that produced the ten <code>Use 'Twig' instead of 'twig'</code>findings above.</p>
<h3 id="toc_8">Tell it that code sitting inside a sentence is still code</h3>
<p>Most of the remaining spelling alerts are identifiers written bare in prose. <code>TokenIgnores</code> takes a comma-separated list of regexes and strips each match before any rule sees the line:</p>
<div>
<pre>TokenIgnores = (\$[A-Za-z_]\w*), (`[^`]+`), (@\w+[^\n]*), \
([A-Za-z]+\\[\w\\]+), (\w+\(\)), \
([a-z]+_[a-z_]+), ([a-z]+[A-Z]\w*)</pre>
</div>
<p>This is where we make the PHP variable syntax, with its leading <code>$</code> sign active. And, of course, this is not the only syntax we want to apply. So, left to right: variables, backticked spans, annotation lines (<code>@param</code>, <code>@return</code>, <code>@see</code> and the rest), namespaced class names, function calls, <code>snake_case</code>, <code>camelCase</code>. Spelling alerts drop from 525 to 70, and what’s left is almost entirely real. The trailing <code>\</code> line continuations are valid <code>.vale.ini</code> syntax; the value parses identically to writing the same regexes on one very long line.</p>
<h3 id="toc_9">Exempt the paragraph nobody’s ever going to rewrite</h3>
<p>Cecil carries the same eight-line license header in all 126 files. Every Open Source project stats its code file with a licence reminder. Left in, they contribute a passive-voice warning, a typography error and two spelling errors per file. It counts 500 alerts spent reviewing a block of legal boilerplate nobody involved has the authority to change.</p>
<div>
<pre>BlockIgnores = (?s)This file is part of Cecil\..*?source code\.</pre>
</div>
<p><strong>The gotcha here is subtler than it looks</strong>: the obvious regex starts <code>/\*\*</code> or <code>\* This file…</code>, and matches nothing, because step three already happened by the time <code>BlockIgnores</code> runs. The <code>php = md</code> association strips the leading asterisks <em>before</em> the ignore patterns are evaluated, so <code>BlockIgnores</code> is looking at the comment body as plain paragraphs, not as a raw block comment. Write the pattern against that, and add <code>(?s)</code> so <code>.</code> crosses the blank lines in between.</p>
<div>
<pre>before tuning ████████████████████████████████████████ 2,232
after tuning ████████ 432</pre>
</div>
<p>What a clean we just did. Along the way, we have learnt about the code base, and the Vale configuration. Now, what’s left is 144 passive-voice warnings, 70 spelling alerts, 69 wordiness warnings, 13 casing findings and 5 rejected terms: oof. Yet, that is a list a human being could actually sit down and work through in an afternoon, which was never true of the first number.</p>
<h2 id="toc_10">Writing rules for docblocks specifically</h2>
<p>Every comment Vale extracts carries a scope. It is a one-line comment is <code>text.comment.line.php</code>, a block comment <code>text.comment.block.php</code>. A rule that declares one of those runs only there, which is what makes it possible to hold a published API docblock to a stricter standard than a throwaway <code>//</code> note two lines below it.</p>
<p>A style is just a folder of YAML under <code>StylesPath</code>. The first rule targets block comments only:</p>
<p><code>.vale/Docblock/Summary.yml</code></p>
<div>
<pre class="brush: yaml; title: ; notranslate">
extends: existence
message: "Start the summary with a verb: '%s' repeats what the signature already says."
link: https://www.php-fig.org/psr/psr-5/
level: warning
scope: text.comment.block.php
nonword: true
tokens:
- '(?i)^\s*This (?:class|function|method|file|property|constant)\b'
</pre>
</div>
<p><code>.vale/Docblock/Marker.yml</code></p>
<div>
<pre class="brush: yaml; title: ; notranslate">
extends: existence
message: "Leftover '%s' marker: link an issue or remove it."
level: error
scope: text.comment
tokens:
- TODO
- FIXME
- XXX
- HACK
</pre>
</div>
<p>Add <code>Docblock</code> to <code>BasedOnStyles</code> and it runs alongside the packages. Applied to Cecil, the summary rule fires 40 times, every single one on a class or method docblock that opens by re-announcing the thing it’s already attached to:</p>
<div>
<pre>$ vale --filter='.Name matches "Docblock.*"' src/
src/Builder.php
27:4 warning Start the summary with a verb: 'This class' repeats… Docblock.Summary
271:8 warning Start the summary with a verb: 'This method' repeats… Docblock.Summary
src/Command/AbstractCommand.php
36:4 warning Start the summary with a verb: 'This class' repeats… Docblock.Summary</pre>
</div>
<p><code>--filter</code> is worth remembering for itself: it takes an expression over the alert’s fields, so <code>--filter='.Level == "error"'</code> or <code>--filter='.Name in ["Vale.Avoid", "Vale.Terms"]'</code> lets you work through one category at a time without touching the config file at all.</p>
<p>Drop this fourteen-line file somewhere and every rule type fires on it at once, in miniature:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
/**
* This class is responsible for handling the various user informations.
*
* It is very important that the cache is invalidated by the caller.
*/
class UserRepository
{
// TODO: abord if the id is empty
public function find(int $id): ?User
{
}
}
</pre>
</div>
<div>
<pre>$ vale demo.php
demo.php
4:4 warning Start the summary with a verb: 'This class' repeats… Docblock.Summary
4:15 warning 'is responsible for' is too wordy. write-good.TooWordy
4:47 warning 'various' is a weasel word! write-good.Weasel
4:60 error Avoid using 'informations'. Vale.Avoid
6:4 warning 'It is' is too wordy. write-good.TooWordy
6:10 warning Remove 'very' if it's not important to the meaning. Microsoft.Adverbs
6:40 warning 'is invalidated' may be passive voice. write-good.Passive
10:8 error Leftover 'TODO' marker: link an issue or remove it. Docblock.Marker
10:14 error Avoid using 'abord'. Vale.Avoid
✖ 5 errors, 7 warnings and 0 suggestions in 1 file.</pre>
</div>
<p>Now, that demo file is trying too hard, deliberately, but every fault it’s committing showed up for real, somewhere in Cecil’s 126 files, once each.</p>
<h2 id="toc_11">Putting it in CI without picking a fight with day one</h2>
<p>Vale exits with a non-zero value when any alert at or above <code>MinAlertLevel</code> is found, so <code>--minAlertLevel</code> is the severity dial. The arrangement that actually survives contact with an existing codebase is to fail the build only on what’s been explicitly declared unacceptable. This is the case with rejected terms, leftover markers, wrong casing. On the other hand, it should still be printing the advisory warnings for anyone who wants to read them.</p>
<p><code>.github/workflows/vale.yml</code></p>
<div>
<pre class="brush: yaml; title: ; notranslate">
name: Prose
on: [push, pull_request]
jobs:
vale:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: vale-cli/vale-action@v2
with:
version: 3.22.0
files: src
fail_on_error: true
</pre>
</div>
<p>Or, without the action, two lines suffice in any runner:</p>
<div>
<pre>vale sync
vale --minAlertLevel=error src/ # exit 1 on errors, warnings still printed</pre>
</div>
<p>One flag worth knowing on a first adoption: <code>--output=JSON</code>. Aggregating that by rule name is exactly how the table earlier in this piece got built, and it’s the fastest way to decide what to switch off next, rather than guessing.</p>
<div>
<pre>vale --output=JSON src/ | jq -r '.[][] | .Check' | sort | uniq -c | sort -rn</pre>
</div>
<div>
<pre> 144 write-good.Passive
70 Vale.Spelling
69 write-good.TooWordy
40 Docblock.Summary
24 Microsoft.Quotes</pre>
</div>
<p>On a large, unreviewed codebase, start with <code>MinAlertLevel = error</code> and an empty <code>reject.txt</code>, so the build is green from the first commit. Every time the team actually agrees on a rule, like a banned term, a house spelling, a docblock convention, promote it to an error. The advisory warnings sit in the log the whole time, visible, opinionated, and blocking absolutely nothing.</p>
<h2 id="toc_12">The finished config</h2>
<p>Here is the configuration file that brought down 2,232 alerts down to 432.</p>
<p><code>.vale.ini</code></p>
<div>
<pre>StylesPath = .vale
MinAlertLevel = warning
Packages = write-good, Microsoft
Vocab = Cecil
[formats]
php = md
[*.php]
BasedOnStyles = Vale, write-good, Microsoft, Docblock
# The licence header, repeated in all 126 files. Matched against the
# comment body after the leading asterisks have been stripped.
BlockIgnores = (?s)This file is part of Cecil\..*?source code\.
# Identifiers, annotations and code spans are not prose.
TokenIgnores = (\$[A-Za-z_]\w*), (`[^`]+`), (@\w+[^\n]*), \
([A-Za-z]+\\[\w\\]+), (\w+\(\)), \
([a-z]+_[a-z_]+), ([a-z]+[A-Z]\w*)
# House-style rules a docblock has no reason to follow.
write-good.E-Prime = NO
Microsoft.Contractions = NO
Microsoft.GeneralURL = NO
Microsoft.Acronyms = NO
Microsoft.QuestionMarks = NO
</pre>
</div>
<div>
<pre>cecil/
├── .vale.ini
├── .vale/
│ ├── config/vocabularies/Cecil/
│ │ ├── accept.txt
│ │ └── reject.txt
│ ├── Docblock/
│ │ ├── Summary.yml
│ │ └── Marker.yml
│ ├── Microsoft/ # vale sync
│ └── write-good/ # vale sync
└── src/</pre>
</div>
<p>Commit <code>.vale.ini</code>, the vocabularies and your own <code>Docblock/</code> rules to the repository. Then, gitignore the synced package folders and let <code>vale sync</code> refill them on every machine and every CI run.</p>
<p>For reference, the four PHP comment forms and where they land:</p>
<table>
<thead>
<tr>
<th>Syntax</th>
<th>Scope</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>// line comment</code></td>
<td><code>text.comment.line.php</code></td>
</tr>
<tr>
<td><code># line comment</code></td>
<td><code>text.comment.line.php</code></td>
</tr>
<tr>
<td><code>/* inline */</code></td>
<td><code>text.comment.line.php</code></td>
</tr>
<tr>
<td><code>/** docblock */</code></td>
<td><code>text.comment.block.php</code></td>
</tr>
</tbody>
</table>
<p>Note that scopes match by containment, not by prefix: a rule scoped <code>comment</code> catches all four forms, <code>comment.block</code> catches docblocks in any language Vale understands, and <code>text.comment.block.php</code> narrows all the way down to PHP docblocks alone.</p>
<h2 id="toc_13">Who read the comments anyway?</h2>
<p>PHP has spent the last decade getting steadily better at making the compiler check things a human used to have to remember. Types, nullability, exhaustiveness, the whole run of the type-system work that keeps closing gaps the language left open a version or two earlier. The same engine has always striped the source code of comments and whitespaces, as they are useless to code execution, event the PHPdoc with extra types. A function can be fully typed, covered, statically verified down to the last edge case, and still be documented by a comment that lies about what it does, restates its own signature, or, as it turns out, even in a codebase good enough to be worth cloning as an example, applies grammar from a different language.</p>
<p>Nobody has time to audit comments, because nothing forces the question the way a red squiggly line under a type mismatch does. Vale doesn’t fix that asymmetry so much as it makes the asymmetry visible for the first time: 2,232 things a well-reviewed project’s comments were quietly getting away with, tunable down to 432 worth a person’s actual attention. Whether prose ever gets the same treatment types did, checked by default, invisible when it’s clean, argued about only at the edges, or whether “the comment sounds right” just stays a taste nobody automates, is the same open question this project’s own docblocks have been answering, one <code>informations</code> at a time, since before anyone thought to ask.</p>
<p>The post <a href="https://www.exakat.io/vale-reviews-your-comments-in-the-code/">Vale Reviews Your Comments In The Code</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Webmention support - Blog entries :: mwop.net
https://mwop.net/blog/2026-09-18-webmentions.html
2026-09-18T15:12:40.000Z
Blog entries :: mwop.net
<p>I've known about <a href="https://indieweb.org/Webmention">webmentions</a> for years, as well as <a href="https://microformats.org">microformats</a>; I've even included microformats in my markup for probably close to a decade at this point. But accepting and sending webmentions, while it seemed like a great idea, just never really made it to my priority list.</p>
<p>Until it did.</p>
<p>Inspired by <a href="https://www.jamestitcumb.com/posts/webmentions" class="u-in-reply-to">James Titcumb</a> and <a href="https://godless-internets.org/2026/08/27/critique-of-microformats2" class="in-reply-to">Owls</a>, I started investigating how to implement webmentions, and ended up going down the rabbit hole myself.</p>
<p>There has long been a <a href="https://packagist.org/packages/mf2/mf2">PHP API for Microformats (php-mf2)</a>. It's not been updated in 4 years — largely because it's complete — and uses PCRE for finding microformats and validating URIs. Here's the thing: the last two releases of PHP (8.4 and 8.5 at the time I write this) have included some amazing features that make things like this both far simpler and more accurate. These include the new HTML5 parser included in PHP 8.4 — which even provides <code>querySelector()</code> and <code>querySelectorAll()</code> implementations — and the <a href="https://www.php.net/manual/en/book.uri.php">Uri extension</a> included in PHP 8.5, which provides robust, standards-based URL parsing and validation.</p>
<p>Add in things like <a href="https://www.php-fig.org/psr/psr-18/">PSR-18's HTTP Client</a>, and it seemed possible to create a pretty minimal library that gives full-featured support for both microformats and webmentions.</p>
<p>So, I decided to try my hand at it.</p>
<p>I'll likely open source the libraries I created (one for parsing microformats, another for sending and receiving webmentions), but I want to wait until I've ironed out any edge cases. While I've tested against the various resources that <a href="https://webmention.rocks">webmention.rocks</a> provides, as well as the <a href="https://microformats.org">microformats website</a>, I'm sure that I'll be observing some oddities in the wild. But I have webmention receiving in place, with microformats discovery for identifying the type of webmention sent to my site. I have also setup a post-publication process for parsing my own content for links, and performing webmention discovery and sending. I want these all to run for a while to find what is working and what isn't in the real world.</p>
<p>In the meantime, feel free to send webmentions to my <a href="/blog">blog posts</a>, <a href="/art">art gallery images</a>, or <a href="/now">Now pages</a>. Just like my comments, I moderate all webmentions before publishing them (someday I'll blog about how I accomplished that as well).</p>
<div class="h-entry">
<img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
<a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-09-18-webmentions.html">Webmention support</a> was originally
published <time class="dt-published" datetime="2026-09-18T10:12:40-05:00">18 September 2026</time>
on <a href="https://mwop.net">https://mwop.net</a> by
<a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O'Phinney</a>.
</div>
Recently updated PIE extensions #7 (since September 10th, 2026) - Exakat
https://www.exakat.io/?p=16444
2026-09-17T05:53:12.000Z
Exakat
<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="PHP Pie updates #6" 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 #7 (since September 10th, 2026)</h2>
<p>28 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/flow-php/arrow-ext">flow-php/arrow-ext</a> (0.44.1): Apache Arrow PHP extension powered by Rust</li>
<li><a href="https://packagist.org/packages/phpolygon/php-vio">phpolygon/php-vio</a> (2.25.4): PHP extension for GPU rendering (OpenGL, Vulkan, Metal), audio, video recording, streaming, and input</li>
<li><a href="https://packagist.org/packages/flow-php/flow-php-ext">flow-php/flow-php-ext</a> (0.44.1): Flow PHP native extension (Rust) – Floe frame-body encoder/decoder for DataFrame Rows</li>
<li><a href="https://packagist.org/packages/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a> (v1.20.0): Pre-compiled tree-sitter grammars for 371 programming languages</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v2.0.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.2.3): 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.0): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/grpc/grpc-php-ext">grpc/grpc-php-ext</a> (v1.84.0): gRPC PHP Extension</li>
<li><a href="https://packagist.org/packages/extport/grpc">extport/grpc</a> (1.84.0): Unofficial PIE-compatible mirror of grpc/grpc</li>
<li><a href="https://packagist.org/packages/open-telemetry/ext-opentelemetry">open-telemetry/ext-opentelemetry</a> (1.4.1): Auto-instrumentation extension for OpenTelemetry</li>
<li><a href="https://packagist.org/packages/satwareag/pdo-fbird">satwareag/pdo-fbird</a> (v13.2.8): PDO driver for Firebird SQL (pdo_fbird) — separate from the main php-firebird extension</li>
<li><a href="https://packagist.org/packages/satwareag/php-firebird">satwareag/php-firebird</a> (v13.2.8): PHP Firebird database extension — native fbird_* API for PHP 8.2+</li>
<li><a href="https://packagist.org/packages/lucasacoutinho/ext-clickhouse-pdo">lucasacoutinho/ext-clickhouse-pdo</a> (v1.5.0): PDO driver for ClickHouse using the native TCP protocol.</li>
<li><a href="https://packagist.org/packages/lucasacoutinho/ext-clickhouse">lucasacoutinho/ext-clickhouse</a> (v1.5.0): Native TCP ClickHouse client extension for PHP.</li>
<li><a href="https://packagist.org/packages/flow-php/pg-query-ext">flow-php/pg-query-ext</a> (0.44.1): PostgreSQL query parser PHP extension using libpg_query</li>
<li><a href="https://packagist.org/packages/laruence/yac">laruence/yac</a> (2.4.2): Yac is a shared and lockless memory user data cache for PHP.</li>
<li><a href="https://packagist.org/packages/xberg-io/crawlberg">xberg-io/crawlberg</a> (v1.7.1): High-performance web crawling engine</li>
<li><a href="https://packagist.org/packages/goopil/rabbit-rs-native">goopil/rabbit-rs-native</a> (v0.3.7): High-performance RabbitMQ transport for PHP and Laravel, powered by Rust</li>
<li><a href="https://packagist.org/packages/jbboehr/php-stemmer">jbboehr/php-stemmer</a> (v2.0.2): PHP bindings for the Snowball stemming library</li>
<li><a href="https://packagist.org/packages/cypherbits/php-blake3">cypherbits/php-blake3</a> (v2026.9.13.1): BLAKE3 hashing algorithm PHP extension (native C, SIMD) for fast cryptographic hashing.</li>
<li><a href="https://packagist.org/packages/maxmind-db/reader-ext">maxmind-db/reader-ext</a> (v1.14.0): C extension for MaxMind DB Reader – provides significantly faster lookups</li>
<li><a href="https://packagist.org/packages/pecl/timezonedb">pecl/timezonedb</a> (2026.4): 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.3): PHP bindings for Biscuit authorization tokens</li>
<li><a href="https://packagist.org/packages/jbboehr/php-yumemi">jbboehr/php-yumemi</a> (v0.1.1): Native extension that adds operators and unit-expression parsing to yumemi.php.</li>
<li><a href="https://packagist.org/packages/jbboehr/perfidious">jbboehr/perfidious</a> (v0.3.1): PHP extension providing access to Linux, Windows, and macOS performance counters</li>
<li><a href="https://packagist.org/packages/jbboehr/php-mustache">jbboehr/php-mustache</a> (v0.10.1): Mustache templating language extension for PHP</li>
<li><a href="https://packagist.org/packages/kumwe/kumwe-engine">kumwe/kumwe-engine</a> (v1.0.4): Bounded Zend binding to the exact embedded Kumwe Engine release of the same version.</li>
<li><a href="https://packagist.org/packages/php-gtk4/php-gtk4">php-gtk4/php-gtk4</a> (v0.4.0-rc.11): GTK 4 bindings for PHP: desktop applications written in PHP against the native GTK 4 API. Installed with PIE (pie install php-gtk4/php-gtk4); the IDE/PHPStan stubs are the separate php-gtk4/stubs package.</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-7-since-september-10th-2026/">Recently updated PIE extensions #7 (since September 10th, 2026)</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
What do you mean, redeclare a property static? - Exakat
https://www.exakat.io/?p=16437
2026-09-16T13:52:43.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16438" src="https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie-300x300.jpg" alt="What do you mean, redeclare a property static?" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>What do you mean, redeclare a property static?</h1>
<p>It started with a single line in a linting run:</p>
<div>
<p><code class="language-none">Fatal error: Cannot redeclare non static DOMElement::$id as static ezcDocumentPropertyContainerDomElement::$id</code></p>
</div>
<p>So, a piece of code used to have a property, and now, one of the classes is now trying to redeclare this as a static. Obviously, this is never going to work, in PHP 8.6, but also, in previous versions. The class comes from the eZ Components, now Zeta Components, Document library. This is not new code, and it has not been updated for a long time. Though, it extends <code>DOMElement</code> and keeps a counter used to hand out unique numbers to nodes:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class ezcDocumentPropertyContainerDomElement extends DOMElement
{
protected static $properties = array();
/**
* Autoincrement unique ID for DOMElement nodes in XML documents.
*/
protected static $id = 1;
// ...
}
?>
</pre>
</div>
<p>There is nothing exotic here. A static counter, named <code>$id</code>, in a class that had been stable for years. It feels like the error message is a bit misleading, or out of context. So what happened?</p>
<h2 id="toc_1">First suspect: PHP 8.6</h2>
<p>The error showed up while testing the code against PHP 8.6, so the natural first guess was a fresh change in the engine. It did not last long. Running the same code on older versions showed that the error was already there in earlier releases, and bisecting quickly narrowed it down to the 8.2 to 8.3 transition: PHP 8.2 runs the class without complaint, PHP 8.3 refuses to compile it.</p>
<h2 id="toc_2">Second suspect: a new error message</h2>
<p>Maybe PHP 8.3 introduced a new check? Not at all. The message “Cannot redeclare %s%s::$%s as %s%s::$%s” lives in <code>Zend/zend_inheritance.c</code> in PHP 7.0, and before that in <code>Zend/zend_compile.c</code> in PHP 5.6. It is an old, well established rule: a child class cannot turn an instance property of its parent into a static one, or the other way round. Userland code has always been subject to it:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class P { public $id; }
class C extends P { protected static $id = 1; }
// Fatal error: Cannot redeclare non static P::$id as static C::$id
?>
</pre>
</div>
<p>So the rule is not new. Something else changed.</p>
<h2 id="toc_3">Third suspect: native classes are checked differently</h2>
<p>Since the parent is <code>DOMElement</code>, a native class, the next idea was that internal classes might escape the compile time check, and only complain later, or under certain conditions. That theory falls apart with a quick test on PHP 8.2, using a property that <code>DOMElement</code> has always had:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class C extends DOMElement {
function __construct() {}
public static $tagName = 42;
}
// PHP 8.2: Fatal error: Cannot redeclare non static DOMElement::$tagName as static C::$tagName
?>
</pre>
</div>
<p>The same happens with <code>DOMNode::$nodeName</code>, <code>DOMDocument::$encoding</code>, <code>XMLReader::$name</code>, <code>ZipArchive::$status</code>, <code>Exception::$message</code>, <code>Error::$code</code> or <code>PDOStatement::$queryString</code>. Native classes get exactly the same treatment as custom ones. There is no special leniency.</p>
<h2 id="toc_4">The actual culprit: a brand new property</h2>
<p>The explanation is much simpler. In PHP 8.2, <code>DOMElement</code> has no <code>$id</code> property at all. Reflection lists <code>$tagName</code>, <code>$schemaTypeInfo</code>, the element traversal properties and everything inherited from <code>DOMNode</code>, but no <code>$id</code>. On an element with an <code>id</code> attribute, <code>property_exists($element, 'id')</code> returns <code>false</code>. PHP 8.3 added two properties to <code>DOMElement</code>, mirroring the DOM standard:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class DOMElement extends DOMNode implements DOMParentNode, DOMChildNode
{
/** @readonly */
public string $tagName;
public string $className;
public string $id;
// ...
}
?>
</pre>
</div>
<p>The UPGRADING file lists this under “New Features”, not under “Backward Incompatible Changes”. From the point of view of <code>DOMElement</code>, it is indeed a feature. From the point of view of every class that extends <code>DOMElement</code> and already had its own <code>$id</code>, it is a breaking change. In PHP 8.2, the static <code>$id</code> of the ezc class did not redeclare anything: there was nothing to redeclare. In PHP 8.3, it suddenly collides with a public, non static, typed property of its parent.</p>
<h2 id="toc_5">Why adding a property is not free</h2>
<p>Adding a class, a function or a constant to PHP is usually uneventful. Adding a method to a non final class is already riskier, as child classes may have a method with the same name and an incompatible signature. Properties come with their own set of inheritance rules, and all of them now apply to any child that happens to use the same name. When the parent property is public or protected, the child’s declaration must:</p>
<ul>
<li>keep the same static or non static nature,</li>
<li>keep the same or a wider visibility,</li>
<li>keep the same type, when the parent property is typed.</li>
</ul>
<p>So, beyond the static case, PHP 8.3 also rejects these, which were perfectly valid in PHP 8.2:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class C extends DOMElement { protected $id; }
// Fatal error: Access level to C::$id must be public (as in class DOMElement)
class C extends DOMElement { public $id; }
// Fatal error: Type of C::$id must be string (as in class DOMElement)
?>
</pre>
</div>
<p>Each child class that used <code>$id</code> for its own purpose has to match a contract it never signed.</p>
<h2 id="toc_6">When would it have been safe?</h2>
<p> </p>
<h3 id="toc_7">A private property</h3>
<p>Private properties do not take part in inheritance checks. Had the new property been private, the child class would simply have its own, unrelated <code>$id</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class P {
private $id = 'parent';
function get() { return $this->id; }
}
class C extends P {
protected static $id = 1;
static function s() { return static::$id; }
}
var_dump((new C)->get(), C::s());
// string(6) "parent"
// int(1)
?>
</pre>
</div>
<p>Both properties live side by side, and each class sees its own. Of course, a private property is useless for a public API such as <code>$element->id</code>, so this was not an option for the DOM extension. It is, however, a good option for library authors: private properties can be added in a minor version without risking this kind of collision in child classes.</p>
<h3 id="toc_8">What about <code>final</code>?</h3>
<p>It is tempting to think that <code>final</code> would help, but it goes the other way. Since PHP 8.4, properties may be marked <code>final</code>, and that forbids any redeclaration, compatible or not:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class P { final public $id; }
class C extends P { public $id; }
// PHP 8.4: Fatal error: Cannot override final property P::$id
?>
</pre>
</div>
<p>A final property turns every same-named property in a child class into an error. <code>final</code> cannot be combined with <code>private</code> either (“Property cannot be both final and private”). The only <code>final</code> that removes the problem is a final class: without children, there are no collisions. <code>DOMElement</code> cannot be final, though, since extending it is the whole point of <code>DOMDocument::registerNodeClass()</code>, and that is exactly what the ezc library does.</p>
<h2 id="toc_9">The popularity factor</h2>
<p>The last ingredient is the name itself. <code>$id</code> is probably one of the most common property names in PHP code: entities, models, nodes, counters, registries all have one. <code>$className</code> is not far behind. A new property called <code>$isConnected</code>, added to <code>DOMNode</code> in the same release, is much less likely to hit anyone. The risk of adding a property to a non final, widely extended class is roughly proportional to how common its name is. <code>DOMElement::$id</code> scores high on both counts. For the record, here are the 20 most commonly used property names in PHP projects:</p>
<ol>
<li style="list-style-type: none;"></li>
</ol>
<ul>
<li>$name</li>
<li>$collection_key</li>
<li>$id</li>
<li>$RequestId</li>
<li>$type</li>
<li>$response</li>
<li>$description</li>
<li>$value</li>
<li>$container</li>
<li>$config</li>
<li>$options</li>
<li>$initialized</li>
<li>$logger</li>
<li>$data</li>
<li>$table</li>
<li>$message</li>
<li>$getters</li>
<li>$setters</li>
<li>$attributeMap</li>
<li>$connection</li>
</ul>
<h2 id="toc_10">Fixing and detecting</h2>
<p>The fix is a rename. The Zeta Components Document library now uses <code>$_id</code> for its counter, which no longer collides with anything. To find such collisions ahead of time, here is a small script that lists properties declared in your classes that share a name with a non private property of an internal ancestor. Load your code first (autoloader, class map…), then run it on the PHP version you are migrating to:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
foreach (get_declared_classes() as $class) {
$r = new ReflectionClass($class);
if ($r->isInternal()) { continue; }
for ($p = $r->getParentClass(); $p; $p = $p->getParentClass()) {
if (!$p->isInternal()) { continue; }
foreach ($r->getProperties() as $prop) {
if ($prop->getDeclaringClass()->getName() !== $class) { continue; }
$name = $prop->getName();
if ($p->hasProperty($name) && !$p->getProperty($name)->isPrivate()) {
printf("%s::$%s collides with %s::$%s\n",
$class, $name, $p->getName(), $name);
}
}
}
}
?>
</pre>
</div>
<p>Incompatible declarations will stop the script with a fatal error as soon as the class is loaded, which is the loud version of the report. Compatible ones, such as a <code>public string $id</code> in a <code>DOMElement</code> child, compile fine on PHP 8.3 but are reported by the script: the name now means something to the parent class, and it is worth checking that both meanings still agree.</p>
<div>
<p> </p>
<pre> MyElement::$id collides with DOMElement::$id</pre>
</div>
<h2 id="toc_11">Conclusion</h2>
<p>Along the way, we ruled out a new PHP version, a new error message and a special treatment for native classes, and reviewed several edge cases of property inheritance: static versus non static, visibility, types, private and final properties. The real cause was a small, well meaning addition to the DOM API.</p>
<p>For the DOM extension, <code>$id</code> was a green field: an empty slot, waiting for a standard property. For the code that extends <code>DOMElement</code>, it was also a green field, when it was written. The migration error could have come from PHP adding the property, or the custome code adding it on its own. The only way out is to rename a property that had done nothing wrong for years. This is yet another case of a painful migration, discovered at compile time, one <code>Fatal error</code> at a time.</p>
<p>The post <a href="https://www.exakat.io/what-do-you-mean-redeclare-a-property-static/">What do you mean, redeclare a property static?</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Tracing a PHP CLI application with filo - Exakat
https://www.exakat.io/?p=16428
2026-09-15T06:05:54.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/piecrust.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16435" src="https://www.exakat.io/wp-content/uploads/2026/09/piecrust.320-300x300.jpg" alt="Tracing a PHP CLI application with filo" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/piecrust.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/piecrust.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/piecrust.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/piecrust.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Tracing a PHP CLI application with filo</h1>
<p><a href="https://github.com/giacomomasseron/filo">filo</a> is a zero-extension PHP call tracer. No PECL module, no Xdebug session: it registers a <code>file://</code> stream wrapper and rewrites your code’s AST on the way in, so every function entry and exit gets timed. Your files on disk are never touched: instrumented copies live in a throwaway cache.</p>
<p>This tutorial builds a small CLI app with a realistic call stack, traces it, and then turns the trace into something you can actually read.</p>
<p>Everything below was run against filo v0.2.0 / <code>dev-main</code> on PHP 8.3.6. All numbers and output are real.</p>
<h2 id="toc_1">1. The demo app</h2>
<p><code>orderbot</code> prints an order report. This is nothing exotic: it has a command, a service, two repositories, a pricing helper, a renderer:</p>
<div>
<pre>src/
├── Cli/
│ ├── Application.php run() → dispatch + poor man's container
│ ├── ReportCommand.php execute()
│ └── TableRenderer.php render()
├── Service/
│ ├── OrderService.php report() ← the interesting one
│ ├── PricingService.php gross()
│ └── TaxCalculator.php rateFor()
└── Repository/
├── OrderRepository.php all()
├── CustomerRepository.php find()
└── Database.php usleep() stands in for query latency</pre>
</div>
<p>The call chain is five levels deep: <code>Application::run</code> → <code>ReportCommand::execute</code> → <code>OrderService::report</code> → <code>OrderRepository::all</code> → <code>Database::selectOrders</code>. Deep enough that a stack trace tells you nothing useful about where time goes.</p>
<p>You can download the <strong><a href="https://www.exakat.io/download/filo-cli-tutorial.zip">filo tutorial source code</a></strong> for this article here. It is a zip archive, with PHP scripts.</p>
<p>Here is the method the whole tutorial revolves around:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
public function report(): array
{
$rows = [];
foreach ($this->orders->all() as $order) {
$customer = $this->customers->find($order['customer_id']);
$rows[] = [
'order' => $order['id'],
'customer' => $customer['name'],
'lines' => $order['lines'],
'gross' => $this->pricing->gross($order),
];
}
return $rows;
}
?>
</pre>
</div>
<p>At a first glance, it looks fine. And that is the point that Filo tries to fix.</p>
<h2 id="toc_2">2. Install</h2>
<div>
<pre>composer require --dev giacomomasseron/filo</pre>
</div>
<p>filo registers <code>bootstrap.php</code> through Composer’s <code>autoload.files</code>, so it executes the instant <code>vendor/autoload.php</code> is loaded — and does nothing at all unless explicitly enabled.</p>
<h2 id="toc_3">3. The one rule that matters for CLI</h2>
<p>The filo feature is not loaded, and neither is anything loaded before <code>vendor/autoload.php</code>.</p>
<p>The stream wrapper can only intercept files that are included after it has been registered. So the entry point must load the autoloader first and everything else second:</p>
<div>
<pre class="brush: php; title: ; notranslate">
#!/usr/bin/env php
<?php
declare(strict_types=1);
// 1. Autoloader first. filo's bootstrap.php runs here and registers the
// stream wrapper — BEFORE any App class has been loaded.
require __DIR__ . '/../vendor/autoload.php';
// 2. Every App class loaded from here on passes through the wrapper
// and comes back instrumented.
exit((new App\Cli\Application())->run($argv));
?>
</pre>
</div>
<p>If you define classes directly inside <code>bin/orderbot</code>, or <code>require</code> them above the autoloader, they simply won’t appear in the trace. For full coverage including the entry script, point <code>auto_prepend_file</code> at <code>vendor/giacomomasseron/filo/bootstrap.php</code>.</p>
<p>Second rule, smaller: <code>vendor</code> is excluded by default with <code>FILO_EXCLUDE</code>, so you trace your code, not Composer’s.</p>
<h2 id="toc_4">4. Turning it on</h2>
<p>Two switches. For a one-off CLI run, the env var is the natural one:</p>
<div><code class="language-bash">FILO_ENABLED=1 php -d opcache.enable_cli=0 bin/orderbot report</code></div>
<p>For an editor-driven workflow, create an empty <code>.filo-on</code> file in the project root instead. It’s checked per run, so toggling is instant. In a Laravel app, a <code>.env</code> entry does not work, as phpdotenv loads after the tracer bootstraps.</p>
<p>The <code>-d opcache.enable_cli=0</code> is critical: opcache CLI is off by default, but if you’ve turned it on, cached opcodes bypass the wrapper entirely and you’ll get an empty trace.</p>
<p>Run it. The output is byte-identical to the untraced run. Instrumentation doesn’t change behaviour:</p>
<div>
<pre>ORDER CUSTOMER LINES GROSS
---------------------------------------------
1001 Aurora Tools BV 3 301.29
1002 Aurora Tools BV 1 48.28
1003 Nordwind GmbH 5 928.80
1004 Nordwind GmbH 2 142.80
1005 Papeterie Lyon 4 492.30
1006 Papeterie Lyon 1 18.00</pre>
</div>
<p>But now there’s a trace next to it:</p>
<div><code class="language-none">.filo/traces/20260914-090557-df3af8ef.json</code></div>
<p>Add <code>.filo/</code> to <code>.gitignore</code>.</p>
<h2 id="toc_5">5. What’s in the trace</h2>
<p>Format v1 is deliberately small. It is a flat event list, not a nested tree:</p>
<div>
<pre class="brush: jscript; title: ; notranslate">
{
"version": 1,
"duration": 41780000,
"context": { "sapi": "cli", "argv": ["bin/orderbot", "report"] },
"events": [
{ "i": 0, "p": -1, "fn": "App\\Cli\\Application::run",
"file": "/home/you/orderbot/src/Cli/Application.php", "line": 15,
"s": 149289, "e": 41653573, "m": 3078600 }
]
}
</pre>
</div>
<ul>
<li><code>i</code>: event id, <code>p</code> — parent event id (<code>-1</code> = root)</li>
<li><code>s</code> / <code>e</code>: start and end offsets in <strong>nanoseconds</strong> from process start</li>
<li><code>m</code>: memory usage at entry, in bytes</li>
<li><code>fn</code>: the <code>__METHOD__</code> form: <code>App\Repo::find</code>, <code>my_function</code>, <code>{closure}</code></li>
</ul>
<p>Worth noting: with CLI, the <code>context</code> object carries <code>sapi</code> and <code>argv</code>, not the <code>method</code>/<code>uri</code> pair the README shows for HTTP requests. If you write your own tooling, you might want to handle both.</p>
<p>The one derived number you actually care about isn’t in the file:</p>
<blockquote><p>self time = <code>(e - s)</code> − Σ (direct children’s <code>e - s</code>)</p></blockquote>
<p>Inclusive time tells you a call was slow. Self time tells you it was slow rather than something it called.</p>
<h2 id="toc_6">6. Displaying it, option A: the built-in viewer</h2>
<div>
<pre>vendor/bin/filo serve # http://127.0.0.1:8090</pre>
</div>
<p>This is a single-file, zero-dependency viewer on PHP’s built-in server: trace list, zoomable flamegraph, call tree with self-times, top-functions table, and a live panel for paused requests. CLI traces land in the same list as web ones. It is verified against the run above:</p>
<div>
<pre> curl -s http://127.0.0.1:8090/api/traces | head -c 200
[{"version":1,"ts":"2026-09-14T09:03:00+00:00","duration":41666870,
"context":{"sapi":"cli","argv":["bin/orderbot","report"]}, ...</pre>
</div>
<p>It’s localhost-only by design, as it is supposed to be used by developers or local AI. And, for security reasons, traces contain file paths and variable values, so never expose that port.</p>
<p>Three JSON endpoints back it: <code>/api/traces</code>, <code>/api/breaks</code>, <code>/api/breakpoints</code>. If you want a nicer UI, drop static files into <code>server/ui/</code> (entry point <code>index.html</code>) and they’re served instead of the built-in page, no code changes needed.</p>
<h2 id="toc_7">7. Displaying it, option B: in the terminal</h2>
<p>For a CLI app, staying in the terminal is often the faster loop, and it makes the trace format concrete. <code>tools/trace-report.php</code>, which is included alongside this tutorial, reads the newest trace and prints a call tree plus a hotspot table. It’s ~150 lines and the only real logic is the self-time subtraction from §5.</p>
<div>
<pre>php tools/trace-report.php # newest trace
php tools/trace-report.php path/to.json # a specific one</pre>
</div>
<p>Here is a real output from the run above, abridged in the middle. Of course, your mileage may vary, but it should stay very similar:</p>
<div>
<pre>filo bin/orderbot report
41.78 ms · 31 calls · sapi cli · 20260914-090557-df3af8ef.json
CALL TREE (inclusive | self)
App\Cli\Application::run 41.58 ms | 0.01 ms self
├─ App\Cli\Application::makeReportCommand 0.36 ms | 0.36 ms self
└─ App\Cli\ReportCommand::execute 41.21 ms | 0.01 ms self
├─ App\Service\OrderService::report 41.17 ms | 0.04 ms self
│ ├─ App\Repository\OrderRepository::all 4.17 ms | 0.07 ms self
│ │ └─ App\Repository\Database::selectOrders 4.10 ms | 4.10 ms self
│ ├─ App\Repository\CustomerRepository::find 6.11 ms | 0.01 ms self
│ │ └─ App\Repository\Database::selectCustomer 6.11 ms | 6.11 ms self
│ ├─ App\Service\PricingService::gross 0.00 ms | 0.00 ms self
│ │ └─ App\Service\TaxCalculator::rateFor 0.00 ms | 0.00 ms self
│ ├─ App\Repository\CustomerRepository::find 6.09 ms | 0.00 ms self
│ │ └─ App\Repository\Database::selectCustomer 6.09 ms | 6.09 ms self
│ ⋮ (four more identical pairs)
└─ App\Cli\TableRenderer::render 0.03 ms | 0.03 ms self
TOP FUNCTIONS BY SELF TIME
FUNCTION CALLS SELF ms SHARE
---------------------------------------------------------------------------
App\Repository\Database::selectCustomer 6 36.90 88.3% ← N+1?
App\Repository\Database::selectOrders 1 4.10 9.8%
App\Cli\Application::makeReportCommand 1 0.36 0.9%
App\Repository\OrderRepository::all 1 0.07 0.2%
App\Service\OrderService::report 1 0.04 0.1%
App\Cli\TableRenderer::render 1 0.03 0.1%
App\Service\PricingService::gross 6 0.03 0.1%
App\Repository\CustomerRepository::find 6 0.03 0.1%</pre>
</div>
<p>Two design choices make this readable, and they’re worth stealing for any trace UI:</p>
<p><strong>Aggregate by self time, not inclusive time</strong>. Sorted by inclusive time, the top of the table would be <code>Application::run</code> at 41.58 ms: technically true, completely useless. Sorted by self time, the actual culprit is line one.</p>
<p><strong>Show the call count next to it</strong>. <code>CustomerRepository::find</code> costs 0.01 ms per call; nobody would ever optimise it. What matters is the pair <code>high call count × high total self time</code>, which is the signature of an N+1. The script flags it automatically (≥5 calls and ≥10% of runtime), and the repetition in the call tree above is the same finding in visual form.</p>
<p>Note also how the tree exonerates the innocent: <code>OrderService::report</code> has 41.17 ms inclusive but 0.04 ms self. It isn’t slow. It’s calling something slow, six times.</p>
<h2 id="toc_8">8. Acting on it</h2>
<p>The trace says: 6 customer lookups for 6 orders, but only 3 distinct customers. Batch them.</p>
<div>
<pre class="brush: diff; title: ; notranslate">- $rows = [];
-
- foreach ($this->orders->all() as $order) {
- $customer = $this->customers->find($order['customer_id']);
+ $rows = [];
+ $orders = $this->orders->all();
+
+ // Resolve every customer up front, in one pass.
+ $customers = $this->customers->findMany(array_column($orders, 'customer_id'));
+
+ foreach ($orders as $order) {
+ $customer = $customers[$order['customer_id']];</pre>
</div>
<p>with <code>findMany()</code> de-duplicating ids before hitting storage. Re-run, re-measure:</p>
<table>
<thead>
<tr>
<th></th>
<th>calls traced</th>
<th><code>selectCustomer</code></th>
<th>total</th>
</tr>
</thead>
<tbody>
<tr>
<td>before</td>
<td>31</td>
<td>6 × — 36.90 ms</td>
<td><strong>41.78 ms</strong></td>
</tr>
<tr>
<td>after</td>
<td>23</td>
<td>3 × — 18.44 ms</td>
<td><strong>23.68 ms</strong></td>
</tr>
</tbody>
</table>
<p>43% off the runtime, and the measurement loop was: run the command, run the report script. That’s the useful part of having a tracer wired into a CLI app.</p>
<h2 id="toc_9">9. Bonus: pausing a CLI run</h2>
<p>filo also does function-entry breakpoints, without a daemon, nor a IDE protocol. They work on a CLI process exactly as on a web request:</p>
<div>
<pre>$ vendor/bin/filo break "App\Service\PricingService::gross"
breakpoint added: App\Service\PricingService::gross
# terminal 1 — the process freezes at that function's entry
$ FILO_ENABLED=1 php -d opcache.enable_cli=0 bin/orderbot report
# terminal 2
$ vendor/bin/filo pending
090547-13d7e9 App\Service\PricingService::gross bin/orderbot report
$ vendor/bin/filo show 090547-13d7e9
App\Service\PricingService::gross
at /home/you/orderbot/src/Service/PricingService.php:11
request: bin/orderbot report
vars:
{
"order": {
"id": 1001,
"customer_id": 7,
"lines": 3,
"net": 249,
"country": "NL"
},
"__this": {
"__class": "App\\Service\\PricingService",
"props": { "tax": { "__class": "App\\Service\\TaxCalculator", "props": [] } }
}
}
$ vendor/bin/filo continue 090547-13d7e9
released 090547-13d7e9</pre>
</div>
<p>The command then finishes normally and still writes its trace. There are some noteworthy details:</p>
<ul>
<li><strong>Entry only</strong>. You see arguments and <code>$this</code> as the function begins. No stepping, no eval, as these are deliberately left to Xdebug.</li>
<li><strong>Once per request</strong>. So a breakpoint inside a loop pauses once, not six times.</li>
<li><strong>Pause time is excluded from trace timings</strong>. So inspecting doesn’t corrupt your numbers.</li>
<li><strong>Auto-continue</strong> after <code>FILO_BREAK_TIMEOUT</code> seconds, by default 120, a forgotten breakpoint can’t hang a process forever.</li>
<li>State lives in <code>.filo/breakpoints.json</code> and <code>.filo/traces/breaks/</code>; the CLI and the web UI read and write the same files, so you can mix them freely.</li>
</ul>
<p>One practical note from building this: if a paused process is killed rather than continued, its snapshot lingers in <code>pending</code>. <code>vendor/bin/filo continue --all</code> clears the strays.</p>
<h2 id="toc_10">10. Gotchas worth knowing up front</h2>
<table>
<thead>
<tr>
<th>Gotcha</th>
<th>What happens</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>opcache on</td>
<td>Cached opcodes bypass the wrapper: empty or stale traces. Worse on web: opcache may keep serving instrumented code after you disable tracing, since the file on disk never changed</td>
<td><code>opcache.enable=0</code> while tracing (CLI is off by default)</td>
</tr>
<tr>
<td>Code loaded before the autoloader</td>
<td>Silently missing from the trace</td>
<td>Load <code>vendor/autoload.php</code> first, or use <code>auto_prepend_file</code></td>
</tr>
<tr>
<td>Native functions, <code>eval</code>, arrow functions</td>
<td>They are not instrumented: they show up as self time of their caller</td>
<td>Expected; read a fat self time as “this frame plus its native calls”</td>
</tr>
<tr>
<td>Line numbers in instrumented files</td>
<td>Drift, because of the pretty printer</td>
<td>Trace line numbers are still correct — baked in from the original AST</td>
</tr>
<tr>
<td>Long-running workers (Octane, RoadRunner, FrankenPHP)</td>
<td>Shutdown flush never fires</td>
<td>Call <code>\Filo\Collector::cycle($dir)</code>at your request boundary</td>
</tr>
<tr>
<td>Traces contain values and paths</td>
<td>Leaking them would be bad</td>
<td>Never expose <code>filo serve</code>; keep <code>.filo/</code> gitignored</td>
</tr>
</tbody>
</table>
<h2 id="toc_11">11. Where to take it next</h2>
<p>filo also plugs into test suites, which is the natural home for what section 8 did by hand: it turns the finding into a regression guard:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Pest
expect(fn () => $service->report())->toRunUnder(30); // ms
expect(fn () => $service->report())->toCall('App\Repository\Database::selectCustomer')->atMost(3);
// PHPUnit, via the Filo\Testing\FiloAssertions trait
$this->assertCallCount('App\Repository\Database::selectCustomer', atMost: 3, callable: fn () => $service->report());
?>
</pre>
</div>
<p><code>toCall()</code> on its own asserts nothing — always finish the chain with <code>atMost()</code> / <code>atLeast()</code> / <code>times()</code>. And if the suite runs without filo enabled, call-based assertions throw <code>FiloNotEnabledException</code> rather than silently passing, which is the right default.</p>
<p>Register <code>Filo\Testing\PHPUnit\TraceExtension</code> in <code>phpunit.xml</code> and every failing test drops a trace artifact in <code>.filo/traces/tests/</code> — openable in the same viewer, uploadable from CI as a build artifact.</p>
<h2 id="toc_12">12. Conclusion</h2>
<p>filo is a genuinely interesting tool, and the reason is the idea at its core rather than the feature list.</p>
<p>Call tracing in PHP has always been extension territory. You install Xdebug or XHProf or Excimer, you fight your php.ini, and on shared hosting or a locked-down container you simply don’t get to profile at all. filo sidesteps the whole problem by noticing that PHP lets you take over <code>file://</code> yourself: register a stream wrapper, parse each incoming file with nikic/php-parser, inject entry and exit hooks into the AST, hand the rewritten source back to the engine. The engine never knows. Your files on disk are never touched: the instrumented copies live in a throwaway cache. It is a genuinely creative use of a mundane part of the language, and it’s rare to see a userland trick buy you this much.</p>
<p>What that creates is actual feature. From the run in this tutorial: a full parent-linked call tree with correct self-time accounting, aggregation that surfaced an N+1 three layers down as 88.3% of runtime, a flamegraph viewer, per-test trace artifacts for CI, performance assertions you can commit as regression guards, and, the part I expected to be vapour, working function-entry breakpoints on a live CLI process, complete with arguments and <code>$this</code>, with pause time correctly excluded from the timings so inspecting doesn’t corrupt your numbers. No extension. No daemon. No IDE protocol. Two commands from <code>composer require</code> to a flamegraph.</p>
<p>The rough edges are real, though, and mostly follow from the same design choice. Because instrumentation happens at include time, anything the engine loads another way is invisible: your entry script, anything required above the autoloader, native functions, <code>eval</code>, arrow functions. Because the trick operates on source that opcache has already cached, opcache and filo cannot coexist, and the failure mode on a web SAPI is nasty, since opcache may keep serving instrumented code after you switch tracing off, the file on disk being unchanged. Smaller scrapes show elsewhere: line numbers drift inside instrumented files, a killed process leaves a stale entry in <code>pending</code> until you run <code>continue --all</code>, and the shipped web UI is explicitly a functional placeholder with a documented seam for replacing it. None of these bit me hard, but you do need to know them: see section 3 and section 10.</p>
<p>That’s a fair trade for v0.2.0 on a package with single-digit installs. The architecture is sound, the constraints are honestly documented rather than hidden, and the author has clearly thought about the failure modes: the auto-continue timeout on breakpoints and the <code>FiloNotEnabledException</code> on silently-unenforced assertions are both the kind of detail you only add after being burned. Whether it matures into something you’d reach for over Xdebug depends on questions byeond this tutorial: overhead on a large real codebase, behaviour under frameworks with heavy bootstrapping, and how the viewer develops.</p>
<p>Worth watching, and worth an afternoon on a project where installing an extension isn’t an option. Keep an eye on where filo goes.</p>
<p>The post <a href="https://www.exakat.io/tracing-a-php-cli-application-with-filo/">Tracing a PHP CLI application with filo</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
PHP Property Resolution Order: Class · Trait · Parent · Interface - Exakat
https://www.exakat.io/?p=16423
2026-09-14T09:45:25.000Z
Exakat
<h1><a href="https://www.exakat.io/wp-content/uploads/2026/09/pumpkin.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16424" src="https://www.exakat.io/wp-content/uploads/2026/09/pumpkin.320-300x300.jpg" alt="PHP Property Resolution Order: Class · Trait · Parent · Interface" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/pumpkin.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/pumpkin.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/pumpkin.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/pumpkin.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>PHP Property Resolution Order: Class · Trait · Parent · Interface</h1>
<p>PHP offers the same four places to define a property that it offers for constants: directly in a class, in a trait the class uses, in a parent class it extends, and, as of PHP 8.4, in an interface it implements. Since these different places may all happen at the same time, they are in competition. So, thre will be only one winner in the PHP property resolution order: Class, Trait, Parent, Interface</p>
<p>That last one used to be a flat “no”. For the entire history of the language before 8.4, interfaces could not declare properties at all; they were the one member kind interfaces had nothing to say about. Property hooks changed that, but not by making interfaces work like they do for constants. They introduced a third model, distinct from both.</p>
<p>There’s also a dimension constants never had to deal with: storage. A constant is a value: read it from anywhere in the hierarchy and you get one thing back, copied conceptually into whichever class resolves it. A property is a slot in an object, or a shared cell on a class, and where that slot lives is not always where you’d guess. Two of the surprises below come directly from that difference.</p>
<h2 id="toc_1">The priority stack</h2>
<table>
<thead>
<tr>
<th>Priority</th>
<th>Layer</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>1 highest</strong></td>
<td><strong>Class own property</strong></td>
<td>Always wins. Defined directly in the class body, or as a promoted property.</td>
</tr>
<tr>
<td><strong>2</strong></td>
<td><strong>Trait property</strong></td>
<td>Used when the class has no own definition. Copied in at compile time: value-checked against the class’s own definition, but not against the parent’s.</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td><strong>Parent class property</strong></td>
<td>Inherited when nothing more local claims the name. A “inherited” doesn’t always mean “copied”. See the static-property section below.</td>
</tr>
<tr>
<td><strong>4 new in 8.4</strong></td>
<td><strong>Interface hooked property only</strong></td>
<td>A plain property on an interface is a compile error. A hooked property is a contract, exactly like an interface method: the interface says the property must exist and be readable or writable or both; the implementing class supplies the actual logic.</td>
</tr>
</tbody>
</table>
<p>Tier 4 isn’t really competing with tiers 1–3 the way an interface constant competes with a parent constant. A hooked property in an interface has no storage and no default value of its own. It’s a shape, not a value. So there’s no version of the constants article’s “ambiguous, parent and interface both supply a value” fatal error here. Properties sidestep that entire category of conflict, at the cost of interfaces being locked out of the storage-based kind of property for good.</p>
<h2 id="toc_2">A concrete example</h2>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
trait HasGreeting {
public string $greeting = 'Hello from trait';
}
class BaseGreeter {
public string $greeting = 'Hello from parent class';
}
class App extends BaseGreeter {
use HasGreeting;
public string $greeting = 'Hello from App itself';
}
echo (new App)->greeting;
// "Hello from App itself"
?>
</pre>
</div>
<p>This code actually fails, as the compatibility between the class and the trait definition MUST be complete: down to the default value. If the definition is not the same, it yields a “<a href="https://php-errors.readthedocs.io/en/latest/messages/ps-and-ps-define-the-same-property-q$psr-in-the-composition-of-ps.-however,-the-definition-differs-and-is-considered-incompatible.-class-was-composed.html">App and HasGreeting define the same property ($greeting) in the composition of %s. However, the definition differs and is considered incompatible. Class was composed</a>” compilation error.</p>
<p>Now, remove <code>App</code>‘s own property and the trait takes over: <code>"Hello from trait"</code>, with no complaint that it disagrees with the parent’s value: class-vs-parent and trait-vs-parent are silent overrides, not a checked ones. Remove the trait too, and <code>App</code> simply inherits <code>BaseGreeter</code>‘s property, no conflict, because nothing else at that level is competing for the name.</p>
<h2 id="toc_3">Interfaces couldn’t join this club, until PHP 8.4</h2>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
interface Greeter {
public $greeting;
}
?>
</pre>
</div>
<div>
<pre><code class="language-none">Fatal error: Interfaces may only include hooked properties
?>
[/php]
</code></pre>
</div>
<p>That’s the whole story, for every PHP version before 8.4: an interface demanding a property was simply not expressible. Property hooks reopen the door, but as a contract for behavior, not storage:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
interface Greeter {
public string $greeting { get; }
}
class App implements Greeter {
public string $greeting { get => 'Hello, hooked'; }
}
echo (new App)->greeting; // "Hello, hooked"
?>
</pre>
</div>
<p>This is structurally closer to how interfaces handle methods than how they handle constants: the interface declares that reading or writing <code>$greeting</code> must be possible, and leaves the actual value, computed, stored, whatever the implementer wants, entirely up to the class. Two interfaces can declare the same hooked property name without any risk of the “ambiguous” fatal error the constants article ran into, because neither interface is supplying a competing value to be ambiguous about.</p>
<h2 id="toc_4">The rules that catch people out</h2>
<h3 id="toc_5">Trait beats parent, no questions asked</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
trait T { public $foo = 'trait-val'; }
class ParentC { public $foo = 'parent-val'; }
class ChildC extends ParentC { use T; }
echo (new ChildC)->foo; // "trait-val" — no error, despite disagreeing with the parent
?>
</pre>
</div>
<p>Same rule as constants: a trait property is compiled directly into the class body, so it’s treated as the class’s own definition the moment there’s nothing more local to contest it.</p>
<h3 id="toc_6">Class beats trait — but only if they agree, or the class overrides</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
trait T { public $foo = 'trait-val'; }
class C {
use T;
public $foo = 'class-val'; // different default
}
?>
</pre>
</div>
<div>
<pre><code class="language-none">Fatal error: C and T define the same property ($foo) in the
composition of C. However, the definition differs and is
considered incompatible.
?>
[/php]
</code></pre>
</div>
<p>Give the class the same default the trait has and it compiles fine. This value-sensitivity is specific to the class-vs-its-own-traits relationship: it doesn’t apply to trait-vs-parent, which never checks at all.</p>
<h3 id="toc_7">Two traits, same property: fatal only if the defaults differ</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
trait T1 { public $foo = 'one'; }
trait T2 { public $foo = 'two'; }
class C { use T1, T2; } // Fatal: differing definition, considered incompatible
?>
</pre>
</div>
<p>Identical defaults compile without complaint. As with constants, there’s no <code>insteadof</code>/<code>as</code> for properties: that syntax only resolves method conflicts. The only fix is declaring the property explicitly on the class.</p>
<h3 id="toc_8">Shadowing a private property doesn’t override it — it duplicates it</h3>
<p>This is the one with no equivalent anywhere in the constants article, because constants aren’t storage and can’t be duplicated. Properties can:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class ParentF {
private $x = 'parent-x';
public function dumpParent() { return $this->x; }
}
class ChildF extends ParentF {
private $x = 'child-x';
public function dumpChild() { return $this->x; }
}
$obj = new ChildF();
var_dump($obj);
?>
</pre>
</div>
<div>
<pre><code class="language-none">object(ChildF)#1 (2) {
["x":"ParentF":private]=>
string(8) "parent-x"
["x":"ChildF":private]=>
string(7) "child-x"
}
?>
[/php]
</code></pre>
</div>
<p>Both <code>$x</code> properties are alive in the same object, in two separate storage slots, each visible only from methods declared in its own class. <code>dumpParent()</code> reads <code>ParentF</code>‘s slot, <code>dumpChild()</code> reads <code>ChildF</code>‘s slot — from the outside this looks like one property, but it’s genuinely two. This only happens with <code>private</code>; <code>protected</code> and <code>public</code> properties with the same name really do get overridden, one slot, like you’d expect.</p>
<h3 id="toc_9">Static properties: shared storage unless you redeclare</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class ParentG { public static $count = 0; }
class ChildG extends ParentG {} // no redeclaration
ParentG::$count = 5;
echo ChildG::$count; // 5 — same cell
ChildG::$count = 99;
echo ParentG::$count; // 99 — writing through the child mutated the parent
?>
</pre>
</div>
<p>Skip redeclaring a static property in the child and it isn’t “inherited” in the copy sense at all: it’s the literal same storage cell, and writes through either class name are visible through the other. Redeclare it, even with the same value, and that link is severed:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class ParentG2 { public static $count = 0; }
class ChildG2 extends ParentG2 { public static $count = 0; } // separate storage now
?>
</pre>
</div>
<p>Nothing in the constants model prepares you for this, because constants have no concept of a write happening after declaration: this is a live, mutable, shareable cell, not a value baked in at compile time.</p>
<h3 id="toc_10"><code>readonly</code> blocks a subclass from re-touching an already-set property</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class ParentI {
public readonly string $foo;
public function __construct() { $this->foo = 'parent-init'; }
}
class ChildI extends ParentI {
public function __construct() {
parent::__construct();
$this->foo = 'child-attempt'; // too late — already initialized
}
}
?>
</pre>
</div>
<div>
<pre><code class="language-none">Error: Cannot modify readonly property ParentI::$foo
?>
[/php]
</code></pre>
</div>
<p>The lock applies per-property, not per-class: once <code>parent::__construct()</code> has set it, no code anywhere, including a subclass constructor, gets a second write.</p>
<h3 id="toc_11">Uninitialized typed properties throw exceptions, they don’t default to null</h3>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class C { public int $x; }
echo (new C)->x;
?>
</pre>
</div>
<div>
<pre><code class="language-none">Error: Typed property C::$x must not be accessed before initialization
?>
[/php]
</code></pre>
</div>
<p>This isn’t strictly a resolution-order rule, but it’s a failure mode constants simply cannot have, so it is worth mentioning here. A constant always has a value the moment the class is compiled. A typed property without a default can exist, structurally, in a state where reading it is an error.</p>
<h2 id="toc_12">Calling up the chain: <code>self::</code> vs <code>static::</code></h2>
<p>Same late-static-binding split as constants, but with real consequences now that static properties are shared storage rather than fixed values:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class ParentH {
public static $foo = 'parent';
public static function selfVal() { return self::$foo; }
public static function staticVal() { return static::$foo; }
}
class ChildH extends ParentH {
public static $foo = 'child';
}
echo ChildH::selfVal(); // "parent" — self:: is fixed to the declaring class
echo ChildH::staticVal(); // "child" — static:: resolves against the calling class
?>
</pre>
</div>
<p><code>self::$foo</code>, written inside <code>ParentH</code>, always reads <code>ParentH</code>‘s cell, no matter which subclass calls the method. <code>static::$foo</code> reads whichever class’s cell is actually in play. Combined with the storage-sharing rule above, this means <code>self::</code> and <code>static::</code> can end up pointing at two different cells entirely, not just two different values.</p>
<h2 id="toc_13">Side note reminder: enums</h2>
<p>This is the sharpest three-way contrast of the whole series. Enums can declare their own constants freely, and they can <code>implement</code> interfaces just like classes: but they cannot declare properties at all, of any kind:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
enum Suit {
public static $counter = 0; // Fatal error: Enum Suit cannot include properties
case Hearts;
}
?>
</pre>
</div>
<p>Not instance properties, not static ones, not hooked ones: the baillon is total. An enum case is meant to be a fixed, singleton value; giving it mutable storage would undermine that guarantee, so PHP forbids the whole category rather than picking rules for it. Where the constants article found enums following the same rules as classes, properties are where enums diverge completely.</p>
<h2 id="toc_14">The mental model</h2>
<p>For class constants, “closest wins” was nearly the whole story, with one wrinkle: same-distance conflicts, like two interfaces or two traits, need an explicit tiebreaker because there’s no storage to fall back on: only values, and values don’t merge. Properties keep the “closest wins” rule for which definition is visible, but add a second, independent question the constants model never had to ask: is it the same storage, or two different storages wearing the same name?</p>
<p>Public and protected inheritance answer that question the boring, expected way: one property, always redefined identically. Private inheritance answers it the surprising way: two properties, coexisting, as dizigot twins. Static properties answer it based on whether the child bothered to redeclare, one shared cell, or two independent ones. Interfaces, which fully participated in the constants story, sit almost entirely outside the properties story, contributing shape through hooks but never storage. Knowing which value resolves is only half of understanding a property; the other half is knowing which piece of memory it actually lives in.</p>
<p>In the end, it is a zoo with very diverse animals.</p>
<p>The post <a href="https://www.exakat.io/php-property-resolution-order-class-%c2%b7-trait-%c2%b7-parent-%c2%b7-interface/">PHP Property Resolution Order: Class · Trait · Parent · Interface</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
RisingWave, PHP, and the Streaming Database Idea - Exakat
https://www.exakat.io/?p=16415
2026-09-12T20:50:44.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/wave.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16416" src="https://www.exakat.io/wp-content/uploads/2026/09/wave.320-300x300.jpg" alt="RisingWave, PHP, and the Streaming Database Idea " width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/wave.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/wave.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/wave.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/wave.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>RisingWave, PHP, and the Streaming Database Idea</h1>
<p>A regular database answers the question you ask it, once, against whatever state happens to be sitting on disk at that moment. If the data changes a second later, your answer is stale until you ask again. This is why dashboards poll, and why “real-time analytics” has historically meant “batch job that runs every five minutes instead of every night.” Streaming SQL engines, such as Storm, Kafka Streams, ksqlDB, or Flink SQL, grew up to close that gap, but they did it by putting a processing layer in front of a database, not by being one: you still needed somewhere to land the output.</p>
<p>A streaming database collapses that distinction. You write an ordinary <code>CREATE MATERIALIZED VIEW ... AS SELECT ...</code>, and instead of computing it once, the engine keeps it continuously correct: every insert that touches the view’s inputs is incrementally folded into the result, and the query never has to run again from scratch. Materialize popularized the idea, and, tellingly, chose to speak Postgres’s wire protocol rather than invent its own client ecosystem. <a href="https://risingwave.com/">RisingWave</a>, first released in 2022 and written in Rust, took the same premise of incremental view maintenance, not recomputation, and paired it with a Snowflake-style architecture: compute nodes that scale independently of storage, with S3 or a compatible object store as the actual source of truth underneath.</p>
<p>For this piece it’s enough to know it as a database that happens to update its views for you. To make that concrete, imagine a coffee shop, BrewStream, whose orders you want to track as they arrive rather than re-query for. Now, let’s use RisingWave to make this work and deliver delicious coffee.</p>
<h2 id="toc_1">Setting up RisingWave</h2>
<p>RisingWave ships a self-contained “playground” image for exactly this kind of exploration: one process, in-memory storage, gone after 30 minutes of inactivity. We’ll use Docker for that.</p>
<div><code class="language-bash">docker run -it --pull=always -p 4566:4566 -p 5691:5691 risingwavelabs/risingwave:latest playground</code></div>
<p>Port 4566 is the SQL interface: this is the one PHP will talk to. Port 5691 is a dashboard, useful for watching a materialized view’s fragments graph while you tinker, but nothing below depends on it.</p>
<p>Connect with <code>psql</code>, because RisingWave speaks the PostgreSQL wire protocol closely enough that the actual <code>psql</code> binary doesn’t know the difference:</p>
<div><code class="language-bash">psql -h localhost -p 4566 -U root</code></div>
<p>PSA: we are not using password here. This is a playground, not a production environment.</p>
<h2 id="toc_2">A table, same as any other</h2>
<div>
<pre class="brush: sql; title: ; notranslate">
CREATE TABLE coffee_orders (
order_id INT,
coffee_type VARCHAR,
size VARCHAR,
customer_name VARCHAR,
order_time TIMESTAMP
);
</pre>
</div>
<p>Nothing streaming-specific here yet. <code>coffee_orders</code> is a plain table you can <code>INSERT</code> into, exactly as PostgreSQL would let you. The streaming part will be added later, at the moment something else shows up and queries it continuously.</p>
<h2 id="toc_3">The producer: pushing orders from PHP</h2>
<p>There is no <code>risingwave/php-client</code> package published on Packagist, and there doesn’t need to be one. RisingWave’s front door is the Postgres wire protocol, and PHP has shipped a driver for that protocol since <code>pdo_pgsql</code> landed in PHP 5.1L a driver written years before RisingWave existed, which will connect to it without modification. The only requirement is the extension, which is part of the PHP core:</p>
<div><code class="language-bash">php -m | grep pdo_pgsql</code></div>
<p>Now, <code>producer.php</code> simulates BrewStream’s influx of coffee orders, one at a time:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$host = 'localhost';
$port = 4566;
$dbname = 'dev';
$user = 'root';
$password = '';
try {
$conn = new PDO("pgsql:host=$host;port=$port;dbname=$dbname;user=$user;password=$password");
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$coffeeTypes = ['Espresso', 'Latte', 'Cappuccino', 'Americano'];
$sizes = ['Small', 'Medium', 'Large'];
$customers = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'];
for ($i = 1; $i <= 10; $i++) {
$order = [
'order_id' => $i,
'coffee_type' => $coffeeTypes[array_rand($coffeeTypes)],
'size' => $sizes[array_rand($sizes)],
'customer_name' => $customers[array_rand($customers)],
'order_time' => date('Y-m-d H:i:s'),
];
$stmt = $conn->prepare(
'INSERT INTO coffee_orders (order_id, coffee_type, size, customer_name, order_time)
VALUES (:order_id, :coffee_type, :size, :customer_name, :order_time)'
);
$stmt->execute($order);
echo "Order #{$i}: {$order['coffee_type']} ({$order['size']}) for {$order['customer_name']} at {$order['order_time']}\n";
sleep(1);
}
echo "All orders placed!\n";
} catch (PDOException $e) {
die('Error: ' . $e->getMessage());
}
</pre>
</div>
<div><code class="language-bash">php producer.php</code></div>
<p>Ten coffee <code>INSERT</code>s over ten seconds. This is fine for a demo, and it represents a fair stream. A real BrewStream would front this with a CDC source or a Kafka topic rather than a PHP loop holding a PDO connection open; that distinction matters enough, so we’ll come back to it at the end. For now, let’s leave it aside.</p>
<h2 id="toc_4">Reading the stream back, four ways</h2>
<p>The interesting part isn’t that PHP can read from RisingWave: any Postgres client can. It’s that the same stored rows support four genuinely different query shapes without extra plumbing on RisingWave’s side.</p>
<h3 id="toc_5">Everything</h3>
<p>This is basically a SQL database. Not impressive, but perfect to have some classic tools to start with a new technology.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$conn = new PDO('pgsql:host=localhost;port=4566;dbname=dev;user=root;password=');
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $conn->query('SELECT * FROM coffee_orders');
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $order) {
echo "Order #{$order['order_id']}: {$order['coffee_type']} ({$order['size']}) for {$order['customer_name']} at {$order['order_time']}\n";
}
?>
</pre>
</div>
<h3 id="toc_6">Filtered</h3>
<p>This supports also the <code>WHERE</code> clause.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$conn = new PDO('pgsql:host=localhost;port=4566;dbname=dev;user=root;password=');
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$coffeeType = 'Latte';
$stmt = $conn->prepare('SELECT * FROM coffee_orders WHERE coffee_type = :coffee_type');
$stmt->execute(['coffee_type' => $coffeeType]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $order) {
echo "Order #{$order['order_id']}: {$order['size']} for {$order['customer_name']} at {$order['order_time']}\n";
}
?>
</pre>
</div>
<h3 id="toc_7">Aggregated</h3>
<p>And the <code>GROUP BY</code> clause.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$conn = new PDO('pgsql:host=localhost;port=4566;dbname=dev;user=root;password=');
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $conn->query('SELECT coffee_type, COUNT(*) AS order_count FROM coffee_orders GROUP BY coffee_type');
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo "{$row['coffee_type']}: {$row['order_count']} orders\n";
}
?>
</pre>
</div>
<p>Three ordinary SQL queries, indistinguishable from what you’d write against MySQL. The fourth one is where RisingWave stops pretending to be an ordinary database.</p>
<h3 id="toc_8">Continuously aggregated</h3>
<div>
<pre class="brush: sql; title: ; notranslate">
CREATE MATERIALIZED VIEW coffee_orders_by_type AS
SELECT coffee_type, COUNT(*) AS order_count
FROM coffee_orders
GROUP BY coffee_type;
</pre>
</div>
<p>It looks a bit like SQL, but it has some important nuance. That <code>GROUP BY</code> doesn’t get recomputed on read. RisingWave maintains it incrementally: every new row in <code>coffee_orders</code> nudges the count for its <code>coffee_type</code> rather than triggering a full rescan.</p>
<p>The naive way to read that back from PHP is what the previous three scripts already do: <code>SELECT * FROM coffee_orders_by_type</code> in a <code>while (true)</code> loop with a <code>sleep(2)</code>. It works, and it’s also exactly the busy-polling this whole exercise was supposed to get away from: PHP asking “anything new?” on a timer, indistinguishable from hammering a plain table.</p>
<p>RisingWave has a better primitive for this, and PHP has one too: they just need introducing to each other.</p>
<p>On the RisingWave side, a subscription turns a table or materialized view into something you <code>FETCH</code>from rather than <code>SELECT</code> from, and the fetch can block on the server until a row actually shows up:</p>
<div>
<pre class="brush: sql; title: ; notranslate">CREATE SUBSCRIPTION coffee_orders_sub FROM coffee_orders_by_type WITH (retention = '1D');
DECLARE order_cursor SUBSCRIPTION CURSOR FOR coffee_orders_sub SINCE now();
</pre>
</div>
<div>
<pre class="brush: sql; title: ; notranslate">-- returns as soon as a change arrives, or after 5 seconds, whichever is first
FETCH NEXT FROM order_cursor WITH (timeout = '5s');
</pre>
</div>
<p>Each row that comes back carries an <code>op</code> column: <code>Insert</code>, <code>UpdateInsert</code>, <code>UpdateDelete</code>, or <code>Delete</code> — so a consumer sees not just the new count but what kind of change produced it.</p>
<p>That solves RisingWave’s half of the problem: the server no longer has to be asked twice for the same unchanged answer. It doesn’t yet solve PHP’s half: calling that <code>FETCH</code> through an ordinary synchronous query still ties up the PHP process for up to five seconds, unable to do anything else, which is a strange way to treat “waiting for I/O” in a language whose sockets have supported non-blocking waits for decades. This is where <code>pg_socket()</code> earns its keep. It hands you the raw socket resource behind a PostgreSQL connection: it is a resource PDO’s <code>pgsql</code> driver deliberately keeps hidden, which is why this one script switches to the lower-level <code>pgsql</code> extension instead. Once you have that socket, <code>stream_select()</code> can lock on it exactly the way it would block on a file, a pipe, or any other stream, which means “wait for RisingWave’s next change” stops being a special case and becomes one more file descriptor in an ordinary I/O wait:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$conn = pg_connect('host=localhost port=4566 dbname=dev user=root');
pg_query($conn, "CREATE SUBSCRIPTION IF NOT EXISTS coffee_orders_sub FROM coffee_orders_by_type WITH (retention = '1D')");
pg_query($conn, 'DECLARE order_cursor SUBSCRIPTION CURSOR FOR coffee_orders_sub SINCE now()');
$socket = pg_socket($conn);
echo "Watching coffee_orders_by_type... (Ctrl+C to stop)\n";
while (true) {
// Dispatch the fetch without waiting for it — RisingWave will hold this
// open server-side for up to 5s if nothing has changed yet.
pg_send_query($conn, "FETCH NEXT FROM order_cursor WITH (timeout = '5s')");
// Block on the actual socket instead of the PHP process. Nothing here
// says $socket has to be the only thing in $read — a signal pipe or a
// second RisingWave connection could sit in the same stream_select().
do {
$read = [$socket];
$write = $except = null;
stream_select($read, $write, $except, 6);
pg_consume_input($conn);
} while (pg_connection_busy($conn));
$result = pg_get_result($conn);
while ($row = pg_fetch_assoc($result)) {
printf("[%s] %s: %d\n", $row['op'], $row['coffee_type'], $row['order_count']);
}
}
</pre>
</div>
<p>The shape looks similar to the polling version. After all, it is still a <code>while (true)</code>, still one iteration per change. But the wait inside it is now a real wait. <code>stream_select()</code> returns the moment data lands on the wire rather than on a fixed clock tick, and because it’s a wait on a file descriptor rather than a <code>sleep()</code>, it composes: hand it a second socket and this loop watches two RisingWave subscriptions, or ten, without spinning a thread per connection. At ten orders a <code>sleep(2)</code> loop and this one are indistinguishable to the person watching the terminal. The difference only shows up under load, and under load is exactly when you can’t afford a poll interval as your latency floor.</p>
<h2 id="toc_9">Putting it together</h2>
<ol>
<li>Start RisingWave (<code>playground</code> image).</li>
<li>Create <code>coffee_orders</code> and <code>coffee_orders_by_type</code>.</li>
<li>Run <code>producer.php</code> to generate orders.</li>
<li>Run any consumer script against the same tables: plain reads and the continuously maintained aggregate coexist without conflict.</li>
</ol>
<h2 id="toc_10">The bigger picture: fewer moving parts, not just faster ones</h2>
<p>The BrewStream example above is one PHP script <code>INSERT</code>-ing into a table it also owns: a fine way to learn the SQL, but not why anyone puts RisingWave in front of a real system. The actual case starts one layer up: a production Postgres or MySQL database that’s the system of record for orders, and a reporting need that shouldn’t be allowed anywhere near it.</p>
<p>The conventional answer is Change Data Capture: point Debezium at the database’s write-ahead log or binlog, publish every row change to a Kafka topic, run a stream processor to reshape it, land the result somewhere queryable. That’s four systems, namely connector, broker, processor, sink, to keep configured, monitored, and upgraded in lockstep, in service of one goal: let something read the data without touching the primary. RisingWave’s CDC connectors fold all four into one <code>CREATE TABLE ... FROM cdc</code> statement, reading the source database’s log directly and taking a consistent snapshot before it starts streaming, so the pipeline that used to be a small distributed system becomes a line of SQL.</p>
<p>What you get for that is the second half of the trade: heavy, bursty, or badly-written analytical queries, such as the <code>GROUP BY</code> a dashboard fires every ten seconds, the report someone runs at month-end, move onto RisingWave’s copy of the data instead of the OLTP database that also has to process checkout. The primary stops being asked to be two things at once.</p>
<h2 id="toc_11">PostgreSQL protocol everywhere!</h2>
<p>And underneath both of those wins sits the detail that should, honestly, be a little startling: none of this required a new PHP driver. <code>pdo_pgsql</code> for the ordinary reads, the plain <code>pgsql</code> extension for the socket-level subscription — both predate RisingWave by well over a decade, and neither was written with the faintest idea it would one day be handed a <code>stream_select()</code>-driven subscription cursor to talk to. RisingWave’s actual machinery, a distributed dataflow engine maintaining incremental views over object storage, has nothing in common with a single-node B-tree-and-WAL database. It doesn’t matter. It answers on port 5432-shaped wire protocol like Postgres does, so every tool built assuming “the other end is Postgres”: <code>psql</code>, <code>pdo_pgsql</code>, Grafana, dbt, whatever a PHP developer already had installed in 2007: it just works. Materialize made the same choice; so did CockroachDB, so did YugabyteDB. The wire protocol PostgreSQL happened to define has quietly become the thing every ambitious new database engine speaks at its front door, whatever it’s doing in the basement. It is quite impressive to see the evolution from a protocol between a server and its client become an insdustry standard.</p>
<h2 id="toc_12">Anoter tool for your database architecture</h2>
<p>Databases are notoriously the slowest part of an online application. Data streaming is a modern tool to extract data that needs to be feed back in the application fast and in real time. With a materialized view, and some possible processing between ingestion and publication, RisingWave is a great tool to improve your application level of reactivity.</p>
<p>The post <a href="https://www.exakat.io/risingwave-php-and-the-streaming-database-idea/">RisingWave, PHP, and the Streaming Database Idea</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
When PHP Reads Something Else: Visual Traps in Source Code - Exakat
https://www.exakat.io/?p=16411
2026-09-10T20:09:19.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/sunset.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16412" src="https://www.exakat.io/wp-content/uploads/2026/09/sunset.320-300x300.jpg" alt="When PHP Reads Something Else: Visual Traps in Source Code" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/sunset.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/sunset.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/sunset.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/sunset.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>When PHP Reads Something Else: Visual Traps in Source Code</h1>
<p>PHP code is read by two very different audiences: human developers and the PHP parser. Humans read glyphs, the shapes our eyes recognise. The parser reads bytes or characters, raw octets encoded in a file. Most of the time these two views agree perfectly. Sometimes they disagree in ways that are merely confusing. And occasionally they disagree in ways that are actively exploitable.</p>
<p>This post takes a tour through the full spectrum, from the classic letter <code>O</code> versus the digit <code>0</code>, to the genuinely dangerous Cyrillic homoglyphs in class names. Along the way we will look at Unicode comment starters, non-breaking spaces, dollar-sign imposters, and invisible variable names. Each chapter builds on the same underlying idea: <strong>the character you see and the byte PHP reads are not always the same thing</strong>.</p>
<h2 id="toc_1">1. The classic confusables: human eyes under pressure</h2>
<p>Long before Unicode entered the picture, programmers were already fooling themselves with plain ASCII. The most famous offenders are the pairs that look nearly identical in many monospace fonts:</p>
<table>
<thead>
<tr>
<th>Looks like</th>
<th>Actually</th>
<th>Code point</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>O</code> (letter oh)</td>
<td><code>0</code> (digit zero)</td>
<td>U+004F vs U+0030</td>
</tr>
<tr>
<td><code>l</code> (lowercase L)</td>
<td><code>1</code> (digit one)</td>
<td>U+006C vs U+0031</td>
</tr>
<tr>
<td><code>S</code></td>
<td><code>$</code> (dollar)</td>
<td>U+0053 vs U+0024</td>
</tr>
<tr>
<td><code>Q</code></td>
<td><code>0</code> or <code>O</code></td>
<td>U+0051 vs U+0030 / U+004F</td>
</tr>
</tbody>
</table>
<p>The <code>$</code> / <code>S</code> pair is particularly dangerous in PHP because <code>$</code> is syntactically significant. A variable <code>$status</code> misread as <code>Status</code> becomes a bare string constant or, with <code>strict_types</code>, a fatal error. In the other direction, a stray <code>$</code> in a string context can silently interpolate a variable you never intended to reference.</p>
<p>These problems are not purely historical. They still bite developers who:</p>
<ul>
<li>Work in low-contrast dark themes where <code>O</code> and <code>0</code> are hard to tell apart.</li>
<li>Copy-paste from PDFs, which sometimes substitute similar glyphs.</li>
<li>Review code at the end of a long day (yes, cognitive fatigue matters).</li>
<li>Get older and need help with vision (not me, I have had glasses since the age of 7)</li>
</ul>
<p>The traditional defences are the same ones your IDE enforces without you thinking about them: <strong>80-column limits</strong> and <strong>short methods</strong>. A narrow column means fewer characters per line, which reduces the visual search space. A short method means fewer lines on screen at once, which means each identifier is referenced more often and any typo is spotted sooner. These are not arbitrary style rules: they are empirical answers to the limits of human pattern matching.</p>
<h2 id="toc_2">2. Unicode comment starters pack: the <code>#️⃣️</code> trick</h2>
<p>PHP has four comment syntaxes: <code>//</code>, <code>#</code>, <code>/* … */</code>, and <code>/** … */</code>. There is, in fact, a fifth, or rather a family of them, thanks to Unicode.</p>
<p>PHP’s lexer works byte by byte. When it encounters byte <code>0x23</code> outside a string, it opens a line comment and discards everything until the next newline. <code>0x23</code> is the ASCII code for <code>#</code>. The trick is that several Unicode grapheme clusters, that is sequences of code points that a human perceives as one character, begin with the byte <code>0x23</code> followed by combining code points.</p>
<div>
<pre><code class="language-none">#️⃣️ => 23 EF B8 8F E2 83 A3 EF B8 8F (keycap hash emoji)
#⃣ => 23 E2 83 A3 (hash + combining enclosing keycap)
#️ => 23 EF B8 8F (hash + variation selector-16)</code></pre>
</div>
<p>PHP sees the leading <code>0x23</code> and opens a comment. The remaining bytes of the emoji are swallowed as comment content. The result: this is valid PHP that executes without error:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$secret = 'hunter2';
#️⃣️ $secret = 'overwritten'; // this line is a comment!
echo $secret; // prints: hunter2
?>
</pre>
</div>
<p>What works and what does not:</p>
<table>
<thead>
<tr>
<th>Character</th>
<th>First byte</th>
<th>PHP treats as comment?</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#</code></td>
<td>23</td>
<td>✓ always (standard)</td>
</tr>
<tr>
<td><code>#️</code> (# + VS-16)</td>
<td>23</td>
<td>✓ yes</td>
</tr>
<tr>
<td><code>#⃣</code> (# + keycap)</td>
<td>23</td>
<td>✓ yes</td>
</tr>
<tr>
<td><code>#️⃣️</code> (full keycap emoji)</td>
<td>23</td>
<td>✓ yes</td>
</tr>
<tr>
<td><code>#</code> U+FF03 fullwidth</td>
<td>EF</td>
<td>✗ no</td>
</tr>
<tr>
<td><code>﹟</code> U+FE5F small</td>
<td>EF</td>
<td>✗ no</td>
</tr>
</tbody>
</table>
<p>The look-alike Unicode <code>#</code> symbols, fullwidth <code>#</code> and small <code>#</code> do not work because their UTF-8 encoding begins with <code>0xEF</code>, not <code>0x23</code>. The illusion only works for grapheme clusters that literally start with the plain ASCII <code>#</code>.</p>
<h2 id="toc_3">3. Non-breaking spaces: the non-breakable space</h2>
<p>Copy a PHP snippet from a rendered HTML page, a PDF, or a Word document and you may import a character that looks exactly like a space but is not: the non-breaking space, also known as U+00A0, UTF-8 bytes <code>C2 A0</code>.</p>
<p>PHP’s lexer recognises as whitespace only the six ASCII control characters<code>0x20 0x09 0x0A 0x0D 0x0B 0x0C</code>. A non-breaking space is not among them. Its two bytes <code>C2</code>and <code>A0</code> are both in the range <code>0x80–0xFF</code>, which PHP’s identifier rules accept as valid identifier characters. The parser therefore tries to incorporate the non-breaking space into whatever token it is currently building.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// What you pasted (one of those spaces is U+00A0):
$result = myFunction ($arg);
// ^ this space looks normal, is not
// PHP reads:
$result = myFunction\xC2\xA0($arg);
// Parse error: unexpected token "(" because myFunction\xC2\xA0 is an
// identifier, not a function call target followed by an argument list.
?>
</pre>
</div>
<p>The error message is baffling because the file looks perfectly formatted. Most editors render U+00A0 identically to U+0020. The fix is a linter or editor rule that highlights non-ASCII bytes outside strings and comments — or, better, a <code>editorconfig</code> + IDE combination that refuses to save files with unexpected encoding.</p>
<p>Zero-width characters compound this further. U+200B (ZERO WIDTH SPACE), U+200C (ZERO WIDTH NON-JOINER), and U+200D (ZERO WIDTH JOINER) are completely invisible, yet their bytes (<code>E2 80 8B</code>, <code>E2 80 8C</code>, <code>E2 80 8D</code>) are valid PHP identifier bytes. A function named <code>validateInput</code> (with an invisible U+200B between <code>validate</code> and <code>Input</code>) is a different function from <code>validateInput</code>. The call site and the definition can be made to reference different functions while looking completely identical in any editor.</p>
<h2 id="toc_4">4. Dollar-sign look-alikes: the imposters that fail silently</h2>
<p>After comments and spaces, we can start looking at the weirder characters. Unicode contains several characters that look like the PHP variable-starting <code>$</code> <code>U+0024</code>:</p>
<table>
<thead>
<tr>
<th>Character</th>
<th>Code point</th>
<th>UTF-8</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>$</code></td>
<td>U+FF04 fullwidth dollar</td>
<td>EF BC 84</td>
</tr>
<tr>
<td><code>﹩</code></td>
<td>U+FE69 small dollar</td>
<td>EF B9 A9</td>
</tr>
<tr>
<td><code>💲</code></td>
<td>U+1F4B2 heavy dollar sign</td>
<td>F0 9F 92 B2</td>
</tr>
</tbody>
</table>
<p>None of these starts a PHP variable. PHP’s variable rule triggers on byte <code>0x24</code> only. Every UTF-8 multi-byte sequence has a lead byte ≥ <code>0xC0</code>, so no single Unicode character, other than <code>$</code> itself, can trigger variable parsing.</p>
<p>A subtle code-review trap: a line that reads <code>$config['key']</code> looks like it is reading a variable, but PHP would produce a parse error or treat the whole thing as an unexpected token. An attacker who replaces a legitimate <code>$config['key']</code> with <code>$config['key']</code> in a patch could silently neutralise a security check while the diff looks harmless to a distracted reviewer.</p>
<h2 id="toc_5">5. The dollar sign combined with Unicode: invisible variable names</h2>
<p>The flip side of the look-alike problem is the extension problem. Just as <code>#️⃣️</code> combines the ASCII <code>#</code>with emoji-forming code points, you can combine the ASCII <code>$</code> with Unicode combining characters to create grapheme clusters that start a variable but embed the combining bytes into the variable name:</p>
<div>
<pre><code class="language-none">$️⃣ → 24 EF B8 8F E2 83 A3 ($ + VS-16 + combining keycap)</code></pre>
</div>
<p>This is where the analogy with comments breaks down sharply:</p>
<ul>
<li><strong>Comment</strong>: bytes after <code>0x23</code> on that line are inert: they vanish into comment text.</li>
<li><strong>Variable</strong>: bytes after <code>0x24</code> are parsed as the variable name: they survive and matter.</li>
</ul>
<p>Consequence: <code>$️⃣secret</code> and <code>$secret</code> are completely different variables.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?PHP
$️⃣secret = 'injected value';
echo $secret; // prints nothing, $secret was never assigned and it is null
?>
</pre>
</div>
<p>A malicious contributor could introduce a variable assignment that looks identical to a legitimate one but writes to a different slot in the symbol table. The legitimate variable remains unmodified. The injected value is never used. Both facts are invisible without byte-level inspection.</p>
<p>May be malicious is not the right adjective: facetious may be better. But now, we can meet the real serious cases.</p>
<h2 id="toc_6">6. Cyrillic homoglyphs: the real security threat</h2>
<p>Everything above can be classified as confusing, surprising, or exotic. Cyrillic homoglyphs are in a different category: they are a documented attack vector used in real-world supply-chain compromises.</p>
<p>Numerous Cyrillic letters are visually indistinguishable from their Latin counterparts in most fonts:</p>
<table>
<thead>
<tr>
<th>Cyrillic</th>
<th>Code point</th>
<th>Latin look-alike</th>
<th>Code point</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>а</code></td>
<td>U+0430</td>
<td><code>a</code></td>
<td>U+0061</td>
</tr>
<tr>
<td><code>е</code></td>
<td>U+0435</td>
<td><code>e</code></td>
<td>U+0065</td>
</tr>
<tr>
<td><code>о</code></td>
<td>U+043E</td>
<td><code>o</code></td>
<td>U+006F</td>
</tr>
<tr>
<td><code>р</code></td>
<td>U+0440</td>
<td><code>p</code></td>
<td>U+0070</td>
</tr>
<tr>
<td><code>с</code></td>
<td>U+0441</td>
<td><code>c</code></td>
<td>U+0063</td>
</tr>
<tr>
<td><code>х</code></td>
<td>U+0445</td>
<td><code>x</code></td>
<td>U+0078</td>
</tr>
<tr>
<td><code>А</code></td>
<td>U+0410</td>
<td><code>A</code></td>
<td>U+0041</td>
</tr>
<tr>
<td><code>В</code></td>
<td>U+0412</td>
<td><code>B</code></td>
<td>U+0042</td>
</tr>
<tr>
<td><code>С</code></td>
<td>U+0421</td>
<td><code>C</code></td>
<td>U+0043</td>
</tr>
</tbody>
</table>
<p>Because PHP identifiers accept bytes <code>0x80–0xFF</code>, a class defined as <code>Rеquest</code> (where <code>е</code> is Cyrillic U+0435) is syntactically valid and entirely distinct from <code>Request</code> (Latin <code>e</code>). The bytes differ; PHP sees two different class names.</p>
<p>The attack surface is significant:</p>
<p><strong>Shadow class injection</strong>. An attacker adds a file to a project that defines a class whose name is the Cyrillic homoglyph of a framework class. A crafted <code>use</code> statement or a subtly altered autoloader path causes the shadow class to be loaded instead of the real one. The diff shows only what appears to be a trivial typo or whitespace change.</p>
<p><strong>Bypassing <code>instanceof</code> checks</strong>. A guard like <code>if (!$obj instanceof Request)</code> will pass silently when <code>$obj</code> is an instance of Cyrillic <code>Rеquest</code>, because the two class names do not match.</p>
<p><strong>Autoloader confusion</strong>. PSR-4 autoloaders translate class names to file paths. <code>Rеquest</code> with Cyrillic bytes would resolve to a file path that contains those bytes, a file that probably does not exist, or in a hostile scenario, one that was planted.</p>
<p><strong>String comparisons</strong>. Any code that compares class names as strings, <code>get_class($obj) === 'Request'</code>, will silently fail or pass incorrectly depending on which alphabet was used in the definition.</p>
<p>This class of attack is not theoretical. It mirrors the IDN homograph attack with domain names mixing scripts, and the Trojan Source attack, CVE-2021-42574, which uses bidirectional Unicode control characters to reorder how code appears in an editor versus how the parser reads it. Both have real CVEs.</p>
<h2 id="toc_7">7. Defence: what to actually do</h2>
<p>Use <code>token_get_all()</code> as a source-code auditor. PHP’s own tokenizer exposes every token and its byte content. A short script can scan a codebase for non-ASCII bytes appearing outside string literals and comments:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
foreach (token_get_all(file_get_contents($path)) as $token) {
if (!is_array($token)) continue;
if (in_array($token[0], [T_STRING, T_VARIABLE, T_FUNCTION, T_CLASS], true)) {
if (preg_match('/[\x80-\xFF]/', $token[1])) {
printf("Non-ASCII in %s token on line %d: %s\n",
token_name($token[0]), $token[2], bin2hex($token[1]));
}
}
}
?>
</pre>
</div>
<p><strong>Configure your editor</strong> to render non-ASCII bytes visibly: most IDEs have a “show invisible characters” or “highlight non-ASCII” option. Pair this with an <code>.editorconfig</code> rule enforcing ASCII-only identifiers.</p>
<p><strong>Add a CI lint step</strong> that rejects PHP source files containing non-ASCII bytes in identifier positions. Tools like <code>grpc/php-cs-fixer</code>, static analysers, or a custom pre-commit hook can enforce this.</p>
<p><strong>For OSS projects: require signed commits</strong> and enforce Unicode identifier restrictions in code review checklists, particularly for any file that touches authentication, authorisation, or class-loading paths.</p>
<h2 id="toc_8">Summary</h2>
<p>PHP source code sits at the intersection of two audiences: human eyes and a byte-eating parser. And the gap between them is wider than most developers realise.</p>
<table>
<thead>
<tr>
<th>Topic</th>
<th>Human sees</th>
<th>PHP reads</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>S</code> vs <code>$</code></td>
<td>same glyph in bad fonts</td>
<td>different bytes entirely</td>
</tr>
<tr>
<td><code>#️⃣️</code></td>
<td>keycap emoji</td>
<td><code>#</code> comment starter</td>
</tr>
<tr>
<td>Non-breaking space</td>
<td>a space</td>
<td>an identifier byte</td>
</tr>
<tr>
<td><code>$varname</code></td>
<td>a variable</td>
<td>a parse error</td>
</tr>
<tr>
<td><code>$️⃣varname</code></td>
<td>a dollar sign with decoration</td>
<td>a different variable</td>
</tr>
<tr>
<td>Cyrillic <code>Rеquest</code></td>
<td><code>Request</code></td>
<td>a completely separate class</td>
</tr>
</tbody>
</table>
<p>The theme is consistent: <strong>PHP’s lexer is byte-precise, but human perception is not</strong>. The good news is that the same tooling that has always helped with readability, narrow columns, short methods, strict linters, CI enforcement, also helps catch these issues before they become vulnerabilities.</p>
<p>The post <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> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Recently updated PIE extensions #6 - Exakat
https://www.exakat.io/?p=16409
2026-09-10T08:45:32.000Z
Exakat
<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="PHP Pie updates #6" 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 #6 (since September 3rd, 2026)</h2>
<p>33 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/intermaterium/duckeh">intermaterium/duckeh</a> (0.7.3): PHP extension for DuckDB</li>
<li><a href="https://packagist.org/packages/iliaal/mdparser">iliaal/mdparser</a> (0.6.1): 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.11.0): PHP extension for GPU rendering (OpenGL, Vulkan, Metal), audio, video recording, streaming, and input</li>
<li><a href="https://packagist.org/packages/php-debugger/php-debugger">php-debugger/php-debugger</a> (0.3.3): A lightweight, high-performance PHP debugger extension. Comptible with Xdebug step-debugging.</li>
<li><a href="https://packagist.org/packages/iliaal/php-excel">iliaal/php-excel</a> (2.8.0): PHP extension for reading and writing Excel files using LibXL</li>
<li><a href="https://packagist.org/packages/kreuzberg/kreuzberg">kreuzberg/kreuzberg</a> (4.10.3): 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/iliaal/statgrab">iliaal/statgrab</a> (2.2.2): 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/iliaal/php_clickhouse">iliaal/php_clickhouse</a> (0.12.1): Native PHP extension for ClickHouse using the official ClickHouse/clickhouse-cpp client. Connects over the native TCP protocol with LZ4 / ZSTD compression and optional TLS.</li>
<li><a href="https://packagist.org/packages/mongodb/mongodb-extension">mongodb/mongodb-extension</a> (2.5.2): MongoDB driver extension</li>
<li><a href="https://packagist.org/packages/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a> (v1.17.0): Pre-compiled tree-sitter grammars for 371 programming languages</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v2.0.0): 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.1.5): 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.12.3): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/iliaal/phonetic">iliaal/phonetic</a> (0.4.3): 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> (1.5.5.12): PHP PDO Driver for DuckDB, modern analytics</li>
<li><a href="https://packagist.org/packages/iliaal/pdo_duckdb">iliaal/pdo_duckdb</a> (0.7.1): PDO driver for DuckDB, the in-process analytical database.</li>
<li><a href="https://packagist.org/packages/iliaal/fast_uuid">iliaal/fast_uuid</a> (0.7.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/intermaterium/phpzmq">intermaterium/phpzmq</a> (2.0.5): ZeroMQ is a software library that lets you quickly design and implement a fast message-based applications</li>
<li><a href="https://packagist.org/packages/codelieutenant/scylla-driver">codelieutenant/scylla-driver</a> (v1.5.1): ScyllaDB/Cassandra PHP driver</li>
<li><a href="https://packagist.org/packages/goldziher/spikard">goldziher/spikard</a> (v0.17.1): 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.6.2): 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.8.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/iliaal/fastchart">iliaal/fastchart</a> (1.7.3): Native C PHP extension for fast chart rendering: 38 chart families (line, area, bar, pie, scatter, bubble, stock with technical indicators, radar, polar, surface, contour, treemap, funnel, waterfall, heatmap, gauge, linear meter, gantt, box plot, bullet, pareto, calendar heatmap, sunburst, sankey, marimekko, vector, arc diagram, chord diagram, network, population pyramid, violin plot, circle packing, pictogram, venn diagram, word cloud, serpentine timeline, dendrogram, partition) plus a 2-class Symbol family (Code128, QrCode). SVG-canonical pipeline rasterized via vendored plutovg + plutosvg; PNG / JPEG / WebP encoders via libpng / libjpeg-turbo / libwebp.</li>
<li><a href="https://packagist.org/packages/prateekbhujel/php-terminal">prateekbhujel/php-terminal</a> (v0.7.0): 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.0.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/laruence/yac">laruence/yac</a> (2.4.2): Yac is a shared and lockless memory user data cache for PHP.</li>
<li><a href="https://packagist.org/packages/xberg-io/crawlberg">xberg-io/crawlberg</a> (v1.6.0): High-performance web crawling engine</li>
<li><a href="https://packagist.org/packages/goopil/rabbit-rs-native">goopil/rabbit-rs-native</a> (v0.1.6): High-performance RabbitMQ transport for PHP and Laravel, powered by Rust</li>
<li><a href="https://packagist.org/packages/developgravity/lua-ext">developgravity/lua-ext</a> (0.1.0-rc.3): A PHP extension embedding a vendored, patched Lua 5.5 interpreter to run untrusted code under enforced CPU, wall-clock, memory and output limits, with a capability-gated standard library.</li>
<li><a href="https://packagist.org/packages/orieg/expanse-extension">orieg/expanse-extension</a> (v0.6.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/xdebug/xdebug">xdebug/xdebug</a> (3.6.0alpha1): Xdebug is a debugging and productivity extension for PHP</li>
<li><a href="https://packagist.org/packages/jbboehr/php-yumemi">jbboehr/php-yumemi</a> (v0.1.0): Native extension that adds operators and unit-expression parsing to yumemi.php.</li>
<li><a href="https://packagist.org/packages/kumwe/kumwe-engine">kumwe/kumwe-engine</a> (v1.0.1): Bounded Zend binding to the exact embedded Kumwe Engine release of the same version.</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-6/">Recently updated PIE extensions #6</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
PHP-Powered Government Websites by Country - Exakat
https://www.exakat.io/?p=16404
2026-09-09T15:55:54.000Z
Exakat
<h1><a href="https://www.exakat.io/wp-content/uploads/2026/09/globe.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16405" src="https://www.exakat.io/wp-content/uploads/2026/09/globe.320-300x300.jpg" alt="PHP-Powered Government Websites by Country" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/globe.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/globe.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/globe.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/globe.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>PHP-Powered Government Websites by Country</h1>
<p>After reading the <a href="https://thephp.foundation/blog/2026/09/02/digital-sovereignty-is-written-in-php/">Digital Sovereignty Is Written in PHP</a>, I became curious where in the world is PHP used in the governement.</p>
<p>Governments may be large or small, and it may have several departements, ministries, cabinets or even local administrations with competing technologies. For this page, I only collected at least one PHP powered site.</p>
<p>The list has all 191 UN Member States + 1 UN Observer State, sorted alphabetically by English name. There are some <strong><a href="#notes">Notes</a></strong> at the bottom, and an <strong><a href="#methodology">Methodology Annex</a></strong> with details about the methodology. This page might not get much update in the future, but a related repository <a href="https://codeberg.org/exakat/php-in-the-world.git">php-in-the-world</a> is ready to host updates and complements.</p>
<p><strong>Note</strong>: some websites may not be available to you for various reasons.</p>
<h2 id="toc_1">PHP in every governments on the planet</h2>
<table>
<thead>
<tr>
<th>Country</th>
<th>Government Website</th>
<th>PHP Technology / CMS</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1f1.png" alt="🇦🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Albania</strong></td>
<td><a href="https://www.e-albania.al/">e-albania.al</a></td>
<td>Drupal / Symfony</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1e9.png" alt="🇦🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Andorra</strong></td>
<td><a href="https://www.govern.ad/">govern.ad</a></td>
<td>TYPO3</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1f4.png" alt="🇦🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Angola</strong></td>
<td><a href="https://www.minfin.gov.ao/">minfin.gov.ao</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1ec.png" alt="🇦🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Antigua and Barbuda</strong></td>
<td><a href="https://www.ab.gov.ag/">ab.gov.ag</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1f7.png" alt="🇦🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Argentina</strong></td>
<td><a href="https://www.argentina.gob.ar/">argentina.gob.ar</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1f2.png" alt="🇦🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Armenia</strong></td>
<td><a href="https://www.gov.am/">gov.am</a></td>
<td>Custom PHP</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1fa.png" alt="🇦🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Australia</strong></td>
<td><a href="https://www.australia.gov.au/">australia.gov.au</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1f9.png" alt="🇦🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Austria</strong></td>
<td><a href="https://www.austria.gv.at/">austria.gv.at</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1ff.png" alt="🇦🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Azerbaijan</strong></td>
<td><a href="https://www.gov.az/">gov.az</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1f8.png" alt="🇧🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bahamas</strong></td>
<td><a href="https://www.bahamas.gov.bs/">bahamas.gov.bs</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1ed.png" alt="🇧🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bahrain</strong></td>
<td><a href="https://www.bahrain.bh/">bahrain.bh</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1e9.png" alt="🇧🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bangladesh</strong></td>
<td><a href="http://www.bangladesh.gov.bd/">bangladesh.gov.bd</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1e7.png" alt="🇧🇧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Barbados</strong></td>
<td><a href="https://www.gov.bb/">gov.bb</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1fe.png" alt="🇧🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Belarus</strong></td>
<td><a href="https://belarus.by/">belarus.by</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1ea.png" alt="🇧🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Belgium</strong></td>
<td><a href="https://www.belgium.be/">belgium.be</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1ff.png" alt="🇧🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Belize</strong></td>
<td><a href="https://www.belize.gov.bz/">belize.gov.bz</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1ef.png" alt="🇧🇯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Benin</strong></td>
<td><a href="https://presidence.bj/">presidence.bj</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1f9.png" alt="🇧🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bhutan</strong></td>
<td><a href="https://www.gov.bt/">gov.bt</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1f4.png" alt="🇧🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bolivia</strong></td>
<td><a href="https://www.bolivia.gob.bo/">bolivia.gob.bo</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1e6.png" alt="🇧🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bosnia and Herzegovina</strong></td>
<td><a href="https://www.mvp.gov.ba/">mvp.gov.ba</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1fc.png" alt="🇧🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Botswana</strong></td>
<td><a href="https://www.gov.bw/">gov.bw</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1f7.png" alt="🇧🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Brazil</strong></td>
<td><a href="https://www2.camara.leg.br/">www2.camara.leg.br</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1f3.png" alt="🇧🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Brunei</strong></td>
<td><a href="https://www.gov.bn/">gov.bn</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1ec.png" alt="🇧🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Bulgaria</strong></td>
<td><a href="https://www.government.bg/">government.bg</a></td>
<td>Symfony / Custom PHP</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1eb.png" alt="🇧🇫" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Burkina Faso</strong></td>
<td><a href="http://www.presidence.bf/">presidence.bf</a></td>
<td>SPIP</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e7-1f1ee.png" alt="🇧🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Burundi</strong></td>
<td><a href="https://www.presidence.bi/">presidence.bi</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1fb.png" alt="🇨🇻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Cabo Verde</strong></td>
<td><a href="https://www.gov.cv/">gov.cv</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1ed.png" alt="🇰🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Cambodia</strong></td>
<td><a href="http://www.cambodia.gov.kh/">cambodia.gov.kh</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1f2.png" alt="🇨🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Cameroon</strong></td>
<td><a href="https://www.spm.gov.cm/">spm.gov.cm</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1e6.png" alt="🇨🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Canada</strong></td>
<td><a href="https://www.canada.ca/">canada.ca</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1eb.png" alt="🇨🇫" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Central African Republic</strong></td>
<td><a href="https://www.presidence.cf/">presidence.cf</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1e9.png" alt="🇹🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Chad</strong></td>
<td><a href="https://presidence.td/">presidence.td</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1f1.png" alt="🇨🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Chile</strong></td>
<td><a href="https://www.gob.cl/">gob.cl</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1f3.png" alt="🇨🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>China</strong></td>
<td><a href="https://en.beijing.gov.cn/">en.beijing.gov.cn</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1f4.png" alt="🇨🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Colombia</strong></td>
<td><a href="https://www.gov.co/">gov.co</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1f2.png" alt="🇰🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Comoros</strong></td>
<td><a href="https://presidence.km/">presidence.km</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1ec.png" alt="🇨🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Congo (Brazzaville)</strong></td>
<td><a href="http://www.gouv.cg/">gouv.cg</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1e9.png" alt="🇨🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Congo (DR)</strong></td>
<td><a href="https://www.presidence.cd/">presidence.cd</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1f7.png" alt="🇨🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Costa Rica</strong></td>
<td><a href="https://www.gov.cr/">gov.cr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ed-1f1f7.png" alt="🇭🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Croatia</strong></td>
<td><a href="https://vlada.gov.hr/">vlada.gov.hr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1fa.png" alt="🇨🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Cuba</strong></td>
<td><a href="https://minrex.gob.cu/">minrex.gob.cu</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1fe.png" alt="🇨🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Cyprus</strong></td>
<td><a href="https://www.cyprus.gov.cy/">cyprus.gov.cy</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1ff.png" alt="🇨🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Czechia</strong></td>
<td><a href="https://www.vlada.gov.cz/">vlada.gov.cz</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1ee.png" alt="🇨🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Côte d’Ivoire</strong></td>
<td><a href="https://www.gouv.ci/">gouv.ci</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e9-1f1f0.png" alt="🇩🇰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Denmark</strong></td>
<td><a href="https://www.denmark.dk/">denmark.dk</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e9-1f1ef.png" alt="🇩🇯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Djibouti</strong></td>
<td><a href="https://www.presidence.dj/">presidence.dj</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e9-1f1f2.png" alt="🇩🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Dominica</strong></td>
<td><a href="https://dominica.dm/">dominica.dm</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e9-1f1f4.png" alt="🇩🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Dominican Republic</strong></td>
<td><a href="https://dominicana.gob.do/">dominicana.gob.do</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ea-1f1e8.png" alt="🇪🇨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Ecuador</strong></td>
<td><a href="https://educacion.gob.ec/">educacion.gob.ec</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ea-1f1ec.png" alt="🇪🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Egypt</strong></td>
<td><a href="https://www.egypt.gov.eg/">egypt.gov.eg</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1fb.png" alt="🇸🇻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>El Salvador</strong></td>
<td><a href="https://www.presidencia.gob.sv/">presidencia.gob.sv</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1f6.png" alt="🇬🇶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Equatorial Guinea</strong></td>
<td><a href="https://presidencia-guinee.com/">presidencia-guinee.com</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ea-1f1f7.png" alt="🇪🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Eritrea</strong></td>
<td><a href="https://www.shabait.com/">shabait.com</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ea-1f1ea.png" alt="🇪🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Estonia</strong></td>
<td><a href="https://www.valitsus.ee/">valitsus.ee</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1ff.png" alt="🇸🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Eswatini</strong></td>
<td><a href="https://www.gov.sz/">gov.sz</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ea-1f1f9.png" alt="🇪🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Ethiopia</strong></td>
<td><a href="http://www.ethiopia.gov.et/">ethiopia.gov.et</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1eb-1f1ef.png" alt="🇫🇯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Fiji</strong></td>
<td><a href="https://www.fiji.gov.fj/">fiji.gov.fj</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1eb-1f1ee.png" alt="🇫🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Finland</strong></td>
<td><a href="https://www.valtioneuvosto.fi/">valtioneuvosto.fi</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1eb-1f1f7.png" alt="🇫🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>France</strong></td>
<td><a href="https://www.service-public.fr/">service-public.fr</a></td>
<td>Symfony / Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1e6.png" alt="🇬🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Gabon</strong></td>
<td><a href="https://gouv.ga/">gouv.ga</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1f2.png" alt="🇬🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Gambia</strong></td>
<td><a href="https://statehouse.gov.gm/">statehouse.gov.gm</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1ea.png" alt="🇬🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Georgia</strong></td>
<td><a href="https://www.gov.ge/">gov.ge</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e9-1f1ea.png" alt="🇩🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Germany</strong></td>
<td><a href="https://www.bundesregierung.de/">bundesregierung.de</a></td>
<td>TYPO3</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1ed.png" alt="🇬🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Ghana</strong></td>
<td><a href="https://www.gov.gh/">gov.gh</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1f7.png" alt="🇬🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Greece</strong></td>
<td><a href="https://www.gov.gr/">gov.gr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1e9.png" alt="🇬🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Grenada</strong></td>
<td><a href="https://www.gov.gd/">gov.gd</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1f9.png" alt="🇬🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Guatemala</strong></td>
<td><a href="https://www.minex.gob.gt/">minex.gob.gt</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1f3.png" alt="🇬🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Guinea</strong></td>
<td><a href="http://www.presidence.gov.gn/">presidence.gov.gn</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1fc.png" alt="🇬🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Guinea-Bissau</strong></td>
<td><a href="http://www.secom.gov.gw/">secom.gov.gw</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1fe.png" alt="🇬🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Guyana</strong></td>
<td><a href="https://minfor.gov.gy/">minfor.gov.gy</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ed-1f1f9.png" alt="🇭🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Haiti</strong></td>
<td><a href="http://presidence.ht/">presidence.ht</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ed-1f1f3.png" alt="🇭🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Honduras</strong></td>
<td><a href="https://www.presidencia.hn/">presidencia.hn</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ed-1f1fa.png" alt="🇭🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Hungary</strong></td>
<td><a href="https://www.magyarorszag.hu/">magyarorszag.hu</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1f8.png" alt="🇮🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Iceland</strong></td>
<td><a href="https://www.government.is/">government.is</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1f3.png" alt="🇮🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>India</strong></td>
<td><a href="https://www.india.gov.in/">india.gov.in</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1e9.png" alt="🇮🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Indonesia</strong></td>
<td><a href="https://www.setkab.go.id/">setkab.go.id</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1f7.png" alt="🇮🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Iran</strong></td>
<td><a href="https://www.iran.ir/">iran.ir</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1f6.png" alt="🇮🇶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Iraq</strong></td>
<td><a href="https://www.cabinet.gov.iq/">cabinet.gov.iq</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1ea.png" alt="🇮🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Ireland</strong></td>
<td><a href="https://www.gov.ie/">gov.ie</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1f1.png" alt="🇮🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Israel</strong></td>
<td><a href="https://www.gov.il/">gov.il</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ee-1f1f9.png" alt="🇮🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Italy</strong></td>
<td><a href="https://www.governo.it/">governo.it</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ef-1f1f2.png" alt="🇯🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Jamaica</strong></td>
<td><a href="https://www.jis.gov.jm/">jis.gov.jm</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ef-1f1f5.png" alt="🇯🇵" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Japan</strong></td>
<td><a href="https://www.japan.go.jp/">japan.go.jp</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ef-1f1f4.png" alt="🇯🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Jordan</strong></td>
<td><a href="https://portal.jordan.gov.jo/">portal.jordan.gov.jo</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1ff.png" alt="🇰🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Kazakhstan</strong></td>
<td><a href="https://primeminister.kz/">primeminister.kz</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1ea.png" alt="🇰🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Kenya</strong></td>
<td><a href="https://www.parliament.go.ke/">parliament.go.ke</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1ee.png" alt="🇰🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Kiribati</strong></td>
<td><a href="http://www.parliament.gov.ki/">parliament.gov.ki</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1fc.png" alt="🇰🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Kuwait</strong></td>
<td><a href="https://kuwait.gov.kw/">kuwait.gov.kw</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1ec.png" alt="🇰🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Kyrgyzstan</strong></td>
<td><a href="https://www.gov.kg/">gov.kg</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1e6.png" alt="🇱🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Laos</strong></td>
<td><a href="http://www.laogov.la/">laogov.la</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1fb.png" alt="🇱🇻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Latvia</strong></td>
<td><a href="https://www.latvija.lv/">latvija.lv</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1e7.png" alt="🇱🇧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Lebanon</strong></td>
<td><a href="https://www.presidency.gov.lb/">presidency.gov.lb</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1f8.png" alt="🇱🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Lesotho</strong></td>
<td><a href="http://www.gov.ls/">gov.ls</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1f7.png" alt="🇱🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Liberia</strong></td>
<td><a href="https://mofa.gov.lr/">mofa.gov.lr</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1fe.png" alt="🇱🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Libya</strong></td>
<td><a href="https://www.gov.ly/">gov.ly</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1ee.png" alt="🇱🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Liechtenstein</strong></td>
<td><a href="https://www.llv.li/">llv.li</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1f9.png" alt="🇱🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Lithuania</strong></td>
<td><a href="https://www.lithuania.lt/">lithuania.lt</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1fa.png" alt="🇱🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Luxembourg</strong></td>
<td><a href="https://www.luxembourg.lu/">luxembourg.lu</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1ec.png" alt="🇲🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Madagascar</strong></td>
<td><a href="http://www.presidence.gov.mg/">presidence.gov.mg</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1fc.png" alt="🇲🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Malawi</strong></td>
<td><a href="https://www.malawi.gov.mw/">malawi.gov.mw</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1fe.png" alt="🇲🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Malaysia</strong></td>
<td><a href="https://www.malaysia.gov.my/">malaysia.gov.my</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1fb.png" alt="🇲🇻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Maldives</strong></td>
<td><a href="https://presidency.gov.mv/">presidency.gov.mv</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1f1.png" alt="🇲🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mali</strong></td>
<td><a href="http://malibo.org/">malibo.org</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1f9.png" alt="🇲🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Malta</strong></td>
<td><a href="https://www.gov.mt/">gov.mt</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1ed.png" alt="🇲🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Marshall Islands</strong></td>
<td><a href="http://www.rmigov.org/">rmigov.org</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1f7.png" alt="🇲🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mauritania</strong></td>
<td><a href="http://primature.gov.mr/">primature.gov.mr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1fa.png" alt="🇲🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mauritius</strong></td>
<td><a href="https://www.govmu.org/">govmu.org</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1fd.png" alt="🇲🇽" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mexico</strong></td>
<td><a href="https://www.gob.mx/">gob.mx</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1eb-1f1f2.png" alt="🇫🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Micronesia</strong></td>
<td><a href="http://www.fsmgov.org/">fsmgov.org</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1e9.png" alt="🇲🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Moldova</strong></td>
<td><a href="https://www.gov.md/">gov.md</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1e8.png" alt="🇲🇨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Monaco</strong></td>
<td><a href="https://en.gouv.mc/">en.gouv.mc</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1f3.png" alt="🇲🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mongolia</strong></td>
<td><a href="https://www.pmis.gov.mn/">pmis.gov.mn</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1ea.png" alt="🇲🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Montenegro</strong></td>
<td><a href="https://www.gov.me/">gov.me</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1e6.png" alt="🇲🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Morocco</strong></td>
<td><a href="https://www.maroc.ma/">maroc.ma</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1ff.png" alt="🇲🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mozambique</strong></td>
<td><a href="http://www.portaldogoverno.gov.mz/">portaldogoverno.gov.mz</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1f2.png" alt="🇲🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Myanmar</strong></td>
<td><a href="https://www.president-office.gov.mm/">president-office.gov.mm</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1e6.png" alt="🇳🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Namibia</strong></td>
<td><a href="https://www.gov.na/">gov.na</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1f7.png" alt="🇳🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Nauru</strong></td>
<td><a href="http://www.nauru.gov.nr/">nauru.gov.nr</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1f5.png" alt="🇳🇵" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Nepal</strong></td>
<td><a href="https://nepal.gov.np/">nepal.gov.np</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1f1.png" alt="🇳🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Netherlands</strong></td>
<td><a href="https://www.government.nl/">government.nl</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1ff.png" alt="🇳🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>New Zealand</strong></td>
<td><a href="https://www.govt.nz/">govt.nz</a></td>
<td>SilverStripe CMS</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1ee.png" alt="🇳🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Nicaragua</strong></td>
<td><a href="https://www.presidencia.gob.ni/">presidencia.gob.ni</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1ea.png" alt="🇳🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Niger</strong></td>
<td><a href="http://www.presidence.ne/">presidence.ne</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1ec.png" alt="🇳🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Nigeria</strong></td>
<td><a href="https://www.nigeria.gov.ng/">nigeria.gov.ng</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f2-1f1f0.png" alt="🇲🇰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>North Macedonia</strong></td>
<td><a href="https://vlada.mk/">vlada.mk</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f3-1f1f4.png" alt="🇳🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Norway</strong></td>
<td><a href="https://www.regjeringen.no/">regjeringen.no</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f4-1f1f2.png" alt="🇴🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Oman</strong></td>
<td><a href="https://www.oman.om/">oman.om</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1f0.png" alt="🇵🇰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Pakistan</strong></td>
<td><a href="https://www.pakistan.gov.pk/">pakistan.gov.pk</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1fc.png" alt="🇵🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Palau</strong></td>
<td><a href="https://palaugov.org/">palaugov.org</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1f8.png" alt="🇵🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Palestine</strong></td>
<td><a href="https://palestinecabinet.gov.ps/">palestinecabinet.gov.ps</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1e6.png" alt="🇵🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Panama</strong></td>
<td><a href="https://www.panama.gob.pa/">panama.gob.pa</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1ec.png" alt="🇵🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Papua New Guinea</strong></td>
<td><a href="https://www.png.gov.pg/">png.gov.pg</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1fe.png" alt="🇵🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Paraguay</strong></td>
<td><a href="https://www.presidencia.gov.py/">presidencia.gov.py</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1ea.png" alt="🇵🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Peru</strong></td>
<td><a href="https://www.gob.pe/">gob.pe</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1ed.png" alt="🇵🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Philippines</strong></td>
<td><a href="https://www.gov.ph/">gov.ph</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1f1.png" alt="🇵🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Poland</strong></td>
<td><a href="https://www.gov.pl/">gov.pl</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f5-1f1f9.png" alt="🇵🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Portugal</strong></td>
<td><a href="https://www.portugal.gov.pt/">portugal.gov.pt</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f6-1f1e6.png" alt="🇶🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Qatar</strong></td>
<td><a href="https://www.hukoomi.gov.qa/">hukoomi.gov.qa</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f7-1f1f4.png" alt="🇷🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Romania</strong></td>
<td><a href="https://www.gov.ro/">gov.ro</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f7-1f1fa.png" alt="🇷🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Russia</strong></td>
<td><a href="https://www.mos.ru/">mos.ru</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f7-1f1fc.png" alt="🇷🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Rwanda</strong></td>
<td><a href="https://www.gov.rw/">gov.rw</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1f3.png" alt="🇰🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Saint Kitts and Nevis</strong></td>
<td><a href="https://www.gov.kn/">gov.kn</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1e8.png" alt="🇱🇨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Saint Lucia</strong></td>
<td><a href="https://www.govt.lc/">govt.lc</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fb-1f1e8.png" alt="🇻🇨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Saint Vincent and the Grenadines</strong></td>
<td><a href="https://www.gov.vc/">gov.vc</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fc-1f1f8.png" alt="🇼🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Samoa</strong></td>
<td><a href="https://www.mofa.gov.ws/">govt.ws</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f2.png" alt="🇸🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>San Marino</strong></td>
<td><a href="https://www.sanmarino.sm/">sanmarino.sm</a></td>
<td>Custom PHP</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1e6.png" alt="🇸🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Saudi Arabia</strong></td>
<td><a href="https://www.my.gov.sa/">my.gov.sa</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f3.png" alt="🇸🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Senegal</strong></td>
<td><a href="https://www.presidence.sn/">presidence.sn</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f7-1f1f8.png" alt="🇷🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Serbia</strong></td>
<td><a href="https://www.srbija.gov.rs/">srbija.gov.rs</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1e8.png" alt="🇸🇨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Seychelles</strong></td>
<td><a href="https://www.statehouse.gov.sc/">statehouse.gov.sc</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f1.png" alt="🇸🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Sierra Leone</strong></td>
<td><a href="https://statehouse.gov.sl/">statehouse.gov.sl</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1ec.png" alt="🇸🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Singapore</strong></td>
<td><a href="https://www.gov.sg/">gov.sg</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f0.png" alt="🇸🇰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Slovakia</strong></td>
<td><a href="https://www.slovensko.sk/">slovensko.sk</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1ee.png" alt="🇸🇮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Slovenia</strong></td>
<td><a href="https://www.gov.si/">gov.si</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1e7.png" alt="🇸🇧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Solomon Islands</strong></td>
<td><a href="https://www.solomon.gov.sb/">solomon.gov.sb</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f4.png" alt="🇸🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Somalia</strong></td>
<td><a href="https://mfa.gov.so/">mfa.gov.so</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ff-1f1e6.png" alt="🇿🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>South Africa</strong></td>
<td><a href="https://www.gov.za/">gov.za</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f0-1f1f7.png" alt="🇰🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>South Korea</strong></td>
<td><a href="https://www.seoul.go.kr/">seoul.go.kr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f8.png" alt="🇸🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>South Sudan</strong></td>
<td><a href="https://mfa.gov.ss/">mfa.gov.ss</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ea-1f1f8.png" alt="🇪🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Spain</strong></td>
<td><a href="https://www.lamoncloa.gob.es/">moncloa.gob.es</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f1-1f1f0.png" alt="🇱🇰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Sri Lanka</strong></td>
<td><a href="https://www.president.gov.lk/">president.gov.lk</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1e9.png" alt="🇸🇩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Sudan</strong></td>
<td><a href="https://www.mofa.gov.sd/">mofa.gov.sd</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f7.png" alt="🇸🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Suriname</strong></td>
<td><a href="https://www.gov.sr/">gov.sr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1ea.png" alt="🇸🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Sweden</strong></td>
<td><a href="https://sweden.se/">sweden.se</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e8-1f1ed.png" alt="🇨🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Switzerland</strong></td>
<td><a href="https://www.admin.ch/">admin.ch</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1fe.png" alt="🇸🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Syria</strong></td>
<td><a href="https://www.egov.sy/">egov.sy</a></td>
<td>Joomla</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f8-1f1f9.png" alt="🇸🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>São Tomé and Príncipe</strong></td>
<td><a href="http://www.saotome.st/">saotome.st</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1ef.png" alt="🇹🇯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Tajikistan</strong></td>
<td><a href="http://parlament.tj/">parlament.tj</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1ff.png" alt="🇹🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Tanzania</strong></td>
<td><a href="https://www.statehouse.go.tz/">statehouse.go.tz</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1ed.png" alt="🇹🇭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Thailand</strong></td>
<td><a href="https://www.thailand.go.th/">thailand.go.th</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1f1.png" alt="🇹🇱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Timor-Leste</strong></td>
<td><a href="http://timor-leste.gov.tl/">timor-leste.gov.tl</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1ec.png" alt="🇹🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Togo</strong></td>
<td><a href="http://www.presidence.tg/">presidence.tg</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1f4.png" alt="🇹🇴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Tonga</strong></td>
<td><a href="https://www.gov.to/">gov.to</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1f9.png" alt="🇹🇹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Trinidad and Tobago</strong></td>
<td><a href="https://www.ttconnect.gov.tt/">ttconnect.gov.tt</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1f3.png" alt="🇹🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Tunisia</strong></td>
<td><a href="http://www.tunisie.gov.tn/">tunisie.gov.tn</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1f2.png" alt="🇹🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Turkmenistan</strong></td>
<td><a href="https://www.turkmenistan.gov.tm/">turkmenistan.gov.tm</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1fb.png" alt="🇹🇻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Tuvalu</strong></td>
<td><a href="https://gov.tv/">gov.tv</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1f9-1f1f7.png" alt="🇹🇷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Türkiye</strong></td>
<td><a href="https://www.ktb.gov.tr/">ktb.gov.tr</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fa-1f1ec.png" alt="🇺🇬" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Uganda</strong></td>
<td><a href="https://www.statehouse.go.ug/">statehouse.go.ug</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fa-1f1e6.png" alt="🇺🇦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Ukraine</strong></td>
<td><a href="https://www.kmu.gov.ua/">kmu.gov.ua</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1e6-1f1ea.png" alt="🇦🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>United Arab Emirates</strong></td>
<td><a href="https://u.ae/">u.ae</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ec-1f1e7.png" alt="🇬🇧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>United Kingdom</strong></td>
<td><a href="https://www.food.gov.uk/">food.gov.uk</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fa-1f1f8.png" alt="🇺🇸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>United States</strong></td>
<td><a href="https://www.whitehouse.gov/">whitehouse.gov</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fa-1f1fe.png" alt="🇺🇾" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Uruguay</strong></td>
<td><a href="https://www.uruguay.gub.uy/">uruguay.gub.uy</a></td>
<td>Drupal</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fa-1f1ff.png" alt="🇺🇿" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Uzbekistan</strong></td>
<td><a href="https://www.gov.uz/">gov.uz</a></td>
<td>Custom PHP / Laravel</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fb-1f1fa.png" alt="🇻🇺" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Vanuatu</strong></td>
<td><a href="https://gov.vu/">gov.vu</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fb-1f1ea.png" alt="🇻🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Venezuela</strong></td>
<td><a href="https://www.minci.gob.ve/">minci.gob.ve</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fb-1f1f3.png" alt="🇻🇳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Vietnam</strong></td>
<td><a href="https://chinhphu.vn/">chinhphu.vn</a></td>
<td>Laravel / Custom PHP</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1fe-1f1ea.png" alt="🇾🇪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Yemen</strong></td>
<td><a href="https://www.mofa-ye.org/">mofa-ye.org</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ff-1f1f2.png" alt="🇿🇲" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Zambia</strong></td>
<td><a href="https://www.statehouse.gov.zm/">statehouse.gov.zm</a></td>
<td>WordPress</td>
</tr>
<tr>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f1ff-1f1fc.png" alt="🇿🇼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Zimbabwe</strong></td>
<td><a href="https://www.gov.zw/">gov.zw</a></td>
<td>WordPress</td>
</tr>
</tbody>
</table>
<h2 id="toc_2">Named PHP Projects</h2>
<p>The table above names these PHP frameworks and CMSs. It excludes generic “Custom PHP” entries, which aren’t a single named project:</p>
<table>
<thead>
<tr>
<th>Project</th>
<th>Home Page</th>
</tr>
</thead>
<tbody>
<tr>
<td>Drupal</td>
<td><a href="https://www.drupal.org/">drupal.org</a></td>
</tr>
<tr>
<td>Joomla</td>
<td><a href="https://www.joomla.org/">joomla.org</a></td>
</tr>
<tr>
<td>Laravel</td>
<td><a href="https://laravel.com/">laravel.com</a></td>
</tr>
<tr>
<td>SilverStripe CMS</td>
<td><a href="https://www.silverstripe.org/">silverstripe.org</a></td>
</tr>
<tr>
<td>SPIP</td>
<td><a href="https://www.spip.net/">spip.net</a></td>
</tr>
<tr>
<td>Symfony</td>
<td><a href="https://symfony.com/">symfony.com</a></td>
</tr>
<tr>
<td>TYPO3</td>
<td><a href="https://typo3.org/">typo3.org</a></td>
</tr>
<tr>
<td>WordPress</td>
<td><a href="https://wordpress.org/">wordpress.org</a></td>
</tr>
</tbody>
</table>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>Caribbean and Central American</strong> nations, Bahamas, Guatemala, Honduras, show a strong preference for WordPress, likely due to ease of use and lower maintenance costs for smaller IT departments.</li>
<li><strong>China’s inclusion</strong> might seem surprising, since the central government in Beijing relies heavily on custom enterprise Java. But many municipal and provincial portals, including the English-language Beijing government site, utilize Drupal to handle multilingual content and international public relations.</li>
<li><strong>Estonia</strong> is famous for its advanced digital society, called e-Governance. While much of its backend infrastructure is custom-built, its main government web portal uses Drupal for frontend content management.</li>
<li><strong>European Microstates</strong>: Liechtenstein relies on Drupal, while Andorra uses TYPO3, sharing a tech affinity with Switzerland and Germany. Monaco’s official portal is a beautifully executed Drupal site.</li>
<li><strong>Francophone Africa</strong> is heavily represented by WordPress. Countries like Benin, Burundi, DR Congo, Djibouti, and Haiti all rely on it for their presidential portals, valuing rapid deployment, easy content updates, and low hosting overhead.</li>
<li><strong>Israel’s single unified government portal</strong> is a major Drupal implementation, handling millions of interactions and integrating with their “Tofes Yarok” (Green Pass) systems during the pandemic.</li>
<li><strong>Japan</strong> and <strong>Malaysia</strong> are great examples of major Asian governments utilizing WordPress VIP or heavily customized WordPress enterprise setups for their primary portals.</li>
<li>Joomla is less common than Drupal or WordPress for large federal portals, but remains in active or legacy use across the <strong>Middle East, North Africa, and parts of Asia</strong>, Iran, Pakistan, Iraq, Mozambique, Laos, and Syria all run government or e-government portals on it, often valued for its strong out-of-the-box multilingual support. Cuba and Nepal are further examples, in more budget-conscious or historically isolated tech environments.</li>
<li>Laravel in <strong>Vietnam</strong>: the main government portal utilizes Laravel, a robust PHP framework. This shows that governments don’t just use “out-of-the-box” CMSs, some build highly customized, bespoke applications using modern PHP frameworks.</li>
<li><strong>The Middle East</strong> has heavily embraced Drupal. Qatar’s “Hukoomi” portal and Saudi Arabia’s “My.gov.sa” are massive, highly customized Drupal implementations serving millions of citizens and expats in multiple languages.</li>
<li><strong>Oman</strong> stands out in the Gulf region. While many of its neighbors use custom enterprise Java or .NET setups, Oman’s central portal has heavily leveraged Drupal to manage its massive digital transformation and Arabic/English content.</li>
<li><strong>The post-Soviet sphere</strong> (Belarus, Kazakhstan, Kyrgyzstan) shows a strong trend of utilizing Drupal for e-government modernization, favoring its ability to handle Cyrillic alphabets and complex user permissions securely.</li>
<li><strong>Russia’s capital city, Moscow</strong>, runs one of the largest and most complex Drupal installations in the world. <code>mos.ru</code> handles tens of millions of users and integrates hundreds of city services: a premier example of enterprise-level PHP at massive scale.</li>
<li><strong>Rwanda</strong> continues its reputation as a tech-forward African nation by running its main portal on Drupal, aligning with its national strategy to digitize government services.</li>
<li><strong>San Marino</strong> uses a custom PHP setup for its official portal, proving that smaller European microstates often rely on bespoke PHP applications tailored to their specific administrative needs rather than off-the-shelf CMSs.</li>
<li>The shift towards Drupal across <strong>Eastern Europe</strong> (Czechia, Latvia, Lithuania, Romania, Slovakia) is particularly strong due to its native support for multilingual content and complex user role management.</li>
<li><strong>Small Island Developing States (SIDS)</strong> across the Caribbean and Pacific, with Grenada, São Tomé and Príncipe, Guyana, Marshall Islands, Micronesia, Nauru, Palau, Samoa, Solomon Islands, Tuvalu, Tonga, Vanuatu, and Saint Vincent and the Grenadines, overwhelmingly favor WordPress and Drupal. Both platforms let small IT departments deploy modern, secure government sites without needing large in-house development or server teams.</li>
<li><strong>Singapore</strong> is a fantastic example of a highly developed, secure digital government portal running on Drupal.</li>
<li><strong>South Korea</strong> might seem like an unusual entry, since the central government often relies on custom Java-based platforms. However, local and major municipal portals, like the Seoul Metropolitan Government, heavily utilize Drupal for their public-facing websites, particularly for their multilingual expat portals.</li>
<li>SPIP powers <strong>Burkina Faso</strong>: the presidency site uses this highly respected PHP CMS, originally created in France. It has historically been very popular in Francophone Africa for its multilingual and structural capabilities, though many sites are now migrating to WordPress or Drupal.</li>
<li><strong>Sweden’s</strong> official informational portal runs on WordPress, demonstrating how different branches of a government, informational vs. administrative, might use different stacks.</li>
<li><strong>Türkiye</strong>: the linked site is the Ministry of Culture and Tourism. Many Turkish government ministries rely on Drupal, though some specific e-service gateways use custom or .NET backends.</li>
<li><strong>Ukraine</strong> is a massive success story for Drupal. Despite the ongoing war, their highly robust Drupal-based government portal has remained online and functional, showcasing the CMS’s resilience and decentralized capabilities.</li>
<li><strong>Uzbekistan</strong> is a standout, utilizing a custom Laravel backend for <code>gov.uz</code>. Laravel is increasingly popular for bespoke government applications that need highly specific API integrations and business logic that a traditional CMS might not handle gracefully out-of-the-box.</li>
</ul>
<h2 id="methodology">Methodology and Caveats</h2>
<p>The country list is based on current UN member states, plus Palestine as a UN observer state.</p>
<p>For each country, sites were searched roughly in this order, stopping once a PHP-powered site was found:</p>
<ol>
<li>The main government portal (e.g. a <code>.gov</code> / <code>.gob</code> / <code>.gouv</code> domain, or an official national portal).</li>
<li>The presidency site, or equivalent head-of-state site, where the main portal wasn’t a good match.</li>
<li>The prime minister’s office and the Ministry of Foreign Affairs.</li>
<li>Major city or capital-city government sites, for countries where no suitable national-level PHP site was found.</li>
</ol>
<p>Government tech stacks are occasionally migrated, and the sites listed above are historically or currently powered by PHP-based frameworks/CMSs (such as Drupal, WordPress, TYPO3, SilverStripe, SPIP, Symfony, or Laravel).</p>
<p>These sites change fast, and availability is not guaranteed: government domains can go offline, get redesigned onto a different stack, or become unreachable for stretches of time: particularly around elections, coups, sanctions, or armed conflict. Treat this list as a snapshot, not a live status check.</p>
<p>This list has a related repository called <a href="https://codeberg.org/exakat/php-in-the-world.git">php-in-the-world</a>, that can accept more insights, domains and recept updates.</p>
<p>The post <a href="https://www.exakat.io/php-powered-government-websites-by-country/">PHP-Powered Government Websites by Country</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
The Evolution of WSL Support in JetBrains IDEs - PhpStorm : The IDE that empowers PHP developers | The JetBrains Blog
https://blog.jetbrains.com/?post_type=platform&p=738427
2026-09-09T10:39:49.000Z
PhpStorm : The IDE that empowers PHP developers | The JetBrains Blog
<p>JetBrains IDEs have worked with WSL for many years, and over time, several ways of using it have emerged across our products. Depending on the entry point, the IDE could rely on a different underlying architecture, leading to a different experience.</p>
<p>Starting with the 2026.2 release, there is one recommended entry point. In IntelliJ IDEA, WebStorm, and PhpStorm, opening a project that lives in WSL runs the IDE in what we call <em>Native</em> mode. The IDE stays a Windows application, while a small agent inside WSL handles files and processes on its behalf.</p>
<p>Below is how <em>Native</em> mode works, how our approach to WSL integration evolved to get here, and – since we tried three other approaches first – why we’re confident it’s the right foundation for your work.</p>
<h2 class="wp-block-heading">What it takes to support WSL in an IDE</h2>
<p>We’ll start with an idealized picture in which the IDE functions seamlessly inside WSL while being operated from Windows. In practical terms, “seamlessly” means you should be able to use any of the IDE’s capabilities – terminal, run and debug configurations, profiling, etc. – against a project that resides in a Linux environment. Not only that, but this must incur as little latency as possible for the coding experience to feel quick and natural, as if the project files and the IDE itself resided in a single operating-system environment.</p>
<figure class="wp-block-image size-full"><img style="width:100% !important; height:auto !important; max-width:100% !important;" fetchpriority="high" decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2026/09/image3.jpg" alt="" class="wp-image-738429"/></figure>
<p>We’ll trace how this challenge has been addressed, beginning with the earliest implementation, in which the IDE interacted with WSL through the 9P transport protocol. We will then cover Remote Development and WSLg before examining <em>Native</em> mode – the currently preferred mode for how IDEs work with WSL.</p>
<h2 class="wp-block-heading">Toward native IDE execution in WSL </h2>
<h3 class="wp-block-heading">9P filesystem access and GeneralCommandLine</h3>
<p>For the IDE to function, it needs access to the basics of a project, which are plain files. This is where the 9P filesystem protocol comes in. It provides the mechanism that allows Windows-side processes access files inside WSL. In practice, project-related file I/O performed by the IDE – scanning, indexing, archive access, and similar operations – is routed through the 9P-based filesystem layer to the Linux virtual machine.</p>
<figure class="wp-block-image size-full"><img style="width:100% !important; height:auto !important; max-width:100% !important;" decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2026/09/image5.jpg" alt="" class="wp-image-738440"/></figure>
<p>Once file access is accounted for, the next challenge is process execution. While running on Windows, the IDE must be able to invoke tools inside WSL with the correct Linux paths, working directories, executables, environment variables, and arguments. The IDE-internal <a href="https://github.com/17712484466/intellij-community/blob/master/platform/platform-api/src/com/intellij/execution/configurations/GeneralCommandLine.java" target="_blank" rel="noopener"><code>GeneralCommandLine</code></a> class manages this by normalizing command execution in the WSL environment.</p>
<p>While this architecture enables the IDE to operate against WSL, it introduces significant trade-offs in both file access and process execution.</p>
<p>The 9P protocol is known to be problematic for several reasons:</p>
<ul class="wp-block-list">
<li>Limited symlink handling – 9P does not properly expose Linux symbolic links to Windows through <code>\\wsl$</code>. As a result, the IDE may detect an entry but be unable to resolve or index the linked directory tree. This affects pnpm workspaces, Python virtual environments, PHP Composer path repositories, and other environments that rely on symlinks.</li>
<li>Microsoft Defender scans – on-access scanning over 9P can stretch WSL file reads by tens of seconds.</li>
<li>Latency and degraded throughput – 9P-backed filesystem access adds latency, and throughput can degrade significantly, especially in workflows involving many small files. Core IDE operations such as indexing follow this pattern, generating numerous separate requests that cross the Windows/WSL VM boundary through 9P.</li>
</ul>
<p>The situation was no less challenging on the <code>GeneralCommandLine</code> side. The pipeline added a maintenance burden, as developers had to account for WSL-specific execution semantics across the codebase.</p>
<p>As a consequence, the approach became increasingly difficult to scale and maintain, and it surfaced persistent issues with symlink handling and performance.</p>
<h3 class="wp-block-heading">Running the IDE inside WSL with WSLg</h3>
<p>What if, instead, the IDE itself resided inside WSL, close to the project it works with? This is where WSLg comes in. It provides a way to run Linux GUI applications whose visual output is integrated directly into the Windows desktop.</p>
<figure class="wp-block-image size-full"><img style="width:100% !important; height:auto !important; max-width:100% !important;" decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2026/09/image4.jpg" alt="" class="wp-image-738451"/></figure>
<p>Here’s why we do not recommend the WSLg approach:</p>
<ul class="wp-block-list">
<li>From a product perspective, if the IDE is displayed on Windows, it is preferable for it to behave as a Windows-native application rather than as a Linux GUI application projected into the Windows desktop. This preserves greater control over the IDE experience and avoids dependence on an additional GUI-remoting layer.</li>
<li>In a WSLg setup, the IDE may run on the native Wayland path, where JetBrains Runtime uses WLToolkit against WSLg’s Wayland compositor. This path carries known limitations in rendering, popups, window management, input methods, and desktop integration.</li>
</ul>
<p>No first-class product experience was ever built around this setup. Although running the IDE through WSLg is technically viable, it has never been considered the preferred direction, and no dedicated out-of-the-box installation or onboarding flow was provided. It remains an ad hoc workaround rather than a supported IDE workflow.</p>
<h3 class="wp-block-heading">The Remote Development approach</h3>
<p>Another approach was Remote Development, which addressed the same fundamental challenges of file access and process execution from a different angle. Rather than extending the Windows-based IDE across the WSL boundary, it moved the IDE backend into WSL and kept only the client on Windows. This architecture solved the underlying integration problems more directly, but introduced a different set of trade-offs.</p>
<figure class="wp-block-image size-full"><img style="width:100% !important; height:auto !important; max-width:100% !important;" loading="lazy" decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2026/09/image6.jpg" alt="" class="wp-image-738462"/></figure>
<p>The main technical challenge of this configuration is establishing and maintaining communication between them while ensuring that the UI accurately reflects user actions and the IDE continues to function as a coherent whole. At the same time, placing the backend inside WSL provides the clear advantage of direct access to Linux files and processes.</p>
<p>In this setup, the two IDE parts communicate over the JetBrains RD protocol. The protocol is a structured, bidirectional stream of IDE models and events that keeps the thin client and the backend IDE logically in sync, while all heavy work, such as indexing, analysis, builds, debugging, VCS operations, and so on, happens on the backend.</p>
<p>The client, in its turn:</p>
<ul class="wp-block-list">
<li>Renders all windows and editors, and handles user input.</li>
<li>Mirrors the project and editor state it receives from the backend over the RD protocol, and sends back edits, caret moves, refactoring commands, debug actions, and so on.</li>
<li>Loads UI-level plugins.</li>
</ul>
<p>The elephant in the room, however, is the cost of this split architecture. The IDE backend is a heavyweight component in its own right, requiring roughly 2 GB of additional disk space and time to download and install inside WSL. The split also introduces an inherent performance penalty, as UI-event state, user input, and other interaction data must travel continuously between the local client and the backend.</p>
<p>It also adds development overhead, as the code has to be split into client and server parts. Leaving specific modules undivided can cause delays and freezes in highly dynamic UIs. The associated engineering effort therefore becomes a constant and unavoidable cost.</p>
<h3 class="wp-block-heading"><em>Native</em> mode and the IJent agent</h3>
<p>The current design – now integrated into most of our IDEs – represents the culmination of this work and addresses many of the shortcomings of earlier approaches to WSL.</p>
<figure class="wp-block-image size-full"><img style="width:100% !important; height:auto !important; max-width:100% !important;" loading="lazy" decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2026/09/WSL-Native-Mode.jpg" alt="" class="wp-image-738562"/></figure>
<p>To provide access to the Linux filesystem, processes, and other environment resources without relying on 9P, <code>GeneralCommandLine</code>, or other second-class communication mechanisms, we developed a small agent named IJent. Because it is designed specifically for IDE scenarios, we can shape its behavior, protocol, and capabilities according to our requirements.</p>
<p>Together, the IDE and IJent form a client–server pair. This may resemble the Remote Development model, but the server component is much thinner and, at the same time, more versatile. Installing IJent into a WSL environment is merely one of several possible configurations; the same model can also apply to Docker and Dev Containers.</p>
<p>By choosing this particular technology stack, we address a broad set of concerns:</p>
<ul class="wp-block-list">
<li>Rust implementation – Rust keeps the executable slim. By avoiding Java/Kotlin for the agent, we eliminate additional runtime dependencies inside containers or WSL.</li>
<li>Transport layer – Stdio gives us a portable, firewall-friendly transport across all environments. Hyper-V sockets on WSL offer a faster path, which is especially beneficial for large or numerous filesystem transfers.</li>
<li>Correct filesystem semantics – IJent executes filesystem operations on behalf of the IDE inside the target environment. As a result, path resolution, including symbolic links, follows correct Linux semantics rather than being mediated through 9P. Third-party plugins also benefit from this model, since their file operations are routed through IJent.</li>
</ul>
<p></p>
<h4 class="wp-block-heading">EelApi: one interface for any environment</h4>
<p>For the IDE to fully benefit from IJent, changes on the IDE side are also required. Enter EelAPI.</p>
<p>EelApi is an API designed to abstract away the distinction between local and remote environments for everyone writing IDE-related code – plugin authors and platform contributors alike. With EelApi, the underlying environment against which the IDE operates should no longer matter, removing the need to account for these concerns explicitly in code. In this sense, EelApi is platform-agnostic, and the IDE can remain unaware of whether it is working locally, in WSL, in Docker, or in a Dev Container.</p>
<p>In general, IJent implements the EelApi interface, providing the actual functionality that EelApi exposes to the IDE and plugins.</p>
<p>If you’re interested in a more detailed explanation of EelApi, together with a practical introduction to the topic, head over to the article <a href="https://blog.jetbrains.com/platform/2026/06/the-dev-containers-story-introducing-eelapi-for-plugin-authors">The Dev Containers Story: Introducing EelApi for Plugin Authors</a>.</p>
<h2 class="wp-block-heading">What this means in practice</h2>
<p>There are two ways to open a WSL project today:</p>
<ul class="wp-block-list">
<li>Opening the project directly gives you <em>Native</em> mode in IntelliJ IDEA, WebStorm, and PhpStorm, and more IDEs are adopting the IJent and EelApi architecture as we speak.</li>
<li>The Remote Development entry point on the Welcome screen still works, but it is no longer the recommended way to open a WSL project.</li>
</ul>
<p>We conducted performance tests in order to compare 9P mode and <em>Native</em> mode. The results show a clear performance gain in favor of the new approach:</p>
<section class="jb-wsl-perf-widget" data-jb-wsl-perf-widget aria-labelledby="jb-wsl-perf-title">
<style>
.jb-wsl-perf-widget {
--jb-wsl-perf-text: #19191c;
--jb-wsl-perf-muted: #5f5f66;
--jb-wsl-perf-line: #d9d9e3;
--jb-wsl-perf-soft: #f4f4f7;
--jb-wsl-perf-track: #e9e9ee;
--jb-wsl-perf-legacy: #7b7b84;
--jb-wsl-perf-accent: #6b57ff;
color: var(--jb-wsl-perf-text);
font: inherit;
margin: 32px 0;
}
.jb-wsl-perf-widget * { box-sizing: border-box; }
.jb-wsl-perf-panel {
background: #fff;
border: 1px solid var(--jb-wsl-perf-line);
border-radius: 8px;
overflow: hidden;
}
.jb-wsl-perf-header {
display: grid;
gap: 18px;
grid-template-columns: minmax(0, 1fr);
padding: 26px 28px 24px;
}
.jb-wsl-perf-eyebrow {
color: var(--jb-wsl-perf-accent);
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
line-height: 1.3;
margin: 0 0 8px;
text-transform: uppercase;
}
.jb-wsl-perf-title {
color: var(--jb-wsl-perf-text);
font-size: clamp(24px, 4vw, 34px);
font-weight: 700;
letter-spacing: -.02em;
line-height: 1.15;
margin: 0;
max-width: 680px;
}
.jb-wsl-perf-chart {
border-top: 1px solid var(--jb-wsl-perf-line);
padding: 24px 28px 26px;
}
.jb-wsl-perf-legend {
display: flex;
flex-wrap: wrap;
gap: 10px 20px;
margin: 0 0 20px;
}
.jb-wsl-perf-legend-item {
align-items: center;
color: var(--jb-wsl-perf-muted);
display: inline-flex;
font-size: 13px;
gap: 7px;
line-height: 1.3;
}
.jb-wsl-perf-swatch {
background: var(--jb-wsl-perf-legacy);
border-radius: 2px;
display: inline-block;
height: 10px;
width: 10px;
}
.jb-wsl-perf-swatch-ijent { background: var(--jb-wsl-perf-accent); }
.jb-wsl-perf-rows { display: grid; gap: 22px; }
.jb-wsl-perf-row {
display: grid;
gap: 12px;
grid-template-columns: minmax(130px, .7fr) minmax(220px, 2fr);
min-width: 0;
}
.jb-wsl-perf-metric {
align-self: center;
color: var(--jb-wsl-perf-text);
font-size: 15px;
font-weight: 700;
line-height: 1.35;
}
.jb-wsl-perf-bars { display: grid; gap: 7px; min-width: 0; }
.jb-wsl-perf-barline {
align-items: center;
display: grid;
gap: 10px;
grid-template-columns: minmax(0, 1fr) 52px;
}
.jb-wsl-perf-track {
background: var(--jb-wsl-perf-track);
border-radius: 3px;
height: 13px;
overflow: hidden;
}
.jb-wsl-perf-fill {
background: var(--jb-wsl-perf-legacy);
border-radius: 3px;
display: block;
height: 100%;
}
.jb-wsl-perf-fill-ijent { background: var(--jb-wsl-perf-accent); }
.jb-wsl-perf-number {
color: var(--jb-wsl-perf-muted);
font-size: 13px;
font-variant-numeric: tabular-nums;
line-height: 1;
white-space: nowrap;
}
.jb-wsl-perf-delta {
color: #3a327d;
font-size: 12px;
font-weight: 700;
line-height: 1.3;
margin: 3px 0 0;
}
.jb-wsl-perf-notes {
background: var(--jb-wsl-perf-soft);
border-top: 1px solid var(--jb-wsl-perf-line);
display: grid;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 18px 28px 20px;
}
.jb-wsl-perf-note {
color: var(--jb-wsl-perf-muted);
font-size: 13px;
line-height: 1.5;
margin: 0;
}
.jb-wsl-perf-note strong { color: var(--jb-wsl-perf-text); }
@media (max-width: 640px) {
.jb-wsl-perf-header { grid-template-columns: 1fr; padding: 22px 18px 20px; }
.jb-wsl-perf-chart { padding: 21px 18px 23px; }
.jb-wsl-perf-row { gap: 8px; grid-template-columns: 1fr; }
.jb-wsl-perf-notes { grid-template-columns: 1fr; padding: 17px 18px 19px; }
}
@media (prefers-color-scheme: dark) {
.jb-wsl-perf-widget {
--jb-wsl-perf-text: #f4f4f7;
--jb-wsl-perf-muted: #b7b7bf;
--jb-wsl-perf-line: #45454d;
--jb-wsl-perf-soft: #29292e;
--jb-wsl-perf-track: #414148;
--jb-wsl-perf-legacy: #a4a4ac;
--jb-wsl-perf-accent: #9d8cff;
}
.jb-wsl-perf-panel { background: #202024; }
.jb-wsl-perf-delta { color: #c7bdff; }
}
</style>
<div class="jb-wsl-perf-panel">
<div class="jb-wsl-perf-header">
<div>
<p class="jb-wsl-perf-eyebrow">Cold-open benchmark</p>
<p class="jb-wsl-perf-title" id="jb-wsl-perf-title">IJent cuts 38% off the wait for a large WSL project</p>
</div>
</div>
<div class="jb-wsl-perf-chart" aria-label="Median duration in seconds: 9P compared with IJent">
<div class="jb-wsl-perf-legend" aria-label="Chart legend">
<span class="jb-wsl-perf-legend-item"><span class="jb-wsl-perf-swatch" aria-hidden="true"></span>9P</span>
<span class="jb-wsl-perf-legend-item"><span class="jb-wsl-perf-swatch jb-wsl-perf-swatch-ijent" aria-hidden="true"></span>IJent</span>
</div>
<div class="jb-wsl-perf-rows">
<div class="jb-wsl-perf-row">
<div class="jb-wsl-perf-metric">Ready to work</div>
<div class="jb-wsl-perf-bars">
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill" style="width:100%"></span></span><span class="jb-wsl-perf-number">18.5 s</span></div>
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill jb-wsl-perf-fill-ijent" style="width:62.2%"></span></span><span class="jb-wsl-perf-number">11.5 s</span></div>
<p class="jb-wsl-perf-delta">38% less</p>
</div>
</div>
<div class="jb-wsl-perf-row">
<div class="jb-wsl-perf-metric">Scan project tree</div>
<div class="jb-wsl-perf-bars">
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill" style="width:43.8%"></span></span><span class="jb-wsl-perf-number">8.1 s</span></div>
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill jb-wsl-perf-fill-ijent" style="width:20%"></span></span><span class="jb-wsl-perf-number">3.7 s</span></div>
<p class="jb-wsl-perf-delta">54% less</p>
</div>
</div>
<div class="jb-wsl-perf-row">
<div class="jb-wsl-perf-metric">Index files</div>
<div class="jb-wsl-perf-bars">
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill" style="width:58.4%"></span></span><span class="jb-wsl-perf-number">10.8 s</span></div>
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill jb-wsl-perf-fill-ijent" style="width:45.4%"></span></span><span class="jb-wsl-perf-number">8.4 s</span></div>
<p class="jb-wsl-perf-delta">22% less</p>
</div>
</div>
<div class="jb-wsl-perf-row">
<div class="jb-wsl-perf-metric">Read file content</div>
<div class="jb-wsl-perf-bars">
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill" style="width:65.9%"></span></span><span class="jb-wsl-perf-number">12.2 s</span></div>
<div class="jb-wsl-perf-barline"><span class="jb-wsl-perf-track"><span class="jb-wsl-perf-fill jb-wsl-perf-fill-ijent" style="width:30.8%"></span></span><span class="jb-wsl-perf-number">5.7 s</span></div>
<p class="jb-wsl-perf-delta">53% less</p>
</div>
</div>
</div>
</div>
<div class="jb-wsl-perf-notes">
<p class="jb-wsl-perf-note"><strong>Where this applies:</strong> A cold first open of <code>spring-framework</code>, with 23 subprojects and 8,191 source files. A small project with few source files showed no measurable difference.</p>
<p class="jb-wsl-perf-note"><strong>Test setup:</strong> Median of five measured runs on Windows 11 with WSL 2, Ubuntu 24.04, and IntelliJ IDEA Ultimate 263.SNAPSHOT.</p>
</div>
</div>
</section>
<p>Beyond performance, this architecture places development in WSL within the broader context of development in non-local environments, allowing them to be approached through the same underlying model. The same IJent agent already backs our work on Docker and Dev Containers.</p>
<p>Thank you for reading and for all the feedback that helped us reach this point. Tell us how <em>Native</em> mode holds up against your projects in the comments below.</p>
Generate an image of a stereotypical PHP developer - Exakat
https://www.exakat.io/?p=16392
2026-09-08T08:43:06.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/phpdev.medieval.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16401" src="https://www.exakat.io/wp-content/uploads/2026/09/phpdev.medieval.320-300x300.jpg" alt="Generate an image of a stereotypical PHP developer" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/phpdev.medieval.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/phpdev.medieval.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/phpdev.medieval.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/phpdev.medieval.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Generate an image of a stereotypical PHP developer</h1>
<p>That was a <a href="https://bsky.app/profile/justinjackson.ca/post/3ld5aep64nz2s">popular toot</a> on Bluesky, by <a href="https://bsky.app/profile/justinjackson.ca">Justin Jackson</a>.</p>
<p>I thought it would be interesting to make it again, in 2026, so here we go.</p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<h1 id="toc_1"><a href="https://chatgpt.com/">ChatGPT</a> in 2024</h1>
<p>The original image, below, was produced on ChatGPT. It is a cartoon, with quite a number of references to IRL memes: I <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;" /> PHP, the laptop, the cables and the coffee. The beard is already here.</p>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/original.webp"><img decoding="async" class="size-medium wp-image-16393 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/original-300x226.webp" alt="" width="300" height="226" srcset="https://www.exakat.io/wp-content/uploads/2026/09/original-300x226.webp 300w, https://www.exakat.io/wp-content/uploads/2026/09/original-500x377.webp 500w, https://www.exakat.io/wp-content/uploads/2026/09/original.webp 1000w, https://www.exakat.io/wp-content/uploads/2026/09/original-300x226@2x.webp 600w" sizes="(max-width: 300px) 100vw, 300px" /></a><a href="https://www.exakat.io/wp-content/uploads/2026/09/claude-scaled.png"><br />
</a></p>
<p> </p>
<p> </p>
<h1 id="toc_2"><a href="https://chatgpt.com/">ChatGPT</a> in 2026</h1>
<p> </p>
<p>Same prompt in 2026, and we get the same vibe (no pun intended). I <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;" /> PHP is gone, but coffee and laptops are still here. It is also a graphic picture, rather than a cartoon.</p>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT.png"><img decoding="async" class="size-medium wp-image-16399 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT-300x300.png" alt="" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT-500x500.png 500w, https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT.png 1000w, https://www.exakat.io/wp-content/uploads/2026/09/ChatGPT-300x300@2x.png 600w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
<p>“It works on my machine” meme appears, along with a todo list and books: they are actual titles!</p>
<p>Also, why is the screen behind the developer?</p>
<p>Note the poor elephpant, which is both an actual plush toy and has the PHP logo tattooed on its face!</p>
<p>Logos for PHP 8, Composer, Symfony and Laravel, with mentions of Docker, Linux, HTML, and Git. No comb in sight.</p>
<h1 id="toc_3"><a href="https://gemini.google.com/">Gemini</a></h1>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/Gemini.jpg"><img loading="lazy" decoding="async" class="size-medium wp-image-16398 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/Gemini-300x164.jpg" alt="" width="300" height="164" srcset="https://www.exakat.io/wp-content/uploads/2026/09/Gemini-300x164.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/Gemini-1024x559.jpg 1024w, https://www.exakat.io/wp-content/uploads/2026/09/Gemini-500x273.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/Gemini.jpg 1408w, https://www.exakat.io/wp-content/uploads/2026/09/Gemini-300x164@2x.jpg 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>Gemini also goes with a realistic picture, and a much more sanitized work environment. The developer is still on their laptop, but now correctly facing the screens. The book is used, and it looks like an old one, indeed.</p>
<p>Here, no more PHP elephpant, but Mario and Zelda. So much for the enterprise space…</p>
<h1 id="toc_4"><a href="https://grok.com/">Grok</a></h1>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/grok.jpg"><img loading="lazy" decoding="async" class="size-medium wp-image-16397 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/grok-201x300.jpg" alt="" width="201" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/grok-201x300.jpg 201w, https://www.exakat.io/wp-content/uploads/2026/09/grok-687x1024.jpg 687w, https://www.exakat.io/wp-content/uploads/2026/09/grok-500x745.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/grok.jpg 784w, https://www.exakat.io/wp-content/uploads/2026/09/grok-201x300@2x.jpg 402w" sizes="auto, (max-width: 201px) 100vw, 201px" /></a></p>
<p>The world is very gloomy with Grok. Errors and bugs, and despair in the eyes. Again, lots of books, with related technologies such as HTML, JavaScript.</p>
<p>No more laptop, but screens and coffee.</p>
<h1 id="toc_5"><a href="https://claude.ai/">Claude</a></h1>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/claude-scaled.png"><img loading="lazy" decoding="async" class="size-medium wp-image-16395 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/claude-300x209.png" alt="" width="300" height="209" srcset="https://www.exakat.io/wp-content/uploads/2026/09/claude-300x209.png 300w, https://www.exakat.io/wp-content/uploads/2026/09/claude-1024x712.png 1024w, https://www.exakat.io/wp-content/uploads/2026/09/claude-1536x1069.png 1536w, https://www.exakat.io/wp-content/uploads/2026/09/claude-2048x1425.png 2048w, https://www.exakat.io/wp-content/uploads/2026/09/claude-500x348.png 500w, https://www.exakat.io/wp-content/uploads/2026/09/claude-300x209@2x.png 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>Low effort from Claude, but it provided some explanations, leaving us to finish the job with our imagination.</p>
<p>“The joke is affectionate and aimed at the language’s reputation rather than at anyone in particular: the elePHPant plushie, mysql_query that should have been replaced fifteen years ago, and a TODO comment nobody has touched since 2009.</p>
<p>Worth saying that the stereotype is well out of date. Modern PHP has typed properties, JIT compilation, and frameworks like Laravel and Symfony that look nothing like the tangled 2009 code on that screen. Happy to redraw it as the “PHP 8 developer” version if you want the contrast.”</p>
<h1 id="toc_6"><a href="https://lumo.proton.me/u/7/">Lumo</a></h1>
<p> </p>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/lumo.jpg"><img loading="lazy" decoding="async" class="size-medium wp-image-16400 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/lumo-300x200.jpg" alt="" width="300" height="200" srcset="https://www.exakat.io/wp-content/uploads/2026/09/lumo-300x200.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/lumo-1024x683.jpg 1024w, https://www.exakat.io/wp-content/uploads/2026/09/lumo-500x333.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/lumo.jpg 1248w, https://www.exakat.io/wp-content/uploads/2026/09/lumo-300x200@2x.jpg 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>Lumo, from Protonmail, made the developer a lot younger than the previous pictures. He is without a beard, but he features a classic three hand posture.</p>
<p>The code on the screen is very indented, and this time, the coffee mug does not say it loves PHP.</p>
<h1 id="toc_7"><a href="https://chat.mistral.ai/">Mistral</a></h1>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/mistral.jpg"><img loading="lazy" decoding="async" class="size-medium wp-image-16394 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/mistral-300x225.jpg" alt="" width="300" height="225" srcset="https://www.exakat.io/wp-content/uploads/2026/09/mistral-300x225.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/mistral-500x375.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/mistral.jpg 1024w, https://www.exakat.io/wp-content/uploads/2026/09/mistral-300x225@2x.jpg 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>Mistral went also for the clean cartoon style. Here, we find the longest beard of all, and many, many PHP words. The keyboard is finally a real one, though it might be an ergonomic (or not) one.</p>
<p>Glasses and coffee are a constant.</p>
<p><a href="https://www.exakat.io/wp-content/uploads/2026/09/mistral.medieval.jpg"><img loading="lazy" decoding="async" class="size-medium wp-image-16396 aligncenter" src="https://www.exakat.io/wp-content/uploads/2026/09/mistral.medieval-300x225.jpg" alt="" width="300" height="225" srcset="https://www.exakat.io/wp-content/uploads/2026/09/mistral.medieval-300x225.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/mistral.medieval-500x375.jpg 500w, https://www.exakat.io/wp-content/uploads/2026/09/mistral.medieval.jpg 1024w, https://www.exakat.io/wp-content/uploads/2026/09/mistral.medieval-300x225@2x.jpg 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p> </p>
<p>The last try was to set a context for the picture, and I went… medieval. This works well: no screen, books, keyboard, glasses, or coffee in sight.</p>
<p>But still, this beard…</p>
<h1 id="toc_8">More sterotypical PHP developers?</h1>
<p>I tried some other LLM, such as z.ai, and deepseek.com, which have no image generation in the chat.</p>
<p>Could you generate other PHP developers pictures? <strong><a href="https://phpc.social/home">Share them</a></strong></p>
<p>The post <a href="https://www.exakat.io/generate-an-image-of-a-stereotypical-php-developer/">Generate an image of a stereotypical PHP developer</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
13 Modern PHP Tips & Tricks from 2015 - Exakat
https://www.exakat.io/?p=16388
2026-09-07T20:04:14.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/2015.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16389" src="https://www.exakat.io/wp-content/uploads/2026/09/2015.320-300x300.jpg" alt="13 Modern PHP Tips & Tricks from 2015" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/2015.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/2015.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/2015.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/2015.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>13 Modern PHP Tips & Tricks from 2015</h1>
<p>PHP has undergone a massive transformation over the last decade. If you’re still writing PHP like it’s 2015, you’re missing out on massive performance gains, cleaner syntax, and better security.</p>
<p>I ran into a nice blog post <a href="https://geeksprogramming.com/13-php-tips-tricks/">13 PHP Tips That Make Development Faster</a> from Linet M. that covered tips for PHP of that era: that was PHP 7.0 at that time. It is interesting to discover the way we have covered since that moment, so I read the whole piece. That was really good, and it also showed a bit of age. Probably like myself anyway.</p>
<p>Let’s take a modern look at the tips that still matter in 2026, the ones you need to forget, and the new tricks you should be using today.</p>
<h2 id="toc_1">🗑️ The “Dead and Buried” Tips</h2>
<p>Before we get to the good stuff, let’s clear out the obsolete advice from older blog posts:</p>
<ul>
<li><strong>“Use single quotes instead of double quotes for performance”</strong>. Modern PHP engines parse both equally fast. Use double quotes for string interpolation <code>("Hello {$name}")</code> and single quotes when you don’t need interpolation. Readability matters more than micro-optimizations.</li>
<li><strong>“Use <code>echo</code> instead of <code>print</code>“</strong>. First, nobody is using <code>print</code> anymore anyway, and the performance difference is non-existent in PHP 8.x.</li>
<li><strong>“Avoid <code>require_once</code> because it’s slow”</strong>. Everyone use <a href="https://php-dictionary.readthedocs.io/en/latest/index/composer.html">Composer</a> and PSR-4 <a href="https://php-dictionary.readthedocs.io/en/latest/index/autoload.html">autoloading</a> for classes now, so manual <code>require</code>/<code>require_once</code> for classes is indeed obsolete — though bootstrap files and front controllers still <code>require</code> things on purpose, even in fully Composer-driven apps.</li>
<li><strong>“Pre-increment <code>++$i</code> is faster than <code>$i++</code>“</strong>. The PHP engine optimized this years ago. Use whichever makes sense in terms of readability.</li>
<li><strong>“Suppress errors with <code>@</code>“</strong>. This one is actually harmful. Never do this, or in well monitored situations. Modern PHP prefers typed errors and exceptions over a silenced failure (though not everything became an exception in PHP 8 — plenty of <code>E_WARNING</code>/<code>E_DEPRECATED</code> notices still exist the old-fashioned way). Suppressing errors hides bugs and makes debugging a nightmare.</li>
</ul>
<h2 id="toc_2">✅ The Modern, Updated PHP Tips</h2>
<p>The numbers is the reference in the original article.</p>
<h3 id="toc_3">1. Always Use Strict Types</h3>
<p>In 2026, loosely typed PHP is a thing of the past. Always declare <a href="https://php-dictionary.readthedocs.io/en/latest/index/strict_types.html"><code>strict_types</code></a> at the very top of your PHP files. This forces PHP to enforce the types you declare in function signatures, preventing sneaky bugs caused by automatic type coercion.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
declare(strict_types=1);
function calculateTotal(int $price, float $tax): float {
return $price + ($price * $tax);
}
?>
</pre>
</div>
<h3 id="toc_4">2. Use <code>===</code> for Strict Comparison</h3>
<p>This was true a decade ago, and it’s true today. Use <a href="https://php-dictionary.readthedocs.io/en/latest/index/identity-comparison.html"><code>===</code></a> to check both value and type. PHP’s <code>==</code> (a.k.a. <a href="https://php-dictionary.readthedocs.io/en/latest/index/relaxed-comparison.html">relaxed comparison</a>) still has weird edge cases — <code>null == false</code> is still <code>true</code>, for one. The classic <code>0 == "a"</code> example, though, is no longer one of them: since PHP 8.0’s <a href="https://php-dictionary.readthedocs.io/en/latest/index/coercion.html">“saner string to number comparisons”</a>, a number is compared against a non-numeric string as a string, so <code>0 == "a"</code> is <code>false</code> today. That old chestnut belongs in the “dead and buried” pile above, not here — but the edge cases that remain are reason enough to keep using <code>===</code> and not leave it to chance.</p>
<h3 id="toc_5">3. Leverage the Nullsafe Operator (<code>?-></code>)</h3>
<p>Stop writing deeply nested <code>if</code> statements just to check if an object property exists. The <a href="https://php-dictionary.readthedocs.io/en/latest/index/nullsafe.html">nullsafe operator</a> allows you to chain method calls or property accesses. If any part of the chain evaluates to null, the entire chain short-circuits and returns null safely.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Old way (yuck)
$country = null;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
}
}
// 2026 way (clean)
$country = $user?->getAddress()?->getCountry();
?>
</pre>
</div>
<h3 id="toc_6">4. Use Enums Instead of “Magic Strings”</h3>
<p>Before PHP 8.1, developers used constants or “magic strings” to represent states. Now, use <a href="https://php-dictionary.readthedocs.io/en/latest/index/enum.html">Enums</a> — or, as here, a <a href="https://php-dictionary.readthedocs.io/en/latest/index/backed-enum.html">backed enum</a> when each case needs a scalar value attached. They are type-safe, readable, and prevent typos.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
enum OrderStatus: string {
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
}
function updateOrder(OrderStatus $status) {
// You are guaranteed to only receive a valid status here
}
?>
</pre>
</div>
<h3 id="toc_7">5. Use <code>match</code> Instead of <code>switch</code></h3>
<p>The <code>switch</code> statement is verbose and prone to bugs, like forgetting a <code>break</code>. The <a href="https://php-dictionary.readthedocs.io/en/latest/index/match.html"><code>match</code></a> expression is shorter, uses strict comparison <code>===</code>, returns a value directly, and throws an error if no condition is met.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Old way
switch ($status) {
case 'active':
$message = 'User is active';
break;
case 'inactive':
$message = 'User is inactive';
break;
default:
$message = 'Unknown';
}
// 2026 way
$message = match($status) {
'active' => 'User is active',
'inactive' => 'User is inactive',
default => 'Unknown',
};
?>
</pre>
</div>
<h3 id="toc_8">6. Stop Counting in Loops</h3>
<p>This tip survived the update amd it is worth repeating: Do not put function calls inside <a href="https://php-dictionary.readthedocs.io/en/latest/index/loop.html">loop</a> conditions.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Bad: count() is called on every single iteration
for ($i = 0; $i < count($array); $i++) { ... }
// Good: count() is called once
$count = count($array);
for ($i = 0; $i < $count; $i++) { ... }
?>
</pre>
</div>
<h3 id="toc_9">7. Use <code>str_contains</code>, <code>str_starts_with</code>, and <code>str_ends_with</code></h3>
<p>Stop using <code>strpos() !== false</code> to check if a string exists within another string. PHP 8 gave us dedicated, highly readable functions for this, starting with <a href="https://php-dictionary.readthedocs.io/en/latest/index/str_contains.html"><code>str_contains()</code></a>.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Old way
if (strpos($haystack, $needle) !== false) { ... }
// 2026 way
if (str_contains($haystack, $needle)) { ... }
?>
</pre>
</div>
<h3 id="toc_10">8. Use Readonly Properties and Classes</h3>
<p><a href="https://php-dictionary.readthedocs.io/en/latest/index/immutable.html">Immutability</a> is key to bug-free code. Use the <a href="https://php-dictionary.readthedocs.io/en/latest/index/readonly.html"><code>readonly</code></a> keyword for class properties that should only be initialized once. This is especially useful for <a href="https://php-dictionary.readthedocs.io/en/latest/index/dto.html">Data Transfer Objects (DTOs)</a> and <a href="https://php-dictionary.readthedocs.io/en/latest/index/value-object.html">value objects</a>.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class User {
public function __construct(
public readonly string $name,
public readonly string $email
) {}
}
$user = new User('Alice', 'alice@example.com');
$user->name = 'Bob'; // Throws an Error! Cannot modify readonly property.
?>
</pre>
</div>
<h3 id="toc_11">9. Take Advantage of Property Hooks</h3>
<p>If you are using PHP 8.4 or newer, you no longer need to write verbose <a href="https://php-dictionary.readthedocs.io/en/latest/index/getter.html">getters</a> and <a href="https://php-dictionary.readthedocs.io/en/latest/index/setter.html">setter</a> methods for every property. <a href="https://php-dictionary.readthedocs.io/en/latest/index/property-hook.html">Property hooks</a>allow you to inline access logic directly in the property declaration.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class Product {
public string $name {
get => strtoupper($this->name);
set => $value;
}
}
?>
</pre>
</div>
<h3 id="toc_12">10. Use Named Arguments</h3>
<p>You no longer need to pass <code>null</code> for optional parameters just to reach the one you actually want to change. Named arguments allow you to pass values by their parameter name — the real name from the signature, mind you, or PHP throws <code>Unknown named parameter</code>.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Old way
setcookie("user", "Alice", time() + 3600, "/", "", true, true);
// 2026 way
setcookie(
name: "user",
value: "Alice",
expires: time() + 3600,
httponly: true
);
?>
</pre>
</div>
<p>(The original draft named that third parameter <code>expires_or_options</code> — a plausible-looking guess, but <code>setcookie()</code>‘s actual parameter is <code>$expires</code>; the array-based overload’s parameter is <code>$options</code>, not a portmanteau of the two.)</p>
<h3 id="toc_13">11. Use PDO with Prepared Statements</h3>
<p>The golden rule of database interactions hasn’t changed: Never put user input directly into an SQL query. <a href="https://php-dictionary.readthedocs.io/en/latest/index/sql-injection.html">SQL injection</a> is still the #1 web vulnerability. Always use <a href="https://php-dictionary.readthedocs.io/en/latest/index/pdo.html">PDO</a> (or a modern <a href="https://php-dictionary.readthedocs.io/en/latest/index/orm.html">ORM</a> like Eloquent/Doctrine) with prepared statements.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Safe
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(['email' => $userInput]);
?>
</pre>
</div>
<p>You might also consider going one step further with an ORM. But that might be overkill for a PHP tips.</p>
<h3 id="toc_14">12. Use Arrow Functions for Simple Closures</h3>
<p>Writing multi-line <code>function() use ($var)</code> <a href="https://php-dictionary.readthedocs.io/en/latest/index/closure.html">closures</a> is clunky for simple array operations. <a href="https://php-dictionary.readthedocs.io/en/latest/index/arrow-function.html">Arrow functions</a> (<code>fn()</code>) automatically capture variables by value and allow one-line expressions.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
// Old way
$multiplied = array_map(function($n) use ($factor) {
return $n * $factor;
}, $numbers);
// 2026 way
$multiplied = array_map(fn($n) => $n * $factor, $numbers);
?>
</pre>
</div>
<h3 id="toc_15">13. Rely on OPcache and JIT</h3>
<p>Instead of spending hours trying to optimize string concatenation or array syntax, ensure your production server has <a href="https://php-dictionary.readthedocs.io/en/latest/index/opcache.html">OPcache</a> enabled. Furthermore, if you are running CPU-heavy applications, like math processing or image manipulation, enable <a href="https://php-dictionary.readthedocs.io/en/latest/index/jit.html">JIT</a> (Just-In-Time) compilation in your <code>php.ini</code>. This compiles PHP code into native machine code — worth knowing, though, that the “massive performance boosts” mostly show up on CPU-bound, numeric workloads; a typical request-response web app spends most of its time waiting on a database or the network, so don’t expect JIT alone to rescue a slow endpoint. It’s OPcache, not JIT, that does the heavy lifting for ordinary apps. Either way, both make old-school micro-optimizations irrelevant.</p>
<h2 id="toc_16">What a decade did to this list</h2>
<p>Going through it line by line, what strikes me most isn’t the new tips that appeared: it’s how little needed fixing at all. Thirteen tips, written for a PHP that was still years away from union types, enums, or a JIT, and eleven of them read today exactly as they did then, no asterisk required. <code>===</code> over <code>==</code>, prepared statements over string concatenation, don’t call <code>count()</code> from inside a loop condition: these aren’t PHP 7 tips that happened to survive, they’re just good engineering advice that PHP’s version number was never going to change. That kind of stability is rare, and it says something about the original instinct for which parts of a “tips” article age like the language and which parts age like a haircut.</p>
<p>The post <a href="https://www.exakat.io/13-modern-php-tips-tricks-from-2015/">13 Modern PHP Tips & Tricks from 2015</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Rebuilding Taipan! with an AI Pair Programmer in PHP - Exakat
https://www.exakat.io/?p=16383
2026-09-06T17:01:43.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/taipanicon128.png"><img fetchpriority="high" decoding="async" class="alignleft wp-image-16384 size-medium" src="https://www.exakat.io/wp-content/uploads/2026/09/taipanicon128-300x300.png" alt="Rebuilding Taipan! with an AI Pair Programmer in PHP" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/taipanicon128-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/09/taipanicon128-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/09/taipanicon128-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/09/taipanicon128.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Rebuilding Taipan! with an AI Pair Programmer in PHP</h1>
<p>Taipan! was one of the games that hooked me on computers. My brother and I would play it on the Apple ][c bitmap font and the original’s line-drawing art still aren’t ported: a reminder that “faithful clone” has a visual dimension too, and it’s the one still outstanding.</p>
<h2 id="toc_4">Dogfooding as the real division of labor</h2>
<p>The pattern that emerged: AI was fast and reliable at mechanical porting: reconciling sources, adding a new mechanic once specified, keeping the implementation clean enough to sail through static analysis at the strictest settings. What it couldn’t do was tell me the pirate encounter rate felt wrong, or that a status line was truncating, or that a fight was tedious rather than tense. That took playing.</p>
<p>This is dogfooding in miniature, and it’s a discipline that professional teams already know the value of. Using your own product is one of the most reliable ways to surface the problems a spec review never will. What this project made obvious is how much more that matters once AI is doing a large share of the implementation. The bottleneck didn’t disappear, it moved: from writing the code to actually using the thing you built.</p>
<h2 id="toc_5">Playing as work</h2>
<p>There’s a strange side effect to using play as your QA process: at some point, finding a bug stops being a happy accident and starts being the point of the session. You end up running two modes at once. Playing to enjoy the game, and playing to break it. And those two modes aim at different targets. That’s a slightly schizophrenic way to spend an evening with a game you loved as a kid, and it’s worth naming as a real cost of this workflow, not just a funny anecdote.</p>
<h2 id="toc_6">Where this could go with Taipan! 2026 on PHP?</h2>
<p>Where could this go next? First, it is playable, so get your PHP, <a href="https://github.com/dseguy/taipan">clone the repository</a> and run <code>php bin/taipan</code>. Have fun! Report bugs, submit PR, etc.</p>
<p>Some of it is easy and mostly cosmetic: more goods, or different ones to avoid explaining that Opium is the best good to trade at the next family dinner, more ports, more flavor, larger warehouses. I actually added a feature where the Hong Kong shipbuilding company offers to upgrade the warehouse by 10000 more unit. I can now buy enough Silk to have the entire production since prehistory in my warehouse. And sell it next month.</p>
<p>Some of it is more ambitious: a networked version where several Taipans trade in the same world at once, or an economy with real constraints instead of infinite cargo moving between randomly fluctuating prices. It is quite astounding to realize that the whole mechanics of the game was built on random numbers: for prices, for events, etc. Not a single real life impact of flooding a city with enough Arms to start a revolution.</p>
<p>There’s probably real room in the market for games in this register: simple, old-school shapes, but richer and more modern underneath than what 1982 hardware could support. Taipan! was built around the constraints of its era. Rebuilding it in 2026 is a chance to ask which of those constraints were the game, and which were just the Apple II.</p>
<p>The post <a href="https://www.exakat.io/rebuilding-taipan-with-an-ai-pair-programmer-in-php/">Rebuilding Taipan! with an AI Pair Programmer in PHP</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Post on Exakat - Exakat
https://www.exakat.io/?p=16376
2026-09-04T10:08:48.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/tools.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16377" src="https://www.exakat.io/wp-content/uploads/2026/09/tools.320-300x300.jpg" alt="I Have a Dream of a PHP Preprocessor" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/tools.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/tools.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/tools.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/tools.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>I Have a Dream of a PHP Preprocessor</h1>
<p>What if PHP could be faster, stricter, and more expressive, without waiting for PHP 9? What if we could preprocess PHP code into something more optimized, more elegant, or even a completely new dialect? The name itself hints at the possibility: <strong>PHP: Hypertext Preprocessor</strong>. But here, we’re imagining a preprocessor <em>for PHP itself</em>.</p>
<p>This is the dream of the PHP preprocessor: a tool to transform PHP code before execution, adding macros, type safety, or even a new syntax. The idea is to automate tightening at the source code level, so developers don’t have to do it manually. Over the years, at least five brave souls tried, and almost succeeded, in making this happen. Their attempts may not have changed the world, but they proved that innovation is always worth pursuing.</p>
<p>Let’s take a whimsical tour of their journeys, with real examples of what they aimed to achieve and where they ran into truly difficult problems. Then, let’s gaze into the crystal ball: what would a PHP preprocessor look like today?</p>
<h2 id="toc_1">Attempt #1: PHP-preprocessor by gizmore</h2>
<p><strong>Goal:</strong> Create a preprocessor that adds macros, compile-time evaluation, and even a new syntax layer to PHP.</p>
<p><a href="https://github.com/gizmore/php-preprocessor">Gizmore’s php-preprocessor, or PP</a>, was an ambitious attempt to give PHP compile-time superpowers. It promised:</p>
<ul>
<li><strong>Macros</strong> for repetitive code blocks.</li>
<li><strong>Compile-time evaluation</strong> of expressions.</li>
<li>A <strong>custom syntax</strong> for easier metaprogramming.</li>
</ul>
<p>For example, imagine writing a macro to auto-generate getters and setters:</p>
<pre class="brush: php; title: ; notranslate">
<?php
#[Getters(Author)]
class Author {
private $name;
private $email;
}
?>
</pre>
<p>This would be transformed into:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class Author {
private $name;
private $email;
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = $name; }
// ... and so on for each property
}
?>
</pre>
</div>
<p>The idea was to reduce boilerplate and make PHP feel more like a language with first-class metaprogramming. However, PHP’s dynamic nature made it hard to predict macro behavior: static analysis could be bypassed at runtime. The tool remained a fascinating experiment.</p>
<h2 id="toc_2">Attempt #2: PHP Preprocessor by ircmaxell</h2>
<p><strong>Goal:</strong> Create a preprocessor that adds compile-time optimizations, strict typing, and better error messages.</p>
<p>In 2016, <a href="https://github.com/ircmaxell/php-preprocessor">ircmaxell’s PHP Preprocessor</a> aimed to make PHP faster and safer by preprocessing code into optimized, strictly-typed versions. The goals included:</p>
<ul>
<li><strong>Enforcing types at compile time</strong> (e.g., ensuring a function only accepts integers).</li>
<li><strong>Optimizing loops and function calls</strong> based on static analysis.</li>
<li><strong>Adding better error messages</strong> for common mistakes.</li>
</ul>
<p>For example:</p>
<pre class="brush: php; title: ; notranslate">
<?php
function add(int $a, int $b) { return $a + $b; }
add(1, "2");
// Would throw a compile-time error: "Argument 2 must be of type int, string given."
?>
</pre>
<p>The preprocessor caught the error before runtime, making PHP feel more like a statically-typed language. Ultimately, the complexity of integrating this into existing PHP 7 projects proved to be a major hurdle.</p>
<h2 id="toc_3">Attempt #3: PHP Plus by stevenkellow</h2>
<p><strong>Goal:</strong> Create a stricter, more modern dialect of PHP, with better type handling and syntax.</p>
<p><a href="https://github.com/stevenkellow/PHP-Plus">PHP Plus</a> was designed as a stricter, more modern version of PHP. It introduced:</p>
<ul>
<li><strong>Stricter type coercion</strong> for better predictability.</li>
<li><strong>A new syntax</strong> for arrays and objects.</li>
<li><strong>Better error handling</strong> for undefined variables.</li>
</ul>
<p>For example, PHP Plus aimed to allow:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$a = [1, 2, 3];
$b = $a[10]; // Would throw a warning instead of returning null.
?>
</pre>
</div>
<p>And for functions:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function greet(string $name) {
echo "Hello, $name!";
}
greet("World"); // Standard PHP
greet(123); // Would throw a warning: "Argument must be of type string."
?>
</pre>
</div>
<p>The project started in 2016, and version 2 was released in the last days of August 2026. It may be time to take a closer look.</p>
<h2 id="toc_4">Attempt #4: PHP+ Proposal (InfoWorld, 2011)</h2>
<p><strong>Goal:</strong> Create a new, stricter dialect of PHP called PHP++. In 2011.</p>
<p>InfoWorld’s <a href="https://www.infoworld.com/article/2262496/php-plus-p-proposal-would-create-a-stricter-dialect.html">2011 article</a> proposed PHP+ (or PHP++) as a stricter dialect, with: – <strong>Stricter type checking</strong>(no more silent type juggling). – <strong>A cleaner syntax</strong> for arrays and strings. – <strong>Better error handling</strong> for undefined variables and functions.</p>
<p>For example, PHP+ would:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$a = "123" + "456"; // Would throw an error: "Cannot add string to string."
?>
</pre>
</div>
<p>And:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
function process(array $data) {
// Would enforce that $data is an array at compile time.
}
?>
</pre>
</div>
<p>The proposal was bold at the time of writing, and it remained mostly a thought experiment. It included early assessments from Zeev Suraski about feasibility and ended without support from the PHP Group. Interestingly, many of its ideas were later adopted in PHP 7 and 8: strict operators, typed variables, and more.</p>
<p>Again, being stricter about coding is an important aspect of a preprocessor.</p>
<h2 id="toc_5">Attempt #5: Plus-1 by Nuno Maduro</h2>
<p><strong>Goal:</strong> A preprocessing tool that adds modern features (like arrow functions, typed properties) to older PHP versions.</p>
<p><a href="https://github.com/nunomaduro/plus-1">Nuno Maduro’s Plus-1</a> was a transpiler for older PHP versions. It aimed to: – <strong>Add arrow functions</strong> to PHP 5.6+. – <strong>Support typed properties</strong> before PHP 7.4. – <strong>Enable modern syntax</strong> for older projects.</p>
<p>For example, you could write:<code>php $getUsers = fn(array $filters) => User::where($filters)->get(); </code>And have it automatically transformed into:<code>php function ($filters) { return User::where($filters)->get(); } </code>Plus-1 was a pragmatic way to bridge the gap between PHP versions: use modern PHP syntax on older PHP versions and transpile it to the target version. PHP’s rapid evolution made it obsolete.</p>
<p>There might also be a philosophical argument: instead of adapting modern PHP features to older engines, why not adapt older codebases to modern PHP features? That would help with PHP migration toward newer versions. That might be a consideration when preparing new PHP versions: what abandoned PHP features from previous versions can be transpiled to a new PHP version?</p>
<h2 id="toc_6">So… What’s Relevant Today?</h2>
<p>All five attempts eventually stopped. It’s not a failure, though there are no actual successes either. They didn’t stop because the idea was bad. They stopped because PHP itself evolved. The PHP core team added many of the features these tools aimed to provide: strict typing, arrow functions, typed properties, and even JIT compilation.</p>
<p>So, why build a preprocessor in 2026? What is left to experiment with?</p>
<p>Because innovation isn’t about winning: it’s about experimenting.</p>
<p>A modern PHP preprocessor in 2026 could focus on:</p>
<ul>
<li><strong>Generics</strong>: Of course. This feature is still in high demand for PHP. With a preprocessor, you could simulate them today. And then, you could go further with <a href="https://en.wikipedia.org/wiki/Dependent_type">dependent types</a>.</li>
<li><strong>Simplicity</strong>: Making PHP more like its early, forgiving self, even in stricter contexts. You have a better view of what should and shouldn’t be done: can we encode that in the syntax?</li>
<li><strong>Leaner and fitter</strong>: Reduce boilerplate by offloading it to the preprocessor. No need to write getters, setters, and withers: one attribute and let the engine produce it. Less code for you to read, and for AI too!</li>
<li><strong>Extra strict</strong>: You like strict code? Go all in with mathematically provable code that cannot fail, right from the start. It might go against business rules that need to be flexible and sometimes fuzzy, so where is the middle ground?</li>
<li><strong>Validation</strong>: Try new approaches before they get an RFC and into the language itself, like experimental syntax, macros, or standardization of function names (<code>str_len()</code> and <code>str_join()</code>, anyone?). Just compare PHP to other languages and bring in more ideas!</li>
<li><strong>Operators</strong>: Add more operators with strange Unicode symbols (≣ ≩ ≶ ⊂ ⊗ ⊞), include imaginary numbers, tetration, Roman numerals, and quaternions… OK, you get the point.</li>
</ul>
<h2>The Lesson: Innovation is Always Worth It</h2>
<p>The attempts to build a PHP preprocessor may have faded into history, but their spirit of experimentation lives on. Innovation isn’t about creating the next big thing; it’s about pushing boundaries, trying new ideas, and learning from failure. Sometimes, you have to learn what you <em>can’t</em>do, because what’s left is what you <em>can</em> do.</p>
<p>The post <a href="https://www.exakat.io/16376-2/"></a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Recently updated PIE extensions #5 (since August 27th, 2026) - Exakat
https://www.exakat.io/?p=16374
2026-09-03T15:01:43.000Z
Exakat
<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="PHP Pie updates #5" 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 #5 (since August 27th, 2026)</h2>
<p>27 PHP Pie extensions were updated.</p>
<ul>
<li><a href="https://packagist.org/packages/iliaal/mdparser">iliaal/mdparser</a> (0.6.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/iliaal/php-excel">iliaal/php-excel</a> (2.7.0): PHP extension for reading and writing Excel files using LibXL</li>
<li><a href="https://packagist.org/packages/mongodb/mongodb-extension">mongodb/mongodb-extension</a> (2.5.0): MongoDB driver extension</li>
<li><a href="https://packagist.org/packages/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a> (v1.16.1): Pre-compiled tree-sitter grammars for 371 programming languages</li>
<li><a href="https://packagist.org/packages/xberg-io/liter-llm">xberg-io/liter-llm</a> (v1.19.2): Universal LLM API client with Rust-powered polyglot bindings.</li>
<li><a href="https://packagist.org/packages/xberg-io/html-to-markdown">xberg-io/html-to-markdown</a> (v3.12.0): High-performance HTML to Markdown converter</li>
<li><a href="https://packagist.org/packages/iliaal/phonetic">iliaal/phonetic</a> (0.4.1): 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> (1.5.5.8): PHP PDO Driver for DuckDB, modern analytics</li>
<li><a href="https://packagist.org/packages/grpc/grpc-php-ext">grpc/grpc-php-ext</a> (v1.83.1): gRPC PHP Extension</li>
<li><a href="https://packagist.org/packages/iliaal/pdo_duckdb">iliaal/pdo_duckdb</a> (0.6.0): PDO driver for DuckDB, the in-process analytical database.</li>
<li><a href="https://packagist.org/packages/extport/grpc">extport/grpc</a> (1.83.1): Unofficial PIE-compatible mirror of grpc/grpc</li>
<li><a href="https://packagist.org/packages/extport/protobuf">extport/protobuf</a> (36.1): Unofficial PIE-compatible mirror of protocolbuffers/protobuf</li>
<li><a href="https://packagist.org/packages/folk-project/ext-folk">folk-project/ext-folk</a> (0.2.16): Folk PHP application server extension — pre-built binaries with all plugins (http, jobs, grpc, metrics, process)</li>
<li><a href="https://packagist.org/packages/iliaal/phpser">iliaal/phpser</a> (0.6.1): 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/fastchart">iliaal/fastchart</a> (1.7.1): Native C PHP extension for fast chart rendering: 38 chart families (line, area, bar, pie, scatter, bubble, stock with technical indicators, radar, polar, surface, contour, treemap, funnel, waterfall, heatmap, gauge, linear meter, gantt, box plot, bullet, pareto, calendar heatmap, sunburst, sankey, marimekko, vector, arc diagram, chord diagram, network, population pyramid, violin plot, circle packing, pictogram, venn diagram, word cloud, serpentine timeline, dendrogram, partition) plus a 2-class Symbol family (Code128, QrCode). SVG-canonical pipeline rasterized via vendored plutovg + plutosvg; PNG / JPEG / WebP encoders via libpng / libjpeg-turbo / libwebp.</li>
<li><a href="https://packagist.org/packages/phpstan/turbo">phpstan/turbo</a> (2.2.12): Native acceleration extension for PHPStan</li>
<li><a href="https://packagist.org/packages/kjdev/lz4">kjdev/lz4</a> (0.7.1): A compression/decompression with LZ4</li>
<li><a href="https://packagist.org/packages/laruence/yac">laruence/yac</a> (2.4.1): Yac is a shared and lockless memory user data cache for PHP.</li>
<li><a href="https://packagist.org/packages/laruence/yar">laruence/yar</a> (dev-master): Light, concurrent RPC framework for PHP</li>
<li><a href="https://packagist.org/packages/hosmelq/ext-anydoc">hosmelq/ext-anydoc</a> (v0.2.4): PHP extension for converting documents to GitHub-Flavored Markdown with Firecrawl anydoc.</li>
<li><a href="https://packagist.org/packages/xberg-io/crawlberg">xberg-io/crawlberg</a> (v1.5.1): High-performance web crawling engine</li>
<li><a href="https://packagist.org/packages/php-io-extensions/appkit">php-io-extensions/appkit</a> (0.8.0.x-dev): AppKit bound 1:1 into PHP (Zephir extension, macOS only)</li>
<li><a href="https://packagist.org/packages/goopil/rabbit-rs-native">goopil/rabbit-rs-native</a> (v0.0.9): 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.5.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/xqkeji/xqkeji">xqkeji/xqkeji</a> (dev-main): xqkeji low-code development framework PHP extension</li>
<li><a href="https://packagist.org/packages/win32service/win32service">win32service/win32service</a> (1.1.1beta19): 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/jbboehr/php-yumemi">jbboehr/php-yumemi</a> (dev-master): Native extension that adds operators and unit-expression parsing to yumemi.php.</li>
</ul>
<p>The post <a href="https://www.exakat.io/recently-updated-pie-extensions-5-since-august-27th-2026/">Recently updated PIE extensions #5 (since August 27th, 2026)</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Tree-sitter and PHP: Two Directions, Endless Possibilities - Exakat
https://www.exakat.io/?p=16368
2026-09-03T10:35:49.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320.png"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16369" src="https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320-300x300.png" alt="Tree-sitter and PHP: Two Directions, Endless Possibilities" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320-150x150@2x.png 300w, https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320-150x150.png 150w, https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320-100x100.png 100w, https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320.png 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Tree-sitter and PHP: Two Directions, Endless Possibilities</h1>
<p><a href="https://tree-sitter.github.io/tree-sitter/">Tree-sitter</a> is a very popular library in the computer ecosystem at large. It powers syntax highlighting in Neovim, structural search in VS Code, and a growing wave of static analysis tools, including in PHP environment. What makes it compelling is a combination of properties rarely found together: a full concrete syntax tree or CST, incremental re-parsing on every update, and first-class bindings for languages as different as Rust and PHP.</p>
<p>This post explores two distinct ways PHP and tree-sitter interact. Both pull in opposite directions.</p>
<ul>
<li><strong>From Rust, parsing PHP</strong>: building fast PHP developer tools in Rust, with tree-sitter doing the heavy lifting of understanding PHP source code.</li>
<li><strong>From PHP, parsing everything else</strong>: using tree-sitter as a library inside PHP to parse other languages such as JavaScript, Python, JSON, or any of the 100+ supported languages.</li>
</ul>
<h2 id="toc_1">Building PHP Tools in Rust with Tree-sitter</h2>
<h3 id="toc_2">Why Rust?</h3>
<p>PHP’s ecosystem of static analysis, linting, and code formatting tools is mature, but it has a structural problem: they run on the PHP runtime itself. A large scan can take tens of seconds. Sometimes, developers reach for <code>--parallel</code> flags just to get bearable CI times.</p>
<p>Rust sidesteps the issue entirely. A native binary has no interpreter startup, no garbage collector pauses, and can spread work across all available cores without coordinating through a VM. The only thing still needed is a way to understand PHP code: that is exactly what the <code>tree-sitter-php</code>crate provides.</p>
<h3 id="toc_3">The <code>tree-sitter-php</code> crate</h3>
<p>The official grammar lives at <a href="https://github.com/tree-sitter/tree-sitter-php">tree-sitter/tree-sitter-php</a>. It ships bindings for Node.js, Python, Go, Swift and Rust via <a href="https://crates.io/crates/tree-sitter-php">crates.io/crates/tree-sitter-php</a>.</p>
<p>In Rust, integrating it is minimal:</p>
<div>
<pre>
use tree_sitter::Parser;fn main() {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_php::LANGUAGE_PHP.into()).unwrap();let source = r#"<!--?php echo "Hello, tree-sitter!"; ?-->"#;
let tree = parser.parse(source, None).unwrap();
let root = tree.root_node();println!("{}", root.to_sexp()); // prints the S-expression CST
}
</pre>
</div>
<p>The resulting tree contains every token, including whitespace and comments: useful for a formatter that must be lossless.</p>
<h3 id="toc_4">Mago: The Flagship Example</h3>
<p>The most ambitious PHP tool currently built on this stack is <a href="https://github.com/carthage-software/mago"><strong>Mago</strong></a>, by Carthage Software.</p>
<p>Mago is a full PHP toolchain in a single Rust binary: linter, formatter, static analyzer, and architectural guard. Its feature surface overlaps with PHPStan, Psalm, PHP-CS-Fixer, exakat and PHPCS: all at once, without having to run four separate PHP processes, and in a fraction of the execution time. Just make sure your favorite rule is actually included.</p>
<p>The performance numbers are striking. On a 500 files project, <code>mago check</code> completes in under a second. PHPCS on the same project takes 8–12 seconds. On a 2400 files project the gap widens to 30 or 40 times.</p>
<p>Mago does not use tree-sitter directly for its deepest analysis: it ships its own handwritten fault-tolerant parser for more control over error recovery. This demonstrates exactly the category of tool that tree-sitter enables: fast, multi-pass PHP analysis written entirely outside the PHP runtime.</p>
<p>Interestingly, Mago is now in its version 1.4 (August 2026), with versions coming out every other week. Quite impressive for a new tool.</p>
<h3 id="toc_5">ast-grep: Structural Search Across 100+ Languages</h3>
<p><a href="https://github.com/ast-grep/ast-grep"><strong>ast-grep</strong></a>, or <code>sg</code> for the initiated, is a Rust CLI that uses tree-sitter to power structural code search and rewriting. Think <code>grep</code>, but pattern-matched against the AST rather than raw text.</p>
<div>
<pre># Find every call to shell_exec() in a PHP codebase
sg --lang php -p 'shell_exec($CMD)'
</pre>
</div>
<p>Because the pattern matches the tree, it finds the call regardless of whitespace, line breaks, or irrelevant surrounding tokens. It supports over 26 languages out of the box, PHP included, making it a practical replacement for fragile regex-based code searches in CI pipelines.</p>
<h3 id="toc_6">php-rust-tools/parser</h3>
<p>Worth mentioning: <a href="https://github.com/php-rust-tools/parser">php-rust-tools/parser</a> is a handwritten, fault-tolerant, recursive-descent PHP parser in Rust. It is not a tree-sitter dependency. It is the foundation for several emerging Rust-based PHP tools and offers finer-grained control over error recovery than a grammar-generated parser. Not tree-sitter, but part of the same wave of Rust-native PHP tooling.</p>
<h2 id="toc_7">Using Tree-sitter from Within PHP</h2>
<p>The reverse direction is less obvious but equally interesting: bring tree-sitter into PHP as an extension, and use it to parse other languages from PHP code.</p>
<h3 id="toc_8">Why Would You Want This?</h3>
<ul>
<li>Build a custom linter for a domain-specific language without leaving PHP</li>
<li>Analyze JavaScript, TypeScript, or Python files from a PHP application</li>
<li>Extract function signatures, imports, or dependency graphs from a polyglot codebase</li>
<li>Write automated codemods that operate on non-PHP source files</li>
<li>Write a transpiler that convert languages to PHP source code</li>
</ul>
<p>The most useful feature is to write a custom linter: tree-sitter can work from manual written grammar, that you can tailor to your format needs. From the grammar, the engine gets the tree for you.</p>
<p>PHP’s own tokenizer <code>token_get_all</code> only speaks PHP. Tree-sitter speaks everything. Why not build static analysis tools for other languages in PHP? After all, static analysis tools for PHP in other languages do exist.</p>
<h3 id="toc_9">tree-sitter-language-pack: Batteries-Included, Polyglot</h3>
<p>The most complete option is <a href="https://github.com/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a>, a native PHP extension, built in Rust via <a href="https://github.com/extphprs/ext-php-rs"><code>ext-php-rs</code></a>, not hand-written C, that ships pre-compiled grammars for 371 languages (PHP, HTML, CSS, JavaScript/JSX, TypeScript/TSX, JSON, Python, F#, R, Fortran, …) behind an API shared with the project’s Rust, Python, Node.js, Go, Java, C#, Ruby, and Elixir bindings. Parsers are downloaded on first use and cached locally, so installing the package doesn’t pull all 371 grammars onto disk at once. It ships pre-packaged binaries and is PIE-compatible, so no local Rust toolchain is required. Note: the project was previously published as <code>kreuzberg-dev/tree-sitter-language-pack</code>; the org renamed to xberg-io in 2026 and the old GitHub URL now redirects: it’s the same package, not a competing one.</p>
<div>
<pre>composer require xberg-io/tree-sitter-language-pack
</pre>
</div>
<p>The API is clean and OOP:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
use Tree\Sitter\Language\Pack\Parser;
$parser = Parser::new();
$parser->setLanguage('php');
$tree = $parser->parse('<?php echo "Hello" . "world"; ');
$root = $tree->rootNode();
echo $root->kind() . "\n"; // object... actually "document" at the root
echo $root->childCount() . "\n";
echo $root->toSexp() . "\n";
</pre>
</div>
<p>This gives the following result:</p>
<div>
<pre>>program
2
(program (php_tag) (echo_statement (binary_expression left: (encapsed_string (string_content)) right: (encapsed_string (string_content)))))
</pre>
</div>
<p>Tree-sitter never throws on malformed input: it parses through the error and marks the offending region as an <code>ERROR</code> node instead. Check for it explicitly, so you can easily validate code in any of the supported languages:</p>
<div>
<pre class="brush: php; title: ; notranslate">
$invalidPHPcode = '<?php echo "Hello" "world"; '; // malformed on purpose
$tree = $parser->parse($invalidPHPcode);
if ($tree->rootNode()->hasError()) {
echo "Syntax error detected\n";
} else {
echo "Valid syntax\n";
}
echo "\n--- positional data ---\n";
$start = $root->startPosition();
$end = $root->endPosition();
echo "start: row {$start->getRow()}, column {$start->getColumn()}\n";
echo "end: row {$end->getRow()}, column {$end->getColumn()}\n";
</pre>
</div>
<p>This shows the following result:</p>
<div>
<pre>--- error detection ---
Syntax error detected</pre>
<p>— positional data —<br />
start: row 0, column 0<br />
end: row 0, column 30</p>
</div>
<p>Every node exposes positional data: <code>startPosition()</code> and <code>endPosition()</code> return a <code>Point</code>with <code>row</code> and <code>column</code>. This makes it straightforward to produce diagnostic messages with precise line numbers.</p>
<p>Side note: on Mac OSX, I had to install it with PHP 8.4, not 8.5.</p>
<h3 id="toc_10">Traversing the Tree: A Practical Example</h3>
<p>Suppose you want to extract every <code>import</code> statement from a TypeScript file. With tree-sitter inside PHP, the walk is explicit but mechanical:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
use Tree\Sitter\Language\Pack\Parser;
use Tree\Sitter\Language\Pack\Node;
$parser = Parser::new();
$parser->setLanguage('typescript');
$source = file_get_contents($argv[1] ?? __DIR__ . '/app.ts');
$tree = $parser->parse($source);
function findImports(Node $node, string $source): void {
if ($node->kind() === 'import_statement') {
$start = $node->startByte();
$end = $node->endByte();
echo substr($source, $start, $end - $start) . "\n";
}
for ($i = 0; $i < $node->childCount(); $i++) {
findImports($node->child($i), $source);
}
}
findImports($tree->rootNode(), $source);
</pre>
</div>
<p>No regex, no fragile string manipulation. And the tree is structurally sound.</p>
<h3 id="toc_11">Other Binding Options</h3>
<p>If you prefer not to install a native extension, one alternative exists:</p>
<p><a href="https://github.com/talbergs/php-tree-sitter">talbergs/php-tree-sitter</a>: FFI-based bindings, valid with PHP 8.0+ and <code>ext-ffi</code>. Pure PHP installation via Composer, loads the tree-sitter shared library at runtime. Lower performance ceiling than a native extension, but zero build step.</p>
<h2 id="toc_12">The Performance Caveat: Tree-sitter vs <code>token_get_all</code> for PHP</h2>
<p>Tree-sitter is impressively fast: it was designed to re-parse large files on every keystroke in an editor. But when your target language is PHP, and all you need is a flat token stream, PHP’s native <code>token_get_all()</code>, or its object-oriented successor <code>PhpToken::getAll()</code> introduced in PHP 8.0, will beat tree-sitter consistently.</p>
<p>The reason is structural: <code>token_get_all</code> is a thin wrapper around the same C lexer PHP uses to execute your code. It runs in-process with zero FFI overhead, produces a flat array rather than a tree, and does no grammar inference. For straightforward tokenization: counting strings, scanning for function names, detecting encoding declarations. This is unbeatable.</p>
<p>Tree-sitter’s value for PHP analysis lies in the tree, not the tokens. If you need to understand scope, resolve variable references, detect unreachable branches, or navigate the structural relationship between a method call and its arguments, the CST pays for itself. For a flat scan, it does not.</p>
<p>A rough rule of thumb from the field: tree-sitter PHP parsing runs about 2–5× slower than <code>token_get_all</code> on the same source. The gap narrows on incremental re-parses (tree-sitter’s speciality), but for a single-pass bulk scan of a codebase, <code>token_get_all</code> / <code>PhpToken::getAll()</code> is still the PHP-native winner.</p>
<h2 id="toc_13">Summary</h2>
<table>
<thead>
<tr>
<th>Direction</th>
<th>Approach</th>
<th>Key Tools</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rust to PHP</td>
<td>Parse PHP in Rust via tree-sitter</td>
<td><code>tree-sitter-php</code> crate, Mago, ast-grep</td>
</tr>
<tr>
<td>PHP to Everything</td>
<td>Parse any language inside PHP</td>
<td>xberg-io/tree-sitter-language-pack, talbergs/php-tree-sitter, ext-treesitter</td>
</tr>
</tbody>
</table>
<p>Tree-sitter sits at an unusual intersection: it is fast enough for editors, expressive enough for static analysis, and portable enough to be embedded almost anywhere. The PHP ecosystem is discovering this from both ends simultaneously. Rust tools are consuming PHP grammars to build the next generation of blazing-fast developer tooling, while PHP itself is gaining the ability to understand every other language in the stack.</p>
<p>Whether you are writing a PHP quality tool in Rust or analysing your JavaScript bundle from a PHP script, tree-sitter is the parsing layer worth knowing.</p>
<p><em>Sources and further reading:</em></p>
<ul>
<li><a href="https://github.com/carthage-software/mago">carthage-software/mago</a>: Mago PHP toolchain</li>
<li><a href="https://github.com/tree-sitter/tree-sitter-php">tree-sitter/tree-sitter-php</a>: official PHP grammar</li>
<li><a href="https://github.com/soulseekah/ext-treesitter">soulseekah/ext-treesitter</a>: PHP C extension</li>
<li><a href="https://github.com/talbergs/php-tree-sitter">talbergs/php-tree-sitter</a>: FFI bindings</li>
<li><a href="https://github.com/xberg-io/tree-sitter-language-pack">xberg-io/tree-sitter-language-pack</a>: 371 grammars for PHP (formerly published as kreuzberg-dev/tree-sitter-language-pack)</li>
<li><a href="https://github.com/ast-grep/ast-grep">ast-grep/ast-grep</a>: structural code search in Rust</li>
<li><a href="https://github.com/php-rust-tools/parser">php-rust-tools/parser</a>: handwritten Rust PHP parser</li>
</ul>
<p>The post <a href="https://www.exakat.io/tree-sitter-and-php-two-directions-endless-possibilities/">Tree-sitter and PHP: Two Directions, Endless Possibilities</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Adding the last types to PHP code in 2026 - Exakat
https://www.exakat.io/?p=16364
2026-09-03T10:01:52.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2022/08/summit.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-14067" src="https://www.exakat.io/wp-content/uploads/2022/08/summit.320-300x300.jpg" alt="Adding the last types to PHP code" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2022/08/summit.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2022/08/summit.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2022/08/summit.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2022/08/summit.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>Adding the last types to PHP code</h1>
<blockquote><p><strong>2026 update note:</strong> This is an update of the <strong><a href="https://www.exakat.io/adding-the-last-types-to-php-code-in-2026/">original 2022 article</a></strong>, with annotations added locally to flag what’s changed in the PHP language since: mainly PHP 8.4’s property hooks and asymmetric visibility, which give native answers to several problems the article works around by hand.</p></blockquote>
<h1 id="toc_1">Adding the last types to PHP code</h1>
<p>Adding types to PHP code is a staple of any PHP code refactoring. With a code base crawling towards 10 years and over 2000 classes, Exakat is quite a consistent piece of software, with a lot of parameters, methods and properties. Covering everything with types was long, and, at time, tortuous.</p>
<p>I have to admit, it also feels like 100% test coverage. Somewhere, between 90 to 95% of type coverage was the confortable spot. Yet, going beyond that level required a lot more effort, and at the same time less spectacular rewards. The challenges it posed were interesting, so I wanted to share them and the adopted solutions.</p>
<p>Some of the issues revolved around adding invisible types, handling the default values, managing temporary values, deciding when to use union types, wrestling with resources and juggling with cache mechanisms. Let’s go!</p>
<h2 id="toc_2">At first, typing is very easy</h2>
<p>Before the last leg of the journey, there was the first leg : obviously. It was the easy part, and, in the retrospect, possibly the least useful. Typing code that already works well doesn’t change anything : it keeps working well.</p>
<p>Adding the first types consists in looking at the best maintained part of the source, and saying : yep, I know this is a string or an array, or an object of type <code>A</code>, for sure. And then, adding the type. And of course, there are very little mistakes at that stage.</p>
<p>There are multiple ways to guess the correct type : knowing the code and its intent, incoming argument, returntype of methods, PHP native support, usage of methods and properties, comparison to literals, usage with operators, type propagation,… I have already made several conferences about automated typing, where the code can be typed with very little human intervention. Even, Exakat is capable of guessing and adding such types to any code.</p>
<p>During this phase, the types flow naturally, and rarely lead to any surprise error. The code was under control, and adding the type doesn’t really change anything. A rough estimate is that until 2/3 of typing coverage, there is no real challenge, nor, any rewards. A.k.a., so special case is discovered.</p>
<p>In fact, I suspect that any ambiguous decision about types was inconsciously set aside for later. The obvious cases are quickly added, and any hesitation leaves the property untyped. When the coverage is still 32%, and there are still several thousands of them to add, no one notices an untyped property.</p>
<h2 id="toc_3">Then, came the hard phase</h2>
<p>When the easy types started to become rarer, the harder to decide types had to be added. This stage covers situations that were not obvious. Sometimes, it took running the tests and some examples to see a fatal error emerge. Trust in the type system was now crucial : adding a type could mean an error later.</p>
<h2 id="toc_4">Null is not a default value anymore</h2>
<p>This one is very easy to understand, and, for some reason, it kept coming back. It really looks like fighting an stubborn habit.</p>
<p>Look at the code below : there is an obvious type for the argument, which later is used to call a method. Adding the type <code>A</code> is a no brainer.</p>
<p>Now, let’s see the same situation for a property, which later is used to call that same method. Adding the type <code>A</code> is also a no brainer. But there is a catch.</p>
<p>When adding <code>null</code> as default value to a parameter, the <code>null</code> type is also silently added to the type. In the first code, it is possible to call <code>foo</code> without argument, with a <code>null</code> or an <code>A</code> object. Even when the type is not explicitely nullable.</p>
<p>When adding a type to a property, <code>null</code>is not automatically added. It has to be explicitely done. Which means that the default value cannot be <code>null</code>.</p>
<p>To be helpful, PHP checks the default value compatibility for properties and arguments at compilation time, so the feedback is quite fast. The repeated errors where definitely the sign of a change of habit.</p>
<h2 id="toc_5">Removing the default value to keep type single</h2>
<p>The first solution is to remove the default value altogether. In particular, when the property is assigned at constructor time, there is no need to provide a default value.</p>
<p>Note that when moving the property to the promoted properties, that property becomes a parameter, and, as such, the hidden <code>null</code> type may apply too.</p>
<h2 id="toc_6">Cache mechanism with null</h2>
<p>Now, removing the <code>null</code> type is not always possible. In particular, when the default value is needed to set up a caching, or lazy loading mecanism.</p>
<p>Here, the constructor will not initialize the property, and the object simply waits for the method to be called once. Then, the argument is cached, and later, reused.</p>
<p>The default value <code>null</code> is used to detect the empty cache, so it is needed. This is a case where the nullable type is useful.</p>
<p>One obvious solution is to add the nullable type to the property. This turns the type is a union type. Indeed, it is a type <code>A</code> or <code>null</code>. In a sense, union types where available before PHP 8.0, but just for <code>null</code>.</p>
<h2 id="toc_7">Property hooks come with superpower</h2>
<p>PHP 8.4 added <strong>property hooks</strong>, which give this exact pattern a native, non-nullable public surface. The backing storage can still be an two-headed type <code>?A</code>, but the hook getter may only return one.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class Consumer
{
private ?A $aCache = null;
public A $a {
get => $this->aCache ??= $this->buildDefaultA();
}
}
?>
</pre>
</div>
<p>Callers of <code>$consumer->a</code> never see a nullable type: the union-type-for-<code>null</code> compromise described below becomes an implementation detail rather than part of the public contract.</p>
<p>This also applies to the setter, which may accept more types than advertized at the property level.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class Consumer
{
private ?A $aCache = null;
public A $a {
set(?A $a) => $this->a = $a ?? $this->buildDefaultA();
}
}
?>
</pre>
</div>
<p>Setting <code>$consumer->a</code> accepts the <code>null</code>, but it is never set. Later, this reduces the number of tests when dealing with that property.</p>
<h2 id="toc_8">NullPattern alternative</h2>
<p>An alternative to the usage of <code>null</code> is to create an <a href="https://en.wikipedia.org/wiki/Null_object_pattern">Null Object Pattern</a> for the <code>A</code>class, and to use it for default detection.</p>
<p>This approach prevents the code to using the <code>null</code> type : only an <code>A</code> class is needed. It may be tested with <code>instanceof</code>, and created anywhere, thanks to the parameterless constructor.</p>
<p>On the other hand, it forces the creation of an extra empty class. This class acts as a simple placeholder, and uses more memory than a single <code>null</code>. Also, an extra class introduces extra code, albeit a very simple one.</p>
<p>This extra class will also prevent usage for the <code>final</code> keyword with the class <code>A</code>, since <code>A</code> class now has a child. And also, every parameter with the <code>A</code> type now needs to check for <code>NullA</code>, or face mayhem.</p>
<h2 id="toc_9">Null or null class?</h2>
<p>The <code>nullable</code> type is a lot easier, and it gives actual value to the <code>null</code> value (sic). The <code>Null</code>class allows for single typing the property : it comes with some rare edges cases, and adds extra coding to the source. So far, we opted for the nullable version, with its less surprising Fatal Errors.</p>
<p>As of PHP 8.4, there’s a third option beyond “nullable” vs. “Null Object Pattern”: a property hook with a single-typed public property and a nullable private backing field (see the note above). It avoids the extra class the Null Object Pattern requires, while keeping the public type non-nullable: arguably the better default now for the specific cache/lazy-init case this section is about. The Null Object Pattern remains the right call when you need <code>instanceof</code>-testable “empty” objects passed around as values, not just lazy-computed properties.</p>
<h2 id="toc_10">Temporary values in properties</h2>
<p>Let’s go back to property typing. We already mentioned that they behave differently than arguments for the type of the default value. They also enforce the type at each step of the life of the property, which means that the property cannot handle temporary values of different types anymore.</p>
<p>This is a concern when acquiring data from sources that needs validation. Here is an example:</p>
<p>Even after testing the incoming variable <code>x</code> as a non-zero positive integer, values in <code>$_GET</code> are <code>string</code>. This will conflict with the early assignation of <code>$i</code>to <code>$this->int</code>.</p>
<p>With this example, the solution is simple : rewrite some of the <code>$this->i</code> with <code>$i</code>. This is sufficient.</p>
<p>In other situations, the temporary value stays a lot longer before being processed into its final form. Then, the type system forbid that the property hold something else than the expected type, even for a short time.</p>
<p>That is one of the most interesting error to catch: it literally cleans the code and makes it a lot more robust.</p>
<p>Property hooks also give a place to put the normalization step instead of scattering casts across every call site. A <code>set</code> hook runs on assignment and can accept a looser input type, cast it, and store the validated final type:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
class Row
{
public int $id {
set(int|string $value) => (int) $value;
}
}
?>
</pre>
</div>
<p>This doesn’t remove the underlying tension the article describes — the property still can’t silently hold a different type at rest — but it centralizes the cast in one declared place instead of at every assignment site, which is closer to what the “rewrite <code>$this->i</code> to <code>$i</code>” workaround below was trying to approximate by hand.</p>
<h2 id="toc_11">More casting values in properties</h2>
<p>For scalar types, the simplest processing of the temporary value might be a type cast. This would be the case here :</p>
<p>This happens a lot with decoded values from JSON or YAML, or incoming values from the Web, (<code>$_GET</code>, <code>$_POST</code>, …). This is definitely a side effect of typing with scalars, and it doesn’t happen so much when typing with classes.</p>
<h2 id="toc_12">Unveiling hidden types</h2>
<p>Adding class types to properties had a minor annoyance : adding the type in the list of use expressions. In typeless code, it was completely hidden. Now, those types are needed. Look at this code :</p>
<p>Obviously, the property <code>$sqlite3</code> will now wear a nice <code>Sqlite3</code> type : this will avantageously replace the <a href="https://exakat.readthedocs.io/en/latest/Reference/Rules.html#semantic-typing">typing by naming</a> previous convention. The important part here is to not forget the <code>use Sqlite3</code> line at the top of the file, since the namespace is not the global one.</p>
<p>Nothing spectacular, and very easy to detect… when one is paying attention. Otherwise, it is easy to add <code>Sqlite3</code> as a type, and get an <code>Unknown class MyNamespace\Sqlite3</code>for that file. Believe me, it is as obvious as it is easy to forget and curse about it. Also, using an IDE was helpful.</p>
<p>In the end, this process makes internally used classes appear in the use list of use expression. This list of <code>use</code> is now getting even bigger than previously, and it is turning into a list of dependencies for the class. This might be useful.</p>
<p>Another situation that make those types appear is with return types. In the example below, a factory is declared with the expected type. Until now, this return type was not explicitely used in this code. It was hidden.</p>
<h2 id="toc_13">resource is not a type</h2>
<p>This leads to the impossible to type case : <code>resource</code>. This is a special and soft reserved keyword : PHP has reserved it, but it is not enforcing it (yet). There is no way to type anything as a <code>resource</code>. So, we have this situation:</p>
<p>The solution for this specific case was provided by <a href="https://twitter.com/TimB0nd">Tim Bond</a> : use <a href="https://www.php.net/manual/en/class.splfileobject.php">SplFileObject</a>. This handy little class uses a OOP syntax and exposes all the classic functions, such as fwrite(), fgets(), etc. as methods. There is only a classic <code>remove the resource and make it a method</code>rewrite.</p>
<p>The call to fopen() is now an instantiation. Note that we also forgot the <code>use SplFileObject</code> to make this run : I told you it was easy to forget!</p>
<p>And now, the class is typed.</p>
<blockquote><p><strong>2026 update:</strong> <code>resource</code> is still not a declarable type as of PHP 8.6 (in beta, targeting Nov 2026) — no RFC has changed this since the article was written. The <code>SplFileObject</code>workaround remains the best available advice for file handles specifically.</p></blockquote>
<h2 id="toc_14">Not all resources are ready</h2>
<p>This trick doesn’t work with <code>stream_socket_server()</code>, which also returns a resource. That resource has no direct alternative, although there might be some solution when looking at the <a href="https://www.php.net/manual/en/book.sockets.php">Sockets</a>extension. Who knows, since there is already a dedicated <code>Socket</code> class. If you have experience with this, please give us a shout!</p>
<p>This is still the state of things: the <code>ext-sockets</code> <code>Socket</code> class covers socket resources, but there’s no general-purpose typed replacement for arbitrary <code>resource</code> values. For example, from <code>curl_init()</code>, which itself moved to a <code>CurlHandle</code> object in PHP 8.0, quietly fixing this exact complaint for that one function without a language-level fix for <code>resource</code> as a concept.</p>
<h2 id="toc_15">Conclusion</h2>
<p>The journey to 99.9% typing was longer than expected, and it revealed some light traps :</p>
<ul>
<li>invisible types</li>
<li>Casting more than before</li>
<li>handling the default values</li>
<li>managing temporary values</li>
<li>wrestling with resources</li>
<li>juggling with cache mechanisms</li>
</ul>
<p>Initially, it is easy to postpone typing and only focus on the easy one. Later, typing gets harder, and let some unclean code situation emerge. Dectecting them, thanks to tests, and fixing them along the way is the best way to go.</p>
<p>In case of unsolvable typing situation, leaving the property, parameter or return type empty, or <code>mixed</code>is a good strategy. The practise shows that such situation tends to simplify itself, by adding the other types in the code. So, just let the hard one on the side, and come back to them later : tightening the code elsewhere do help.</p>
<p>Happy PHP code auditing!</p>
<p>The post <a href="https://www.exakat.io/adding-the-last-types-to-php-code-in-2026/">Adding the last types to PHP code in 2026</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
setlocale() and Its Six Personalities - Exakat
https://www.exakat.io/?p=16361
2026-09-02T14:28:23.000Z
Exakat
<figure id="attachment_16362" aria-describedby="caption-attachment-16362" style="width: 300px" class="wp-caption alignleft"><a href="https://www.exakat.io/wp-content/uploads/2026/09/local.320.jpg"><img fetchpriority="high" decoding="async" class="wp-image-16362 size-medium" src="https://www.exakat.io/wp-content/uploads/2026/09/local.320-300x300.jpg" alt="setlocale() and Its Six Personalities" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/local.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/local.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/local.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/local.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a><figcaption id="caption-attachment-16362" class="wp-caption-text">setlocale() and Its Six Personalities</figcaption></figure>
<h1 id="toc_0">setlocale() and Its Six Personalities</h1>
<p>What does <code>setlocale()</code> actually do? It changes the date format, right? It’s a fair guess, and it’s wrong in an interesting way: <code>setlocale()</code> doesn’t touch one behavior, it touches six, and most of them have nothing to do with dates. Under the hood it’s a thin wrapper around the C library’s <code>setlocale(3)</code>, which means PHP inherited not just the feature but the entire category structure glibc invented for it in the 1990s. It is a <a href="https://php-dictionary.readthedocs.io/en/latest/index/global-state.html">global state</a> that happily outlives the request that set it, if your SAPI happens to keep the process around.</p>
<p>Six <code>LC_*</code> categories, one call. <code>LC_ALL</code> sets them all at once, which is convenient right up until the moment it silently reaches into a category you forgot existed.</p>
<h2 id="toc_1">Six categories, one entry point</h2>
<table>
<thead>
<tr>
<th>Constant</th>
<th>Controls</th>
<th>Key functions</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>LC_COLLATE</code></td>
<td>String comparison order</td>
<td><code>strcoll()</code>, <code>usort()</code></td>
</tr>
<tr>
<td><code>LC_CTYPE</code></td>
<td>Character classification, case conversion</td>
<td><a href="https://php-dictionary.readthedocs.io/en/latest/index/strtoupper.html"><code>strtoupper()</code></a>, <a href="https://php-dictionary.readthedocs.io/en/latest/index/strtolower.html"><code>strtolower()</code></a>, <code>ctype_*</code></td>
</tr>
<tr>
<td><code>LC_MONETARY</code></td>
<td>Currency formatting</td>
<td><code>localeconv()</code>, <code>NumberFormatter</code></td>
</tr>
<tr>
<td><code>LC_NUMERIC</code></td>
<td>Decimal point, digit grouping</td>
<td><code>localeconv()</code></td>
</tr>
<tr>
<td><code>LC_TIME</code></td>
<td>Date and time formatting</td>
<td><code>strftime()</code> <em>(deprecated)</em></td>
</tr>
<tr>
<td><code>LC_MESSAGES</code></td>
<td>Translated system responses</td>
<td><code>gettext()</code>, <code>_()</code></td>
</tr>
</tbody>
</table>
<p><code>LC_ALL</code> sets every row in that table with one call. That’s the whole design tension of <code>setlocale()</code> in one sentence: it’s grouped by what glibc happened to ship together in 1993, not by what a PHP developer would actually want to change together. Wanting French date formatting has nothing to do with wanting French decimal separators, but <code>LC_ALL</code> will hand you both whether you asked or not.</p>
<p>Note that is the source of a classic bugs, where calling <code>setlocale()</code> with <code>LC_ALL</code> has far reaching impacts that break the rest of the application. Nothing like changing the date format to Spanish, and see the sorting of customer names go awry. Connecting theses dots is quite a challenge.</p>
<h2 id="toc_2">LC_COLLATE: string comparison order</h2>
<p><code>strcoll()</code> orders strings the way a human from that culture would alphabetize them, not the way their byte values happen to sort. In English that’s mostly invisible. In Swedish, <code>ö</code> sorts <em>after</em> <code>z</code>. In German, <code>ä</code> behaves like <code>ae</code> for collation purposes. Your “alphabetical” sort was only ever alphabetical for one alphabet.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$cities = ['Örebro', 'Stockholm', 'Uppsala', 'Åre'];
setlocale(LC_COLLATE, 'en_US.UTF-8');
usort($cities, 'strcoll');
// ['Stockholm', 'Uppsala', 'Åre', 'Örebro']
setlocale(LC_COLLATE, 'sv_SE.UTF-8');
usort($cities, 'strcoll');
// ['Åre', 'Örebro', 'Stockholm', 'Uppsala']
?>
</pre>
</div>
<p>Same array, same <code>usort()</code> call, same <code>strcoll</code> callback, two completely different orderings, because the only thing that changed is a global the sort function silently consults. If a user ever files a bug titled “sorting is wrong” and can’t say <em>how</em>, check the locale before you check the algorithm.</p>
<table>
<thead>
<tr>
<th>Rank</th>
<th>C (byte order)</th>
<th>de_DE</th>
<th>sv_SE</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>a</td>
<td>a</td>
<td>a</td>
</tr>
<tr>
<td>2</td>
<td>ä</td>
<td>o</td>
<td>o</td>
</tr>
<tr>
<td>3</td>
<td>o</td>
<td>z</td>
<td>z</td>
</tr>
<tr>
<td>4</td>
<td>ö</td>
<td>ä <em>(sorts as “ae”)</em></td>
<td>ö <em>(after z)</em></td>
</tr>
<tr>
<td>5</td>
<td>z</td>
<td>ö <em>(sorts as “oe”)</em></td>
<td>ä <em>(after ö)</em></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="toc_3">LC_CTYPE — character classification and conversion</h2>
<p><code>strtoupper()</code> and <code>strtolower()</code> don’t have a fixed idea of what “uppercase” means — they ask the current <code>LC_CTYPE</code>locale. Under the plain <code>C</code> locale, anything outside ASCII is invisible to them; accented letters pass through untouched. Give PHP an actual language locale, and accented characters finally get treated as letters instead of noise.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$text = 'café über naïve';
setlocale(LC_CTYPE, 'C');
echo strtoupper($text);
// CAFé üBER NäIVE
setlocale(LC_CTYPE, 'fr_FR.UTF-8');
echo strtoupper($text);
// CAFÉ UBER NAÏVE
?>
</pre>
</div>
<table>
<thead>
<tr>
<th>Input</th>
<th>C locale</th>
<th>fr_FR locale</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>é</code></td>
<td><code>é</code> <em>(unchanged)</em></td>
<td><code>É</code></td>
</tr>
<tr>
<td><code>ü</code></td>
<td><code>ü</code> <em>(unchanged)</em></td>
<td><code>Ü</code></td>
</tr>
<tr>
<td><code>ï</code></td>
<td><code>ï</code> <em>(unchanged)</em></td>
<td><code>Ï</code></td>
</tr>
<tr>
<td><code>c</code></td>
<td><code>C</code></td>
<td><code>C</code></td>
</tr>
</tbody>
</table>
<p>And then there’s Turkish, which every locale-aware string function eventually has to apologize for. Under <code>tr_TR</code>, uppercasing <code>"i"</code>produces <code>"İ"</code> (dotted capital I), not <code>"I"</code>, and lowercasing <code>"I"</code> produces <code>"ı"</code> (dotless), not <code>"i"</code>. This is entirely correct Turkish orthography and entirely fatal to any code that assumed <code>strtolower($username) === strtolower($input)</code> was a safe way to compare identifiers. Somewhere there is a login form that only breaks for users named İbrahim, and nobody on the team can reproduce it from their desk in Amsterdam.</p>
<h2 id="toc_4">LC_MONETARY: currency formatting is out</h2>
<p>This is the section for the older code bases. <code>money_format()</code>, the function every <code>LC_MONETARY</code> tutorial reaches for, was deprecated in PHP 7.4 and removed outright in PHP 8.0. If you’re running anything newer, the function simply doesn’t exist. You can skip this section.</p>
<p>If you stay with me, I have to let you know <code>setlocale(LC_MONETARY, ...)</code> still works, but nothing in the engine uses that configuration anymore. In particular, <code>number_format()</code> is not affected, because it doesn’t have any currency symbol.</p>
<p>We can recommend using the <a href="https://github.com/brick/money">brick/money</a> component, or read <a href="https://www.koladechris.com/blog/how-to-format-currencies-in-php">PHP Number Format Currency – How to Format Currencies in PHP</a>.</p>
<p>The most accessible replacement is <code>NumberFormatter</code> and it isn’t locale-dependent at all. It works with the <code>intl</code> extension, which takes a locale as an explicit argument instead of an ambient global.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$amount = 1234567.89;
$fr = new NumberFormatter('fr_FR', NumberFormatter::CURRENCY);
echo $fr->formatCurrency($amount, 'EUR');
// 1 234 567,89 €
$de = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $de->formatCurrency($amount, 'EUR');
// 1.234.567,89 €
?>
</pre>
</div>
<table>
<thead>
<tr>
<th>Locale</th>
<th>Formatted output</th>
</tr>
</thead>
<tbody>
<tr>
<td>en_US</td>
<td>$1,234,567.89</td>
</tr>
<tr>
<td>de_DE</td>
<td>1.234.567,89 €</td>
</tr>
<tr>
<td>fr_FR</td>
<td>1 234 567,89 €</td>
</tr>
<tr>
<td>ja_JP</td>
<td>¥1,234,568</td>
</tr>
<tr>
<td>hi_IN</td>
<td>₹12,34,567.89 <em>(Indian digit grouping)</em></td>
</tr>
</tbody>
</table>
<p>Notice what changed structurally, not just cosmetically: <code>NumberFormatter</code> takes the locale as a constructor argument. <code>setlocale()</code> doesn’t have an argument for “which call this applies to”: it applies to the whole process until you call it again. That difference is the entire second half of this article.</p>
<h2 id="toc_5">LC_NUMERIC: the one that bites the old</h2>
<p><code>LC_NUMERIC</code> swaps the decimal point and the digit-grouping separator: <code>1,234.56</code> in the US becomes <code>1.234,56</code> in Germany. Reasonable enough for display.</p>
<p>The localisation used to happen whenever a number had to be cast to a string, until PHP 8.0. Before that, the conversion string to integer did not agree with the conversion integer to string.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
echo (float) (string) 3.14; // 3.14
setlocale(LC_ALL, 'de_DE');
echo (float) (string) 3.14; // 3
?>
</pre>
</div>
<p>Since PHP 8.0, all conversion are locale independent, so the above doesn’t happen anymore. In fact, the only leftover of that era is the <code>%F</code> format of the <code>*printf()</code> family.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
setlocale(LC_ALL, 'de_DE');
printf("%.2f", 3.14); // 3,14
print PHP_EOL;
printf("%.2F", 3.14); // 3.14
?>
</pre>
</div>
<p>So, the trap is that PHP’s own number parsing doesn’t get the memo: <code>floatval()</code>, <code>(float)</code> casts, and JSON encoding/decoding all assume a period, always, regardless of locale.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
setlocale(LC_NUMERIC, 'de_DE.UTF-8');
$lc = localeconv();
echo $lc['decimal_point']; // ","
echo $lc['thousands_sep']; // "."
// This now quietly breaks:
$val = floatval("3,14"); // 3.0 — not 3.14
?>
</pre>
</div>
<p><code>floatval()</code> stops reading at the first non-numeric character. It doesn’t consult <code>LC_NUMERIC</code> to figure out what “the decimal point” means this week; it only ever knows <code>.</code>. So the moment you set a European locale, <code>"3,14"</code> doesn’t parse as 3.14 — it parses as <code>3</code>, followed by a comma it discards. No warning, no exception, just a number that’s wrong by a factor that depends on where the comma was.</p>
<p>This is the trap worth memorizing about <code>LC_ALL</code>: calling it with any European locale drags <code>LC_NUMERIC</code> along for the ride even when all you wanted was French month names. If you must set <code>LC_ALL</code>, immediately pin <code>LC_NUMERIC</code> back to <code>C</code>:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
setlocale(LC_ALL, 'fr_FR.UTF-8');
setlocale(LC_NUMERIC, 'C'); // undo the part that breaks floatval()
?>
</pre>
</div>
<p>And as a side note, please, upgrade to PHP 8.3 and more recent.</p>
<h2 id="toc_6">LC_TIME: and the dates too</h2>
<p><code>strftime()</code> reads <code>LC_TIME</code> for month names, weekday names, and AM/PM markers.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$ts = strtotime('2025-03-15 14:30');
setlocale(LC_TIME, 'fr_FR.UTF-8');
echo strftime('%A %d %B %Y', $ts);
// samedi 15 mars 2025
?>
</pre>
</div>
<table>
<thead>
<tr>
<th>Locale</th>
<th><code>strftime('%A, %B %d, %Y')</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>en_US</td>
<td>Saturday, March 15, 2025</td>
</tr>
<tr>
<td>fr_FR</td>
<td>samedi 15 mars 2025</td>
</tr>
<tr>
<td>de_DE</td>
<td>Samstag, 15. März 2025</td>
</tr>
<tr>
<td>ja_JP</td>
<td>土曜日, 3月15日, 2025</td>
</tr>
<tr>
<td>ar_SA</td>
<td>السبت، 15 مارس 2025 <em>(right-to-left)</em></td>
</tr>
</tbody>
</table>
<p><code>strftime()</code> was deprecated in PHP 8.1 and is scheduled to leave core entirely, following the exact path <code>money_format()</code>already walked. The replacement, <code>IntlDateFormatter</code>, again takes its locale as a constructor argument rather than reading an ambient global — the same pattern <code>NumberFormatter</code> uses, which is not a coincidence. Every <code>LC_*</code>-dependent function the <code>intl</code> extension has replaced has been replaced with one that stops trusting <code>setlocale()</code>.</p>
<h2 id="toc_7">LC_MESSAGES: translations, when gettext is here</h2>
<p><code>gettext()</code> looks up translated strings in <code>.mo</code> catalogs, keyed by whatever <code>LC_MESSAGES</code> currently says. This is the one category that isn’t really about formatting at all: it’s entirely internationalization, or i18n. It’s also the one most likely to simply not be there: it only works if PHP was built <code>--with-gettext</code>. And if you don’t have a distinct system for translations.</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
bindtextdomain('myapp', './locales');
textdomain('myapp');
setlocale(LC_MESSAGES, 'fr_FR.UTF-8');
echo _('Welcome!');
// Bienvenue !
?>
</pre>
</div>
<ol>
<li><code>setlocale(LC_MESSAGES, 'fr_FR.UTF-8')</code> sets the lookup locale.</li>
<li><code>_('Welcome!')</code> triggers a lookup by msgid.</li>
<li>PHP searches <code>./locales/fr_FR/LC_MESSAGES/myapp.mo</code>.</li>
<li>It returns the translation — or silently falls back to the original English string if the catalog or the entry is missing.</li>
</ol>
<p>That silent fallback is the sting: a missing <code>.mo</code> file doesn’t error, it just serves English to a French user and waits for someone to notice. Always guard with <code>function_exists('gettext')</code> before relying on this category; it’s the one part of <code>setlocale()</code> that can be entirely absent depending on how PHP was compiled, not just how it was configured.</p>
<h2 id="toc_8">important points about setlocale() and its constants</h2>
<p><strong>They’re not six independent settings: they’re six views onto one process-wide value</strong>. <code>setlocale()</code> doesn’t take a scope, a request ID, or a callback context. It mutates the same C global every other request in the same process will read next, which is invisible on classic PHP-FPM (one process, one request, the state dies with the process) and very much not invisible on anything that keeps a worker alive across requests: I’m looking at you, RoadRunner, Swoole, FrankenPHP, and the others.</p>
<p>Set <code>LC_NUMERIC</code> to <code>de_DE</code> on request #4012 of a long-lived worker and forget to reset it, and request #4013 inherits a decimal comma it never asked for. It’s the same category of bug as leftover <a href="https://php-dictionary.readthedocs.io/en/latest/index/superglobal.html">superglobal</a> state in a coroutine worker, just for a global almost nobody thinks to audit.</p>
<p><strong>The <code>intl</code> extension isn’t a replacement for the same thing: it’s the industry quietly admitting it could do something even more complex</strong>. <code>NumberFormatter</code> and <code>IntlDateFormatter</code> both take the locale as an explicit constructor argument. That single change, locale as a parameter instead of ambient global state, is why they can run correctly, one call after another, in the exact long-lived worker that makes <code>setlocale()</code> unsafe. Every function <code>intl</code> has replaced, like <code>money_format()</code>, and soon <code>strftime()</code>, was replaced with this pattern specifically.</p>
<p><strong>Almost nobody restores what they changed</strong>. <code>setlocale()</code> returns the previous setting for exactly this reason: <code>$old = setlocale(LC_ALL, 0)</code> followed by a restore at the end of the function is the correct pattern: in fact, I can see <code>#[NoDiscard]</code> usage for this value, if it was not breaking so many codes.</p>
<p>Almost no code in the wild capture it and reuses it, because a locale bug doesn’t crash anything. It just makes a French invoice look slightly american state unionist, or a sort order look slightly wrong, or a float silently lose its fractional part. Nothing loud enough to fail a test suite; plenty loud enough to fail an audit six months later.</p>
<h2 id="toc_9">The bigger picture</h2>
<p><code>setlocale()</code> is a 1990s C API that PHP exposed more or less verbatim, and for twenty years that was a reasonable trade: one process per request meant global mutable state cost you nothing, because it never survived long enough to leak anywhere. That assumption is the part that’s aged badly, not the function itself. The <a href="https://php-dictionary.readthedocs.io/en/latest/index/locale.html">locale</a> categories are exactly as coherent as they were in 1993; it’s the execution model around them that quietly stopped matching.</p>
<p>Every long-lived-worker runtime PHP has grown in the last few years inherits this function completely unchanged, along with its assumption that nobody’s still using it three requests later. Auditing a codebase for stray <code>setlocale()</code> calls that never get restored is a fairly cheap static check to write. Whether anyone runs it before switching their app to a worker-based SAPI is a different question entirely.</p>
<p>The post <a href="https://www.exakat.io/setlocale-and-its-six-personalities/">setlocale() and Its Six Personalities</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
The Clock That Cracks Passwords - Exakat
https://www.exakat.io/?p=16349
2026-09-01T15:44:26.000Z
Exakat
<h1 id="toc_0"><a href="https://www.exakat.io/wp-content/uploads/2026/09/thread.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft size-medium wp-image-16350" src="https://www.exakat.io/wp-content/uploads/2026/09/thread.320-300x300.jpg" alt="The Clock That Cracks Passwords" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2026/09/thread.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2026/09/thread.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2026/09/thread.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2026/09/thread.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>The Clock That Cracks Passwords</h1>
<p>To understand what is the clock that cracks passwords, let’s start with a quizz. Which security flaw does the following PHP login check contains?</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
if ($userPassword == $storedSecret) {
grantAccess($user);
}
?>
</pre>
</div>
<p>If you said “the password should be hashed with bcrypt”, you are right and you may stay. If you said “use <code>===</code> instead”, you are wrong and we will get to why later. If you stared at it for thirty seconds and saw nothing, you are in the majority. The bug passes a code review, satisfies a static analyser, and returns the correct answer every single time.</p>
<p>It just returns that answer at slightly different speeds, depending on how close the guess was.</p>
<p>The bug is <code>==</code>. Not because it lies. Because it talks too much.</p>
<h2 id="toc_1">The Lie Inside ==</h2>
<p>PHP compares strings character by character, left to right, and stops the moment it finds a mismatch. This is the efficient method: no need to continue comparing once a mismatch is found. For ordinary work, it is fine. For secrets, it is a slow-motion catastrophe.</p>
<p>Compare the three most common ways to check whether two strings are equal:</p>
<div>
<pre class="brush: php; title: ; notranslate">
<?php
$guess == $secret // exits at the first mismatched byte
$guess === $secret // type-check first, then the same early exit
hash_equals($secret, $guess) // walks every byte, no shortcuts, no hurry
?>
</pre>
</div>
<p><code>hash_equals()</code> arrived in PHP 5.6, introduced specifically for this problem. It compares two strings and returns <code>true</code> or <code>false</code>, but it never stops early. Whether the very first byte mismatches or only the very last byte mismatches, the function takes the same amount of time. It has a poker face.</p>
<p><code>===</code> is often presented as the safe upgrade from <code>==</code>. It is a safer operator in general: it avoids the type-coercion surprises that trip up many PHP developers. But for secret comparison it is no better. It does the type check, then walks the bytes and exits at the first difference. Two distinct problems, regularly confused.</p>
<p>The insight at the heart of this post is a simple one: the <em>duration</em> of a comparison is information. And in security, information is currency.</p>
<h2 id="toc_2">Cracking a Password, One Nanosecond at a Time</h2>
<p>Here is the attack, stated as plainly as possible.</p>
<p>A server checks whether <code>$guess == $secret</code>. The secret is <code>supersecret123</code>, fourteen characters. The attacker does not know the secret. The attacker does know that the server is using an early-exit comparison, and that the server can be queried repeatedly.</p>
<p>So the attacker observes:</p>
<ul>
<li><code>xxxxxxxxxxxxxx</code> is rejected very quickly. The <code>x</code> mismatches at position 0 and the comparison halts.</li>
<li><code>sxxxxxxxxxxxxx</code> takes slightly longer. The <code>s</code> matches position 0. The <code>x</code> mismatches at position 1 and only then does the comparison halt.</li>
<li><code>suxxxxxxxxxxxx</code> takes slightly longer still. Two matching characters, two loop iterations before exit.</li>
</ul>
<p>Each extra matching prefix byte adds one more step. Each step takes a few nanoseconds. The attacker measures those nanoseconds and reads the story they tell.</p>
<p>The algorithm that follows is almost embarrassingly simple:</p>
<ol>
<li>Fix position 0. Try every character in the charset. Keep the one that produces the <em>slowest rejection</em>. That is the correct first character.</li>
<li>Fix that character. Move to position 1. Try every character. Keep the slowest.</li>
<li>Repeat until the response says “access granted” instead of “access denied”.</li>
</ol>
<p>A small digression on the word “oracle”: in cryptography, an oracle is any system that answers yes-or-no questions about a secret. The word is borrowed from antiquity. The oracle at Delphi was famous for appearing to say very little while actually saying quite a lot. The resemblance is apt.</p>
<p>The mathematical payoff of this oracle is dramatic. For a fourteen-character password drawn from lowercase letters and digits, a brute-force search requires at most 36^14 guesses, which is roughly 4.8 × 10²¹. That number is approximately the number of grains of sand on Earth, multiplied by ten. The timing oracle reduces the search to at most 36 × 14 guesses: 504. The difference is not a small improvement. It is the difference between impossible and before lunch.</p>
<p>The <code>attack.php</code> script from this post’s repository makes the oracle visible:</p>
<div>
<pre class="brush: php; title: ; notranslate">
╔══ Vulnerable: early-exit comparison ══╗
xxxxxxxxxxxxxx 252 ns
sxxxxxxxxxxxxx 303 ns ██████████████
suxxxxxxxxxxxx 371 ns ███████████████████████████
supxxxxxxxxxxx 417 ns █████████████████████████████████
superxxxxxxxxx 529 ns ██████████████████████████████...
supersecret1xx 916 ns ████████████████████████████████████...
supersecret123 983 ns ████████████████████████████████████████...
╔══ Secure: hash_equals() ══════════════╗
xxxxxxxxxxxxxx 192 ns
sxxxxxxxxxxxxx 192 ns
suxxxxxxxxxxxx 193 ns
supersecret123 194 ns
</pre>
</div>
<p>The bar chart in the first block is the oracle rendered in ASCII. The flat line in the second block is what security looks like.</p>
<h2 id="toc_3">The Guessing Game</h2>
<p>Reading about an attack is one thing. Actually holding the oracle and using it is another.</p>
<p>The repository includes <code>game.php</code>, a small CLI script that generates a random password and invites you to crack it using timing alone. No other hints are given.</p>
<div>
<pre class="brush: php; title: ; notranslate">
php game.php # interactive: you provide the guesses
php game.php --auto # the solver runs itself, you watch
php game.php --length=8 # set the password length (default: 6)
</pre>
</div>
<p>In interactive mode, the script prints how long each guess took and nothing else. You use those nanoseconds to converge on the answer. There is no scoreboard, but you will feel the moment you start using the oracle deliberately rather than guessing at random. That is the point of the exercise.</p>
<p>The <code>--auto</code> mode is the more instructive one. It runs the byte-by-byte attack in front of you, printing the timing for each position as the solver locks in one character at a time:</p>
<div>
<pre class="brush: php; title: ; notranslate">
Position 1/5 (known so far: _____)
'h' → 173.08 ns <-- winner
'v' → 130.68 ns
...
Position 2/5 (known so far: h____)
'2' → 241.08 ns <-- winner
'z' → 173.39 ns
...
Cracked: h2c7a in 1621 oracle calls.
Brute-force would have needed ~60,466,176 guesses.
</pre>
</div>
<p>Watching the solver converge is the most convincing argument for <code>hash_equals()</code>. No amount of prose is as persuasive as watching a six-character password dissolve in under two thousand guesses.</p>
<h2 id="toc_4">The Framework Illusion</h2>
<p>At this point a reasonable developer might push back. The demo ran each comparison 150,000 times in a tight inner loop. A real server routes the request through middleware, reads from a database, renders a template, writes a log line. The nanosecond signal is buried under fifty milliseconds of framework overhead. Surely this is theoretical?</p>
<p>It is not theoretical. It is slightly more expensive.</p>
<p>The signal-to-noise ratio is genuinely unfavourable. A typical Symfony or Laravel response takes 50 to 200 milliseconds in production. The per-byte timing difference we measured is 50 to 200 nanoseconds. That is a ratio of about one in a million.</p>
<p>But the attacker is not sending one request per candidate. The attacker sends ten thousand requests per candidate and computes the median response time. Framework overhead is random noise: it varies independently of the secret. The per-byte timing difference is a systematic bias: it shifts the median upward, consistently, for the correct character. Statistics are patient. Given enough samples, the signal emerges from the noise with mathematical certainty.</p>
<p>A historical note: the 2023 Marvin attack against RSA decryption recovered private keys over a standard TCP connection, exploiting timing differences measured in milliseconds, not nanoseconds. TLS-level timing attacks have worked against encrypted traffic. The Lucky Thirteen attack on TLS 1.2 broke the CBC padding scheme using remote timing differences on the order of a few hundred microseconds. The argument that “the noise drowns it out” has been disproved so many times that it no longer deserves to be a comfort.</p>
<p>The framework raises the cost of the attack. It does not close the oracle.</p>
<h2 id="toc_5">Time-Padding: Hiding the Clock</h2>
<p>Replacing <code>==</code> with <code>hash_equals()</code> fixes the comparison. It does not fix the clock.</p>
<p>Even with a constant-time comparison, the total response time can leak. A login endpoint that returns in 2 ms when the username is unknown, because it short-circuits before ever reaching the password check, and in 15 ms when the username exists, because it fetches the user record and computes the full comparison, is leaking account existence. That is a different oracle, and it does not touch the password at all.</p>
<p>The fix is to floor every auth response to a minimum duration and add a small random jitter:</p>
<div>
<pre class="brush: php; title: ; notranslate">
function timePaddedAuth(callable $work, int $minMs = 200): mixed
{
$start = hrtime(true);
$result = $work();
$elapsed = (hrtime(true) - $start) / 1_000_000;
$jitter = random_int(0, 10);
usleep((int) max(0, $minMs - $elapsed + $jitter) * 1000);
return $result;
}
</pre>
</div>
<p>Wrap the entire authentication flow in one call. “User not found” and “wrong password” both take the same visible time. The jitter prevents a determined attacker from cancelling out the floor by averaging it away.</p>
<p>Two things to keep in mind. First, this is defence in depth, not a replacement for <code>hash_equals()</code>. Fix the comparison and pad the response: both, not one or the other. Second, set the minimum generously. If your slowest legitimate auth path takes 80 ms on a slow day, a 100 ms floor leaves no room. Make it 300 ms. Users will not notice. The attack becomes several orders of magnitude more expensive.</p>
<h2 id="toc_6">The Rogues’ Gallery</h2>
<p><code>hash_equals()</code> covers <code>==</code>. But <code>==</code> is not the only comparison function in PHP, and the others are equally talkative.</p>
<table>
<thead>
<tr>
<th>Expression or function</th>
<th>Early exit</th>
<th>Safer alternative</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>$a == $b</code></td>
<td>yes</td>
<td><code>hash_equals()</code></td>
</tr>
<tr>
<td><code>$a === $b</code></td>
<td>yes</td>
<td><code>hash_equals()</code></td>
</tr>
<tr>
<td><code>strcmp($a, $b)</code></td>
<td>yes</td>
<td><code>hash_equals()</code></td>
</tr>
<tr>
<td><code>strncmp($a, $b, $n)</code></td>
<td>yes</td>
<td><code>hash_equals()</code></td>
</tr>
<tr>
<td><code>in_array($token, $list)</code></td>
<td>yes</td>
<td>compare hashes of all entries</td>
</tr>
<tr>
<td><code>array_search($token, $list)</code></td>
<td>yes</td>
<td>compare hashes</td>
</tr>
<tr>
<td><code>str_contains($s, $needle)</code></td>
<td>yes</td>
<td>HMAC-based membership check</td>
</tr>
<tr>
<td><code>strpos($s, $needle)</code></td>
<td>yes</td>
<td>HMAC-based membership check</td>
</tr>
</tbody>
</table>
<p><code>in_array</code> is the one that surprises developers the most. It is common to keep a list of valid API tokens and check incoming requests against it. The call iterates the list and exits the moment it finds a match, or exhausts the list and returns false. The timing tells the attacker both whether the token is valid and, if not, how far alphabetically the closest token is. The safe pattern is to hash every candidate in the list and compare hashes: the hashes are not secret, and <code>hash_equals()</code> on two hashes gives away nothing.</p>
<p><code>str_contains</code> and <code>strpos</code> are worth naming because they appear in security code more often than they should. A pattern like <code>if (str_contains($apiKey, $knownPrefix))</code> leaks how far the prefix match extends. That information feeds the same oracle.</p>
<h2 id="toc_7">It Is Not Just Passwords</h2>
<p>We have been talking about passwords. The same clock ticks on anything the application keeps secret.</p>
<p>Password-reset tokens are the highest-risk example after passwords themselves. They are typically random hex strings, stored in the database, compared when a user follows a reset link. A timing oracle on the reset endpoint lets an attacker forge a valid token without ever receiving the email. The fix is <code>hash_equals()</code> on the comparison, a hashed store in the database, and a short expiry window so the oracle has less time to be useful.</p>
<p>Email addresses are a subtler case. They are rarely treated as secrets, because users hand them out freely. But a login form that returns faster when an email address is unknown than when it is known is an account-enumeration oracle. The attacker does not need to guess passwords at all. She just needs to know which emails are registered, which is often enough to cause harm. Breached credential lists are built this way. The time-padding approach from the previous section handles this, since both “user not found” and “wrong password” end up taking the same visible time.</p>
<p>API keys and webhook secrets are long-lived, machine-generated strings. They are the most valuable timing target in a typical web application, because a compromised API key grants ongoing access without triggering any password-reset flow, and the key owner may not notice for months. Constant-time comparison on ingest, a generous time pad on the validation endpoint.</p>
<p>Session tokens sit in cookies and are compared against the session store on every request. Most frameworks handle this correctly in their default session handlers. Custom session handlers, or hand-rolled session logic, are where the problem tends to reappear.</p>
<p>The question to ask about any endpoint that receives a secret value from the outside is: does the total response time, as seen by the caller, correlate with how close the input was to the correct answer? If yes, you have an oracle.</p>
<h2 id="toc_8">When the Secret Hits the Database</h2>
<p>All of the above assumes the comparison lives in PHP. But many of these values arrive via a database query first, and the database has its own clock.</p>
<p>Consider the classic token-verification pattern:</p>
<div>
<pre class="brush: sql; title: ; notranslate">SELECT user_id FROM password_resets WHERE token = ? AND expires_at > NOW()</pre>
</div>
<p>If the <code>token</code> column has a B-tree index, the database engine walks the index tree to find the row. The depth of that walk depends on where the sought value sits relative to the existing rows. Tokens that sort close to existing values in the index may require slightly different numbers of page accesses than tokens that sort far from any existing row. This is a fainter signal than PHP-level early exit, but it is the same category of leak: the server’s work varies with the input value.</p>
<p>The safe pattern is straightforward: never store raw secrets in the database. Store a SHA-256 hash of the token, and verify by hashing the incoming value and looking up the hash.</p>
<div>
<pre class="brush: php; title: ; notranslate">
// Storing a new token
$raw = bin2hex(random_bytes(32));
$hash = hash('sha256', $raw);
// Store $hash in the DB, email $raw to the user// Verifying
$hash = hash('sha256', $_GET['token'] ?? '');
$row = $db->query('SELECT * FROM password_resets WHERE token_hash = ?', [$hash]);
if ($row && hash_equals($row->token_hash, $hash)) {
// valid
}
</pre>
</div>
<p>Notice that <code>hash_equals()</code> appears even here, comparing two hashes. SHA-256 hashes are not secret in the same way the raw token is, so leaking comparison timing on the hash does not directly reveal the token. But the habit costs nothing and removes the question entirely.</p>
<p>Passwords follow a different pattern, because you cannot verify a bcrypt password in SQL at all: the stored hash encodes a random salt and the bcrypt cost factor, and the comparison requires running the full bcrypt computation on the input. The only correct flow is to fetch the user row by email, then call <code>password_verify()</code> in PHP. <code>password_verify()</code> is constant-time by design and handles the hash format automatically. The database never sees the comparison; it only handles the email lookup.</p>
<p>ORM frameworks do not change any of this. Eloquent, Doctrine, and their equivalents generate the same <code>WHERE token = ?</code> query under the hood. They are code-generation tools, not security layers. It is not a criticism: it is a reminder that switching ORMs does not close the oracle.</p>
<h2 id="toc_9">A Checklist for Monday Morning</h2>
<p>The journey from a single <code>==</code> covered rather more ground than expected. Here is the condensed version.</p>
<ul>
<li>Replace <code>==</code>, <code>===</code>, and <code>strcmp</code> on any secret value with <code>hash_equals()</code></li>
<li>Add a time pad with random jitter to every authentication endpoint</li>
<li>Audit every <code>in_array</code> and <code>array_search</code> call that touches API tokens, session identifiers, or other long-lived credentials</li>
<li>Store tokens as SHA-256 hashes in the database; never store raw secrets</li>
<li>Use <code>password_hash()</code> and <code>password_verify()</code> for passwords; never do password comparison in SQL</li>
<li>Check username and email lookups for account-enumeration timing side-channels</li>
</ul>
<p>The attack started with a single two-character operator. The fix also starts there: one function call, <code>hash_equals()</code>. Everything else on the list is the same principle applied consistently, widened to cover the full surface.</p>
<p>The invariant to maintain is simple: the time the server spends on a request must not reveal how close the caller’s guess was. When that holds, the oracle goes silent, and a before-lunch problem becomes 4.8 × 10²¹ guesses again.</p>
<p>The post <a href="https://www.exakat.io/the-clock-that-cracks-passwords/">The Clock That Cracks Passwords</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>