TypeID PHP implements TypeID specification 0.3.0 for PHP ^8.3.
A TypeID joins a type prefix, such as user or invoice, to a UUID encoded as a 26-character base32 suffix:
user_01jsnsf2g7e2saxdjvz3j6tc3x
Generated TypeIDs use UUIDv7. With a correct cryptographic random source, they are designed for global uniqueness with negligible collision probability. TypeIDs with the same type prefix sort by their millisecond timestamps. Ramsey's default UUIDv7 generator also orders successive values within one PHP process. Separate processes and hosts do not share that ordering state.
The package has these properties:
- The type prefix remains visible in strings, logs, and URLs.
- A suffix uses 26 characters instead of the 36 characters in a hyphenated UUID.
- Canonical strings contain only lowercase ASCII letters, digits, and underscores.
- The codec uses PHP bit operations and does not require the GMP or BCMath extensions.
A UUIDv7 TypeID exposes its millisecond timestamp. Do not use a TypeID as a secret or access token.
composer require jewei/typeid-phpGenerate a TypeID with a user type prefix:
use TypeID\TypeID;
$id = TypeID::generate('user');
echo $id; // For example: user_01jsnsf2g7e2saxdjvz3j6tc3x
echo $id->prefix; // user
echo $id->suffix; // 01jsnsf2g7e2saxdjvz3j6tc3x
echo $id->toUuid(); // 01966b97-8a07-70b2-aeb6-5bf8e46d307dParse a canonical TypeID string or a bare suffix:
$id = TypeID::fromString('user_01jsnsf2g7e2saxdjvz3j6tc3x');
$bare = TypeID::fromString('01jsnsf2g7e2saxdjvz3j6tc3x');Convert an existing UUID of any version:
$id = TypeID::fromUuid(
'01966b97-8a07-70b2-aeb6-5bf8e46d307d',
'invoice',
);
echo $id; // invoice_01jsnsf2g7e2saxdjvz3j6tc3xA type prefix makes identifier intent visible at runtime, but every value is still a TypeID. PHP cannot prevent a user_... TypeID from being passed where an order_... TypeID was expected. Wrap TypeID when method signatures must distinguish domain identifiers:
final readonly class UserId implements Stringable
{
private function __construct(private TypeID $value) {}
public static function generate(): self
{
return new self(TypeID::generate('user'));
}
public static function fromString(string $value): self
{
$typeId = TypeID::fromString($value);
if ($typeId->prefix !== 'user') {
throw new InvalidArgumentException('Expected a user TypeID');
}
return new self($typeId);
}
public function __toString(): string
{
return $this->value->toString();
}
}An OrderId wrapper can enforce the order type prefix, making UserId and OrderId distinct PHP types.
For text columns, store the canonical string and restore it with TypeID::fromString():
$stored = $id->toString();
$restored = TypeID::fromString($stored);For binary(16) columns, store the UUID bytes. The bytes do not contain the type prefix, so store the type prefix separately or supply it from the application context.
$stored = $id->bytes();
$restored = TypeID::fromBytes($stored, 'invoice');Use native PHP serialization only with trusted data and for runtime round trips within one major version. Never pass untrusted input to unserialize(). Store or transmit the canonical TypeID string instead.
json_encode() writes the canonical string:
echo json_encode(['id' => $id]);
// {"id":"invoice_01jsnsf2g7e2saxdjvz3j6tc3x"}If your application requires a sentinel, create one with TypeID::zero(). Its suffix encodes the nil UUID.
$zero = TypeID::zero('user');
var_export($zero->isZero()); // true
var_export($zero->isNonZero()); // false
echo $zero; // user_00000000000000000000000000A zero TypeID is not UUIDv7 and is not K-sortable. The same limit applies to TypeIDs created from non-v7 UUIDs. Use a zero TypeID as a foreign-key value only when the data model defines a sentinel record. For an absent relationship, a nullable foreign key is generally clearer.
Invalid input throws TypeID\Exception\ValidationException, which extends InvalidArgumentException. The constructor and all factories can throw it. generate() can additionally throw TypeID\Exception\GenerationException, which extends RuntimeException, when UUID generation fails.
Catch TypeID\Exception\TypeIDException to handle either package exception:
use TypeID\Exception\TypeIDException;
try {
$id = TypeID::fromString($input);
} catch (TypeIDException $exception) {
// Handle an exception from this package.
}Validation messages identify the failed rule and may report the input length. They never include the rejected value.
| Member | Result | Behavior |
|---|---|---|
TypeID::generate(?string $prefix = null) |
TypeID |
Creates a TypeID backed by UUIDv7. |
TypeID::fromString(string $value) |
TypeID |
Parses a canonical string or a bare suffix. |
TypeID::fromUuid(string $uuid, ?string $prefix = null) |
TypeID |
Accepts 32 hexadecimal characters or canonical 8-4-4-4-12 notation in either letter case. |
TypeID::fromBytes(string $bytes, ?string $prefix = null) |
TypeID |
Accepts exactly 16 UUID bytes. |
TypeID::zero(?string $prefix = null) |
TypeID |
Creates a TypeID backed by the nil UUID. |
new TypeID(string $prefix, string $suffix) |
TypeID |
Validates both arguments. |
->prefix, ->suffix |
string |
Expose readonly properties. |
->toString(), (string), ->jsonSerialize() |
string |
Return the canonical TypeID string. |
->toUuid() |
string |
Returns a lowercase, hyphenated UUID. |
->bytes() |
string |
Returns 16 UUID bytes. |
->isZero(), ->isNonZero() |
bool |
Check whether the suffix encodes the nil UUID. |
->equals(TypeID $other) |
bool |
Compares both the type prefix and the suffix. |
TypeID::ZERO_SUFFIX |
string |
Contains the 26-character suffix for the nil UUID. |
fromUuid() normalizes uppercase or bare input. toUuid() always returns lowercase, hyphenated text.
user_01jsnsf2g7e2saxdjvz3j6tc3x
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
| +-- 26-character base32 suffix encoding a 128-bit UUID
+-------- type prefix
A separator joins a non-empty type prefix to the suffix. A bare TypeID contains only the suffix. The last underscore in a canonical string is the separator, so a type prefix can contain underscores.
A type prefix matches this expression:
^([a-z]([a-z_]{0,61}[a-z])?)?$The type prefix follows these rules:
- It contains no more than 63 characters. An empty type prefix is valid.
- It contains only lowercase ASCII letters and underscores.
- A non-empty type prefix starts and ends with a letter.
- It can contain consecutive underscores.
- It cannot contain digits or uppercase letters.
For example, my__type is valid. _user, user_, user1, and User are invalid.
A canonical suffix matches this expression:
^[0-7][0123456789abcdefghjkmnpqrstvwxyz]{25}$The suffix follows these rules:
- It contains exactly 26 lowercase base32 characters.
- Its alphabet omits
i,l,o, andu. - It contains no hyphens or padding.
- Its first character cannot exceed
7.
A UUID contains 128 bits, while 26 base32 characters can hold 130 bits. The encoder adds two zero bits at the start. The parser rejects any suffix greater than 7zzzzzzzzzzzzzzzzzzzzzzzzz because that value exceeds 128 bits.
A canonical TypeID string contains between 26 and 90 characters. The maximum contains a 63-character type prefix, one separator, and a 26-character suffix.
TypeID is the only supported production entry point. The exception classes are supported catch types. Public property and parameter names are stable within a major version and may be used with PHP named arguments. This table defines the compatibility promise within a major version:
| Element | Status |
|---|---|
TypeID factories, constructor, and instance methods |
Supported |
TypeID::$prefix, TypeID::$suffix, and TypeID::ZERO_SUFFIX |
Supported |
Stringable and JsonSerializable behavior |
Supported |
TypeIDException, ValidationException, and GenerationException |
Supported catch types |
Native serialize() and unserialize() |
Supported for runtime round trips |
| Exception message text | Can change |
ValidationException named constructors |
Internal |
TypeID\Base32 |
Internal and can be removed |
PHP cannot hide a top-level class from Composer's autoloader. Composer can therefore load TypeID\Base32, but code that calls it has no compatibility guarantee. composer test:architecture prevents production code and contract tests in this repository from depending on Base32.
CONTEXT.md defines the terms used by the package.
The repository vendors specification 0.3.0 and its conformance vectors in spec/. The files are pinned to jetify-com/typeid commit cb20c6e. spec/provenance.json records their SHA-256 hashes.
The test suite also checks requirements outside the supplied vectors:
- Generated TypeIDs survive string, UUID, and byte round trips.
- Generated TypeIDs contain the required UUIDv7 version and variant bits.
- Ramsey's default UUIDv7 generator returns increasing values within one process.
- The codec uses the specified alphabet and maps the maximum suffix to the maximum UUID value.
- Canonical strings obey the 26-character and 90-character limits.
Install the development dependencies, then run the checks from the repository root:
composer install
composer test
vendor/bin/pint --testThe Composer scripts run these checks:
| Command | Check |
|---|---|
composer test |
Runs the architecture check, PHPStan, and the Pest suite. |
composer test:unit |
Runs the Pest suite. |
composer test:architecture |
Checks the internal dependency rules. |
composer test:types |
Runs PHPStan at maximum strictness against PHP 8.3. |
vendor/bin/pint --test |
Checks PHP formatting. |
The test directories have separate responsibilities:
| Directory | Responsibility |
|---|---|
tests/Contract/ |
Tests the public API through TypeID. |
tests/Spec/ |
Tests the vendored specification and requirements absent from its vectors. |
tests/Codec/ |
Identifies failures in individual base32 bit positions through Base32. |
tests/Tooling/ |
Tests the architecture checker with PHP snippets. |
Only tests/Codec/ can call Base32. Direct codec tests report the failed byte and character. Other tests use TypeID.