dseguy / genie
Systematic value generation for PHP: lazy, composable, fluent.
Requires
- php: ^8.2
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
README
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.
Installation
composer require exakat/generator
Requirements
- PHP 8.2+
- No runtime dependencies
Quick start
use Dseguy\Generator\Letters; use Dseguy\Generator\Digits; foreach (Letters::lower()->merge(Digits::all()) as $value) { // a, b, …, z, 0, 1, …, 9 }
Primitive generators
Each primitive is a class with static factory methods. All generators are lazy — values are computed only when iterated.
Letters
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 }
Digits
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.
Booleans
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 }
Enums
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.
Collection
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], …
FromCallable
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.
Permutations
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
Combinators are methods available on every generator. They return a new generator and preserve laziness. Chain them in any order.
->merge(GeneratorInterface ...$others) — Union
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, …, 9
->product(GeneratorInterface ...$others) — Cartesian product
Yields 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.
->filter(callable $predicate) — Conditional filter
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]
->map(callable $transform) — Transform
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'
->repeat(int $n) — Power (G^n)
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 tuples
Each 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.
Pragmatic usage examples
Real-world scenarios where systematic value generation is useful.
Password / token brute-force testing
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));
Exhaustive unit test data providers
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]); }
Generating test fixtures
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 }
Exhaustive enum coverage in tests
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); }
Slug / identifier collision detection
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; }
Scanning configuration ranges
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)); }
Character set validation
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; } }
Generating SQL / query permutations
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); } }
Terminal method
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().
Composing chains
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));
API reference
Namespace
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;
Primitive generators
| 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) |
GeneratorInterface
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 |
Design principles
- 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).
Out of scope (v1)
- Random / shuffled generation
- Weighted generators
- CSV / file-based value sources
- Output adapters (
->toArray(),->toCsv(),->echo()) - Framework integrations (Laravel, Symfony)