A PHP 8.2+ library for generating all values in a domain using lazy, composable iterators.
Use case: when code needs to walk an entire value domain — all letters, all digit pairs, all 3-character strings — this library provides the building blocks to express that concisely and memory-efficiently.
composer require exakat/generator- PHP 8.2+
- No runtime dependencies
use Dseguy\Generator\Letters;
use Dseguy\Generator\Digits;
foreach (Letters::lower()->merge(Digits::all()) as $value) {
// a, b, …, z, 0, 1, …, 9
}Each primitive is a class with static factory methods. All generators are lazy — values are computed only when iterated.
Yields individual alphabetic characters.
| Factory method | Yields |
|---|---|
Letters::lower() |
a … z (26 values) |
Letters::upper() |
A … Z (26 values) |
Letters::all() |
a … z then A … Z (52 values) |
foreach (Letters::upper() as $c) {
// A, B, C, …, Z
}Yields integers over a bounded range.
| Factory method | Yields |
|---|---|
Digits::all() |
0 … 9 |
Digits::range(int $start, int $end, int $step = 1) |
integers from $start to $end inclusive, stepping by $step |
foreach (Digits::range(1, 10, 2) as $n) {
// 1, 3, 5, 7, 9
}Throws InvalidArgumentException if $start > $end or $step <= 0.
Yields boolean (and optionally null) values.
| Factory method | Yields |
|---|---|
Booleans::values() |
true, false |
Booleans::withNull() |
true, false, null |
foreach (Booleans::withNull() as $b) {
// true, false, null
}Yields values derived from a PHP 8.1+ enum. Three factory methods cover the three things callers typically need from an enum.
| Factory method | Accepts | Yields |
|---|---|---|
Enums::cases(string $class) |
any enum | the enum case objects (\UnitEnum instances) |
Enums::names(string $class) |
any enum | the case names as strings |
Enums::values(string $class) |
backed enum only | the backing values (int or string) |
enum Suit { case Hearts; case Diamonds; case Clubs; case Spades; }
enum Priority: int { case Low = 1; case Medium = 2; case High = 3; }
foreach (Enums::cases(Suit::class) as $case) {
// Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit::Spades
}
foreach (Enums::names(Suit::class) as $name) {
// 'Hearts', 'Diamonds', 'Clubs', 'Spades'
}
foreach (Enums::values(Priority::class) as $value) {
// 1, 2, 3
}Throws InvalidArgumentException if the class is not an enum, or if values() is called on a pure (non-backed) enum.
Wraps an arbitrary PHP array as a generator source. Keys are discarded; only values are yielded.
| Factory method | Yields |
|---|---|
Collection::of(array $items) |
every value in $items, re-indexed from 0 |
use Dseguy\Generator\Collection;
foreach (Collection::of(['foo', 'bar', 'baz']) as $value) {
// 'foo', 'bar', 'baz'
}
// Compose freely with combinators
Collection::of([1, 2, 3])->product(Booleans::values())
// yields: [1, true], [1, false], [2, true], …Wraps a factory callable that returns a \Traversable (generator, iterator, or iterator aggregate). The factory is invoked fresh on every iteration, making the source fully reusable and safe to use with all combinators.
| Factory method | Accepts | Yields |
|---|---|---|
FromCallable::of(callable $factory) |
callable(): \Traversable |
every value produced by the traversable, keys discarded |
use Dseguy\Generator\FromCallable;
// Wrap a generator function
$source = FromCallable::of(fn() => (function () {
yield 'x';
yield 'y';
yield 'z';
})());
// Wrap a database cursor or any lazy iterator
$rows = FromCallable::of(fn() => $db->query('SELECT id FROM items'));
foreach ($rows->filter(fn($row) => $row['id'] > 100) as $row) { … }Warning — infinite sources: a factory that yields forever is valid with
filter(),map(), andmerge(), but will loop forever or exhaust memory insideproduct()orrepeat(). There is currently no way to detect an infinite source at construction time; support for this is planned in a future version.
Yields all ordered arrangements of $length distinct characters drawn from a charset. No character repeats within a single value.
| Factory method | Yields |
|---|---|
Permutations::of(int $length, string $charset) |
All permutations of exactly $length distinct chars from $charset |
foreach (Permutations::of(2, 'abc') as $s) {
// 'ab', 'ac', 'ba', 'bc', 'ca', 'cb' — 6 values
}
// All 2-char permutations over the alphabet: 26×25 = 650 values
$codes = Permutations::of(2, 'abcdefghijklmnopqrstuvwxyz');Throws InvalidArgumentException if $length <= 0 or the charset has fewer than $length distinct characters.
Combinators are methods available on every generator. They return a new generator and preserve laziness. Chain them in any order.
Chains generators end-to-end into a single sequence.
Letters::lower()->merge(Digits::all())
// yields: a, b, …, z, 0, 1, …, 9
Letters::lower()->merge(Letters::upper(), Digits::all())
// yields: a, …, z, A, …, Z, 0, …, 9Yields all combinations as flat arrays (one element per source generator).
Letters::upper()->product(Digits::all())
// yields: ['A', 0], ['A', 1], …, ['Z', 9] — 260 tuples
Letters::lower()->product(Digits::all())->product(Booleans::values())
// yields: ['a', 0, true], ['a', 0, false], ['a', 1, true], …Arrays from nested product() or repeat() calls are flattened — values spread into the result rather than nest.
Excludes values for which $predicate returns falsy.
Digits::range(1, 100)->filter(fn($n) => $n % 2 === 0)
// yields: 2, 4, 6, …, 100
Letters::lower()->product(Digits::all())
->filter(fn($pair) => $pair[1] > 5)
// yields: ['a', 6], ['a', 7], …, ['z', 9]Applies a function to every yielded value.
Letters::lower()->map(fn($c) => strtoupper($c))
// yields: A, B, …, Z
Letters::lower()->product(Digits::all())
->map(fn($pair) => implode('', $pair))
// yields: 'a0', 'a1', …, 'z9'Yields all $n-length sequences, with repetition allowed.
Letters::lower()->repeat(2)
// yields: ['a','a'], ['a','b'], …, ['z','z'] — 676 tuples
Digits::all()->repeat(3)
// yields: [0,0,0], [0,0,1], …, [9,9,9] — 1000 tuplesEach yielded value is a flat array. Throws InvalidArgumentException if $n <= 0.
repeat()vsPermutations:repeat()allows the same value to appear multiple times in one tuple;Permutationsdoes not.
Real-world scenarios where systematic value generation is useful.
Generate all possible PIN codes or short alphanumeric tokens to verify a rate-limiter or lockout policy in tests.
// All 4-digit PINs: 10^4 = 10 000 values
$pins = Digits::all()->repeat(4)
->map(fn($d) => implode('', $d));
foreach ($pins as $pin) {
$response = $client->post('/login', ['pin' => $pin]);
if ($response->status() === 429) {
// lockout triggered — stop here
break;
}
}// All 6-character alphanumeric tokens (case-insensitive)
$tokens = Letters::lower()->merge(Digits::all())->repeat(6)
->map(fn($chars) => implode('', $chars));Feed a PHPUnit data provider with every combination of inputs rather than hand-picking a few.
// Test a validator against every (letter, digit) pair
public static function letterDigitPairs(): iterable
{
return Letters::lower()->product(Digits::all())
->map(fn($pair) => [$pair[0], $pair[1]]);
}
/** @dataProvider letterDigitPairs */
public function test_accepts_alphanumeric(string $letter, int $digit): void
{
$this->assertTrue(Validator::isAlphanumeric($letter . $digit));
}// Test a function against all boolean/null combinations
public static function truthyInputs(): iterable
{
return Booleans::withNull()->map(fn($b) => [$b]);
}Populate a database or in-memory store with a systematic set of records.
// One user per letter of the alphabet
foreach (Letters::lower() as $initial) {
User::factory()->create(['username' => "user_{$initial}"]);
}// All combinations of role × status for permission matrix tests
$roles = Collection::of(['admin', 'editor', 'viewer']);
$statuses = Collection::of(['active', 'suspended', 'pending']);
foreach ($roles->product($statuses) as [$role, $status]) {
// test every role × status combination
}Iterate every case of an enum in a data provider so no case is accidentally omitted from test coverage.
enum Status: string { case Active = 'active'; case Suspended = 'suspended'; case Pending = 'pending'; }
public static function allStatuses(): iterable
{
return Enums::cases(Status::class)->map(fn($s) => [$s]);
}
/** @dataProvider allStatuses */
public function test_notification_is_sent_for_every_status(Status $status): void
{
$this->assertTrue(Notifier::shouldNotify($status));
}// Verify a mapping covers every backing value
foreach (Enums::values(Status::class) as $value) {
$this->assertArrayHasKey($value, StatusLabel::MAP);
}Verify that a slug generator produces unique output across all inputs.
$seen = [];
foreach (Letters::lower()->repeat(3)->map(fn($t) => implode('', $t)) as $trigram) {
$slug = MySlugifier::slugify($trigram);
assert(!isset($seen[$slug]), "Collision on: $trigram → $slug");
$seen[$slug] = true;
}Walk every valid port in a range, every HTTP status code, or every timeout value to verify system behaviour.
// Check that all privileged ports are refused
$privileged = Digits::range(1, 1023)->filter(fn($p) => !in_array($p, $allowList));
foreach ($privileged as $port) {
$this->assertFalse($server->canBind($port));
}// Verify all 5xx codes are handled
$serverErrors = Digits::range(500, 599);
foreach ($serverErrors as $code) {
$this->assertInstanceOf(ServerException::class, $handler->handle($code));
}Probe a sanitiser or encoder against every character in a defined alphabet.
// Every character that must survive HTML encoding unchanged
$safe = Letters::all()->merge(Digits::all());
foreach ($safe as $char) {
$this->assertSame($char, htmlspecialchars($char));
}// Detect which characters a legacy system rejects
$rejected = [];
foreach (Letters::all()->merge(Digits::all()) as $char) {
if (!$legacySystem->accepts($char)) {
$rejected[] = $char;
}
}Build every variant of a parameterised query for fuzz-style integration tests.
// All ORDER BY direction × LIMIT combinations
$directions = ['ASC', 'DESC'];
$limits = Digits::range(1, 5); // 1 … 5
foreach ($limits as $limit) {
foreach ($directions as $dir) {
$results = $db->query("SELECT * FROM items ORDER BY name $dir LIMIT $limit");
$this->assertCount($limit, $results);
}
}After composing a chain, call ->toIterator() to obtain a plain \Iterator that can be assigned to a variable and passed around.
$candidates = Letters::lower()->merge(Digits::all())->toIterator();
foreach ($candidates as $value) {
// a, b, …, z, 0, 1, …, 9
}All generators also implement \IteratorAggregate, so they can be used directly in foreach without calling ->toIterator().
Combinators compose freely. Each step returns a new generator.
use Dseguy\Generator\Letters;
use Dseguy\Generator\Digits;
use Dseguy\Generator\Booleans;
use Dseguy\Generator\Permutations;
// All lowercase alphanumeric characters
$alphanumeric = Letters::lower()->merge(Digits::all());
// All uppercase letter + single digit pairs
$pairs = Letters::upper()->product(Digits::all());
foreach ($pairs as [$letter, $digit]) { … }
// All 3-letter lowercase trigrams (26^3 = 17 576 values)
$trigrams = Letters::lower()->repeat(3);
foreach ($trigrams as [$a, $b, $c]) { … }
// Even numbers from 1 to 50
$evens = Digits::range(1, 50)->filter(fn($n) => $n % 2 === 0);
// Letters mapped to their ASCII code
$ascii = Letters::all()->map(fn($c) => ord($c));
// All 2-char permutations over a-z, formatted as strings
$codes = Permutations::of(2, 'abcdefghijklmnopqrstuvwxyz')
->map(fn($s) => strtoupper($s));use Dseguy\Generator\Letters;
use Dseguy\Generator\Digits;
use Dseguy\Generator\Booleans;
use Dseguy\Generator\Enums;
use Dseguy\Generator\Collection;
use Dseguy\Generator\FromCallable;
use Dseguy\Generator\Permutations;| Class | Factory methods |
|---|---|
Letters |
lower(), upper(), all() |
Digits |
all(), range(int $start, int $end, int $step = 1) |
Booleans |
values(), withNull() |
Enums |
cases(string $class), names(string $class), values(string $class) |
Collection |
of(array $items) |
FromCallable |
of(callable $factory) |
Permutations |
of(int $length, string $charset) |
All generators implement GeneratorInterface, which extends \IteratorAggregate.
| Method | Returns | Description |
|---|---|---|
getIterator() |
\Generator |
Yields domain values |
merge(...$others) |
GeneratorInterface |
Chain generators end-to-end |
product(...$others) |
GeneratorInterface |
Cartesian product |
filter($predicate) |
GeneratorInterface |
Filter by predicate |
map($transform) |
GeneratorInterface |
Transform each value |
repeat(int $n) |
GeneratorInterface |
All n-length sequences |
toIterator() |
\Iterator |
Materialise the chain |
- Lazy by default — every generator uses PHP
yield; no value is computed until iterated. - Composable — any generator can be used as input to any combinator.
- Fluent API — combinators chain directly on generator objects.
- Deterministic — fixed iteration order; generators can be re-iterated to produce the same sequence.
- Fail fast — invalid constructor arguments throw
InvalidArgumentExceptionimmediately, never produce silent empty sequences. - No runtime dependencies — pure PHP; only dev dependencies (PHPUnit, PHPStan).
- Random / shuffled generation
- Weighted generators
- CSV / file-based value sources
- Output adapters (
->toArray(),->toCsv(),->echo()) - Framework integrations (Laravel, Symfony)