Skip to content

Conversation

@zaenalcoders
Copy link

Add Pure-PHP ULID Generator (Spec-Compliant & Monotonic)

Summary

This pull request introduces a fully self-contained ULID (Universally Unique Lexicographically Sortable Identifier) generator for phpMyAdmin.
The implementation is written entirely in pure PHP, adds no external dependencies, and follows the official ULID specification, including timestamp structure, randomness generation, and Crockford Base32 encoding.


Motivation

phpMyAdmin currently supports UUID generation but does not provide a built-in ULID generator.
ULID offers several advantages:

  • Lexicographically sortable identifiers
  • Millisecond timestamp precision
  • 80 bits of secure randomness
  • Human-friendly, URL-safe Base32 output
  • Widely used in modern distributed systems

Adding a built-in ULID generator improves developer experience without increasing dependency footprint.


Design & Implementation

Specification-Compliant

This generator follows the official ULID specification:
https://github.com/ulid/spec

Monotonic Behavior

When multiple ULIDs are generated within the same millisecond, the randomness portion is incremented to maintain:

  • Ordering consistency
  • Collision avoidance
  • Monotonic output (similar to Symfony/Uid)

Dependency-Free

Although inspired by symfony/uid and robinvdvleuten/ulid, this implementation is entirely rewritten to keep phpMyAdmin dependency-light.

Structure & Coding Standards

  • PEAR-style formatting
  • phpMyAdmin directory conventions
  • Documented using phpDoc
  • Focus on clarity, maintainability, and predictability

New Files

Class

libraries/classes/Ulid.php

Public API

Ulid::generate(): string
Generates a 26-character ULID encoded using Crockford Base32.


Tests

Test File

test/classes/UlidTest.php

Test Coverage Includes:

  • Correct ULID length (26 chars)
  • Valid Base32 character set
  • Monotonic ordering within the same millisecond
  • Timestamp correctness
  • Collision avoidance across large batches

All tests are pure PHPUnit and require no external libraries.


Impact

  • No breaking changes
  • No new dependencies
  • Minimal performance overhead
  • Fully isolated; no side effects

Conclusion

This PR adds a modern, reliable, and specification-compliant ULID generator to phpMyAdmin, without altering existing behavior or adding dependencies.
It strengthens phpMyAdmin's utility for modern development workflows.

Signed-off-by: zaenalcoders <zaenal.virus@gmail.com>
…e functions

Signed-off-by: zaenalcoders <zaenal.virus@gmail.com>
@ibennetch
Copy link
Member

ibennetch commented Dec 3, 2025 via email

@zaenalcoders
Copy link
Author

Hi @ibennetch,

Thank you for the feedback and kind words! I’ll update this pull request to target the 6.x branch as suggested and ensure it aligns with the upcoming major release.

Appreciate your guidance and time reviewing this.

@williamdes williamdes reopened this Dec 3, 2025
@williamdes
Copy link
Member

🤔 Maybe it should target 6.x, which we're working hard towards releasing
as the next major version.

I am a bit confused as it already targets master, and QA_6_0 did not start yet

@ibennetch
Copy link
Member

ibennetch commented Dec 3, 2025

I am a bit confused as it already targets master, and QA_6_0 did not start yet

You're quite right - I somehow misread the email notification. I'm not sure what I thought I saw - I definitely misread it as being for the QA branch - but you are of course correct.

}

if ($editField->function === 'ULID') {
/* generate ULID */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add comments that convey no information.


if ($editField->function === 'ULID') {
/* generate ULID */
$ulid = (string) Ulid::generate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$ulid = (string) Ulid::generate();
$ulid = Ulid::generate();

src/Ulid.php Outdated
private static function encodeRandom(array $bytes): string
{
$binary = '';
foreach ($bytes as $b) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach ($bytes as $b) {
foreach ($bytes as $byte) {

The word is short enough that you don't need to be cryptic with it.

src/Ulid.php Outdated
$result = '0';
} else {
while ($value > 0) {
$result = $alphabet[$value % 32] . $result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$result = $alphabet[$value % 32] . $result;
$result = self::ALPHABET[$value % 32] . $result;

Unnecessary reassignments are confusing in the long run.

src/Ulid.php Outdated
$result = '';

if ($value === 0) {
$result = '0';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this line necessary?

}
}

// ULIDs created one after another should be in order thanks to the monotonic logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't think any of these comments add any value. They just repeat what the test already does.

$u1 = Ulid::generate();
$u2 = Ulid::generate();

$this->assertLessThan(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this assertion accept strings? Is there a reason to use strcmp here?

Comment on lines 58 to 59
// Wait for about 2ms to make sure the next ULID lands in a new millisecond.
usleep(2000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you could take the timestamp as an optional parameter to the generate method.

);
}

// The first 10 characters are the timestamp. If you generate a ULID a little later, that part should go up.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment like this belongs in the class where the functionality is implemented, not in the test case.

Comment on lines 71 to 90
public function testMultipleUlidsAreUnique(): void
{
$generated = [];

for ($i = 0; $i < 1000; $i++) {
$u = Ulid::generate();
$this->assertArrayNotHasKey(
$u,
$generated,
"Duplicate ULID found: {$u}"
);
$generated[$u] = true;
}

$this->assertCount(
1000,
$generated,
'All ULIDs generated in batch should be unique'
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A unit test should be reproducible. If it fails, we need to be able to apply a fix and retest with the same parameters. I doubt this test fulfils this requirement. Testing random behaviour may be useful during development, but it isn't helpful in a test suite.

Signed-off-by: zaenalcoders <zaenal.virus@gmail.com>
Signed-off-by: zaenalcoders <zaenal.virus@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants