J—P

Limited availability — accepting new projects starting December 2026.

Human-Transcribable IDs for PHP

Most ID generators optimize for entropy density: pack as many bits as possible into as few characters as possible. That’s great until a person has to read the ID off an invoice, a parcel label or a screen and type it back in. Was that a 0 or an O? A 1, an l or an I? Two ws or three?

I ran into this while building an e-shop. Order numbers end up on confirmation emails and invoices, product codes on labels, and sooner or later a customer reads one back through a form or over the phone. UUIDs are hopeless for that, sequential numbers announce how much you sell, and most short-ID libraries hand you an alphabet full of lookalikes. So I wrote lemmon/id: one class, six public methods, no runtime dependencies, built around four ideas.

  1. Alphabet. Twenty characters, 3 4 7 9 c d e f h j k m n p r t w x y z, with the usual lookalikes either removed or reduced to a single survivor.
  2. Normalizer. Repairs a handful of predictable misreadings (1 → 7, 2 → z, g → 9, …) and strips separators, including the Unicode dashes and invisible characters that sneak in through copy and paste.
  3. Check character. Catches every single-character substitution and nearly every adjacent transposition.
  4. Blocklist. Keeps a few unwanted English words out of customer-facing IDs. Dropping a from the alphabet already rules out many of them.
use Lemmon\Id\Id;

$id = Id::generate();                    // "kwe7wndx4t" — canonical, lowercase
echo Id::chunk(strtoupper($id));         // "KWE7-WNDX-4T" — display form

// Accepting a transcribed ID back:
$input = Id::normalize('KWE1-WNDX-4T');  // misread 7 as 1 → repaired

if (!Id::isValid($input, 10) || !Id::verifyCheck($input)) {
    // Ask the person to check the transcription.
}

IDs are stored lowercase and shown however you like: uppercase, chunked into groups, or both. The round trip is lossless, so Id::normalize(Id::chunk($id)) gives you back exactly what you stored. The normalizer only repairs characters with a single plausible reading; genuinely ambiguous ones (0, o, l, I, s) are never guessed and simply fail isValid(). A typo that lands on another valid character passes that test, which is what verifyCheck() is for: a single wrong character, or almost any pair of swapped neighbours, gets rejected instead of quietly matching somebody else’s order.

The phone call is where the alphabet stops helping. It’s tuned for eyes, not ears, and m/n or d/t still sound alike. Spell the ID with code words (Papa, November, Zulu), and the check character catches most slips that still get through.

WARNING

These are identifiers, not secrets. Don’t use them as session tokens, API keys or password-reset links.

The default length of ten characters gives you nine random characters plus the check, which puts a 50% chance of a collision somewhere around 840 thousand IDs. Stay well below that, or pass a longer length, and enforce uniqueness with a database constraint either way. Two more knobs are worth knowing: the blocklist can be extended for your own users and languages, and an optional cap on repeating characters settles the “two ws or three?” question.

Install it with composer require lemmon/id and find the full documentation, the reasoning behind every excluded character, and a comparison with Crockford Base32 and Plus Codes at github.com/lemmon/id-php.

kirby-plugin-callouts

Bring GitHub-style callouts to Kirby using simple [!TYPE] Markdown syntax. Keep writing Markdown—your site gets polished, theme-ready callouts that make your content stand out.

PHP 2

clsx

A tiny PHP library inspired by the popular JavaScript clsx, making it easy to build clean, dynamic class attributes for your HTML. Write neater templates and keep your code readable, no matter how complex your CSS logic gets.

PHP