Generators
Why generators are in a separate method
PHP attribute arguments must be constant expressions, so #[Given('x', Gen::int())] is not expressible. Instead name a method that returns array<string, ArbitraryInterface> keyed by parameter name. When the generators argument is omitted the runner falls back to a method named <testMethod>Generators.
In both adapters, generators and examples also accept a callable, which is how a provider gets reused between test classes: [Provider::class, 'method'], 'Provider::method', or an invokable object (new Provider()) — all valid attribute expressions on PHP 8.3. PHP 8.5 additionally allows an inline static function (): array { ... } and a first-class callable (Provider::method(...)). A string still resolves to a method on the test class first, so a local method named like a global function (range) keeps winning; the convention stays the default.
Declare generators (and examples) methods public static — or public if the body needs $this. Their only call site is this package's reflection, so static analysis sees them as unused: Rector's dead-code set deletes private ones (RemoveUnusedPrivateMethodRector). Public methods are safe, and Testo never treats a non-void-returning method as a test.
Generators
| Factory | Produces | Shrinks |
|---|---|---|
Gen::int() | PHP_INT_MIN..PHP_INT_MAX | toward 0 |
Gen::intBetween($min, $max) | [$min, $max] | toward 0, clamped to range |
Gen::intPositive() | 1..PHP_INT_MAX | toward 1 |
Gen::float() | [0.0, 1.0) | toward 0.0 |
Gen::floatBetween($min, $max) | [$min, $max) — $max itself is never drawn | toward the point of [$min, $max) nearest to 0.0 — never $max |
Gen::bool() | true / false | true -> false |
Gen::string() | Unicode, length 0..100 — half the characters ASCII printable, a tenth troublemakers (quotes, backslash, combining marks, zero-width joiner, right-to-left override, byte order mark, astral emoji), a tenth Latin-1/Latin Extended, a tenth the rest of the BMP, a fifth uniform over U+0001..U+10FFFF | toward '', then by length, then each character toward a |
Gen::stringAscii() | printable ASCII, length 0..100 | toward '', then by length, then each character toward a |
Gen::stringOf($minLength, $maxLength) | Unicode, bounded length | toward '', then by length, then each character toward a |
Gen::stringFrom($alphabet, $minLength, $maxLength) | characters from a fixed alphabet (multibyte OK) | toward '', then by length, then each character toward the first alphabet character |
Gen::bytes($minLength, $maxLength) | raw byte strings (bytes 0..255) | toward '', then by length, then each byte toward "\x00" |
Gen::arrayOf($element, $minSize, $maxSize) | lists of $element, size 0..100 by default | toward [], then by length, then each element |
Gen::nonEmptyArrayOf($element, $maxSize) | non-empty lists | by length (never below 1), then each element |
Gen::uniqueArrayOf($element, $minSize, $maxSize, $by) | lists of pairwise-distinct elements — by === on the values, or with by: fn ($v) => $v->id by the int|string key the closure returns (any other key type is refused at generation time) | like arrayOf, but element candidates colliding with another element (or key) are skipped |
Gen::dictOf($key, $value, $minSize, $maxSize) | maps with distinct keys from $key (int/string) and values from $value, size 0..100 by default | toward [], then by size, then each value (keys fixed) |
Gen::record($shape) | fixed-shape map ['field' => $arb, ...] | each field via its arbitrary, key set fixed |
Gen::elements($array) | one value from an array (array form of oneOf); an ArbitraryInterface among them is refused | toward earlier-listed distinct values |
Gen::enum(SomeEnum::class) | OneOfArbitrary over the enum's cases | toward earlier-declared cases (declare simpler cases first) |
Gen::constant($value) | always $value | does not shrink |
Gen::withEdgeCases($inner, ...$edgeCases) | $inner with author-supplied edge values: one draw in five is one of them | through the edge values first, in the listed order, then the inner tree |
Gen::composite($body) | a value built by a body that draws dependent values through a Draw — composite generators | the draws, earliest first; later draws re-drawn through the new range |
Gen::randomEngine() / Gen::randomizer() | a Random\Engine (or the Randomizer over it) drawn from the tape — shrinkable randomness | each engine call is a draw#N of eight bytes shrinking toward "\0" |
Gen::char() | a single printable ASCII character | toward a |
Gen::uuid() | RFC 4122 v4 UUID strings | does not shrink |
Gen::datetime($min, $max) | UTC DateTimeImmutable, timestamp in [$min, $max] | toward the Unix epoch, clamped |
Gen::floatSpecial() | OneOfArbitrary over NAN, ±INF, -0.0 and the float representation edges | toward earlier-listed specials |
Gen::intRange($min, $max) | ordered pairs [lo, hi] with lo <= hi | both bounds shrink, order always holds |
Gen::recursive($leaf, $wrap, $maxDepth) | bounded recursive structures: $wrap lifts the previous level's arbitrary | within the branch that generated the value |
Gen::oneOf(...$values) | one of the given values, not generators — an ArbitraryInterface among them is refused, because accepted it would become the generated value itself. Pick between generators with Gen::frequency() | toward earlier-listed distinct values (put simpler values first) |
Gen::nullable($inner) | null or an $inner value | prefers null, then the inner tree |
Gen::map($inner, $map) | $inner transformed by $map | through the inner tree, re-applying $map |
Gen::flatMap($inner, $flatMap) | dependent generator returned by $flatMap($innerValue) | source value first (dependent value regenerated), then the dependent tree |
Gen::filter($inner, $predicate) | $inner values satisfying $predicate (throws GenerationExhaustedException after 100 rejected draws — never yields an out-of-domain value) | inner tree, pruning candidates that fail the predicate |
Gen::tuple(...$elements) | fixed-arity tuple, one value per element | each position via its element, arity fixed |
Gen::frequency($pairs) | weighted choice over [weight, arbitrary] pairs | within the branch that generated the value |
Gen::ipv4() | IPv4 dotted-quad strings | each octet toward 0 |
Gen::ipv6() | IPv6 addresses in the canonical RFC 5952 text form (lowercase, no leading zeros, longest zero run compressed to ::) | each group toward 0, ending at :: |
Gen::email() | local@label.tld addresses | toward the shortest local/label and first TLD |
Gen::url() | http(s)://host.tld[/path] URLs | toward http://a.com |
Gen::json($maxDepth) | a JSON-encodable value (null/bool/int/float/string/list/object) | within the generated structure |
Gen::jsonString($maxDepth) | the json_encode text of Gen::json() | through the value's tree |
Gen::regex($pattern) / Gen::stringMatching($pattern) | strings matching a regex subset (compiled to combinators). Write the pattern without delimiters — [a-z]+, not /[a-z]+/; a delimited one is refused rather than compiled with the delimiters as literals. . and a negated class draw from printable ASCII (0x20..0x7E, never a newline) | shorter/simpler matches (via the compiled trees) |
Gen::subset($values, $minSize, $maxSize) | subsets of a fixed ordered set — distinct members of $values in source order; duplicates in the source are rejected | size first (toward the empty set), then each kept element toward earlier source positions — the minimal subset is a short prefix |
Gen::commands($initialModel, $commandGenerators, $minLength, $maxLength) | valid command sequences for stateful testing — see State machine | drops command blocks, then simplifies each command |
Gen::rules($machine, $minLength, $maxLength) | a RuleSequence over a rule-based machine class — see Rule-based machines | like commands: drops steps, then simplifies each step's arguments |
Gen::swarm($arbitrary) | swarm testing: each case may use only a non-empty subset of the wrapped choice generator's variants — see Swarm testing | inside the subset the case came from, never widening back |
Gen::forClass($class, $overrides, $skipInvalid, $maxDepth) | instances built through a constructor, one generator per parameter: an override, then the @param psalm type (int<0, 100> beats a bare int), then the native type — see Generating from a class | through each parameter's own tree |
Gen::forParameters($reflectionFunction, $overrides) | the same resolution over any function, method or closure, returned as array<string, ArbitraryInterface> in signature order; overrides may be partial | per parameter, as above |
Numeric generators (int*, float*) are boundary-biased: roughly one draw in five returns an in-range edge value (0, ±1, min, max for ints; 0.0 or min for floats), where bugs cluster, instead of a uniform one. Shrinking is unaffected. See Boundary bias.
Sized generators guarantee their minimum: uniqueArrayOf/dictOf (distinct elements/keys) and commands (applicable steps) may fall short of the drawn size when the value space runs out, but never fall below $min — an unreachable minimum throws GenerationExhaustedException rather than hand the property a too-small value.
All of the above live on Gen in rasuvaeff/property-testing-core — the same facade regardless of which adapter's #[Property]/forAll() calls it.