Skip to content

Identifier

Foundation Identifier provides injectable contracts for string identifiers and a default ULID implementation. Generated ULIDs are canonical 26-character uppercase strings that combine a millisecond timestamp with secure randomness.

ULIDs work well for identifiers that must be portable across databases or systems while remaining roughly sortable by creation time.

Install the split package:

composer require stellarwp/foundation-identifier

Identifier services are registered through the shared application provider list:

In src/App.php, add the Foundation provider before features that generate or validate ULIDs:

use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Identifier\IdentifierProvider;

/** @var list<class-string<Provider>> */
private const array PROVIDERS = [
	IdentifierProvider::class,
];

The provider registers the ULID generator and validator as shared services. The supplied generator uses the system clock and secure randomness.

Create src/Job/Job_Creator.php and inject UlidGenerator. Registering IdentifierProvider supplies the implementation:

<?php declare(strict_types=1);

namespace Plugin\Job;

use StellarWP\Foundation\Identifier\Ulid\Contracts\UlidGenerator;

/**
 * Creates identifiers for queued jobs.
 */
final readonly class Job_Creator {

	public function __construct(
		private UlidGenerator $generator
	) {
	}

	public function create_id(): string {
		return $this->generator->generate();
	}
}

A generated value looks like 01ARYZ6S410000000000000000.

In src/Job/Job_Request.php, use UlidValidator at input boundaries before passing an external identifier into application behavior:

<?php declare(strict_types=1);

namespace Plugin\Job;

use InvalidArgumentException;
use StellarWP\Foundation\Identifier\Ulid\UlidValidator;

/**
 * Validates a job identifier received from outside the application.
 */
final readonly class Job_Request {

	public function __construct(
		private UlidValidator $validator
	) {
	}

	/**
	 * @throws InvalidArgumentException When the identifier is not a canonical ULID.
	 */
	public function identifier( string $value ): string {
		if ( ! $this->validator->isValid( $value ) ) {
			throw new InvalidArgumentException( 'The job identifier is invalid.' );
		}

		return $value;
	}
}

Validation accepts canonical uppercase ULIDs only. Lowercase values, invalid lengths, ambiguous characters such as I, L, O, and U, and timestamps outside the ULID range are rejected.

The first ten ULID characters encode creation time in milliseconds, so sorting canonical ULID strings groups identifiers by generation time.

Use the narrowest contract that describes the feature:

Contract Use when
Ulid\Contracts\UlidGenerator The stored or exchanged identifier must be a ULID
Contracts\IdentifierGenerator The feature needs a unique string but should not choose its format

For format-independent features, bind IdentifierGenerator to the application’s chosen strategy. IdentifierProvider supplies the ULID-specific binding used in the example above.

If the application chooses ULIDs as its default, create src/Identifier/Provider.php:

<?php declare(strict_types=1);

namespace Plugin\Identifier;

use StellarWP\Foundation\Container\Contracts\Resolver as C;
use StellarWP\Foundation\Container\Contracts\Provider as Service_Provider;
use StellarWP\Foundation\Identifier\Contracts\IdentifierGenerator;
use StellarWP\Foundation\Identifier\Ulid\Contracts\UlidGenerator;

/**
 * Selects ULID as the application's default identifier strategy.
 */
final class Provider extends Service_Provider {

	public function register(): void {
		$this->container->bind(
			IdentifierGenerator::class,
			static fn ( C $c ): UlidGenerator => $c->get( UlidGenerator::class )
		);
	}
}

Register both providers in src/App.php, in that order:

use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Identifier\IdentifierProvider;
use Plugin\Identifier;

/** @var list<class-string<Provider>> */
private const array PROVIDERS = [
	IdentifierProvider::class,
	Identifier\Provider::class,
];

The callback aliases the broad contract to the configured ULID singleton, so both contracts resolve the same generator.

A service that accepts the application’s chosen format can now import Contracts\IdentifierGenerator and inject it in place of Ulid\Contracts\UlidGenerator. Its call to $this->generator->generate() stays the same. Services whose storage or external API requires ULIDs should keep the ULID-specific contract.

For the Job_Creator above, use a fixture that returns one known ULID. Create tests/Support/Fixtures/Identifier/Fixed_Ulid_Generator.php:

<?php declare(strict_types=1);

namespace Plugin\Tests\Support\Fixtures\Identifier;

use StellarWP\Foundation\Identifier\Ulid\Contracts\UlidGenerator;

/**
 * Returns one predictable identifier in focused tests.
 */
final readonly class Fixed_Ulid_Generator implements UlidGenerator {

	public function __construct(
		private string $identifier
	) {
	}

	public function generate(): string {
		return $this->identifier;
	}
}

After registering IdentifierProvider in the test container, bind the fixture before resolving the service under test:

use Plugin\Job\Job_Creator;
use Plugin\Tests\Support\Fixtures\Identifier\Fixed_Ulid_Generator;
use StellarWP\Foundation\Identifier\Ulid\Contracts\UlidGenerator;

$identifier = '01ARYZ6S410000000000000000';

$this->container->bind(
	UlidGenerator::class,
	new Fixed_Ulid_Generator( $identifier )
);

$service = $this->container->get( Job_Creator::class );

$this->assertSame( $identifier, $service->create_id() );

Use UlidValidator when a test only needs to confirm that production generation returns a valid ULID. Avoid asserting an exact value from the system clock and secure entropy.

For a feature using the optional IdentifierGenerator strategy, implement and bind that contract in its fixture instead. Match the contract injected by the service under test.