rasuvaeff / property-testing-names
Localised person-name generators (en, ru) for rasuvaeff/property-testing-core: first, last, patronymic and gender-consistent full names
Package info
github.com/rasuvaeff/property-testing-names
pkg:composer/rasuvaeff/property-testing-names
Requires
- php: 8.3 - 8.5
- ext-mbstring: *
- rasuvaeff/property-testing-core: ^0.1 || ^0.2
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.33 || ^0.34
- maglnet/composer-require-checker: ^4.17
- rasuvaeff/property-testing-testo: ^0.1
- rasuvaeff/rector-named-literals: ^1.0
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.39 || ^1.0
- vimeo/psalm: ^6.16
This package is auto-updated.
Last update: 2026-08-15 13:56:48 UTC
README
Person-name generators for the
property-testing engine:
given names, surnames and patronymics for en and ru, with grammatical
gender kept consistent across the parts of one name. Names shrink toward the
shortest entries of their dataset, so a counterexample minimises into a plain
name instead of turning into random text.
Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model.
Why a separate package
The core Gen facade holds format-derived primitives — Gen::email(),
Gen::uuid(), Gen::ipv4() — things a formula produces. Name lists are
versioned data with their own update policy, so they live here, behind their
own Names:: facade. There is no Gen::name(): a core method that silently
depends on an optional data package would pass composer-require-checker in
your project and fail at runtime.
| Package | Use it when |
|---|---|
rasuvaeff/property-testing-core |
The engine itself: arbitraries, shrinking, corpus |
rasuvaeff/property-testing-testo |
You test with Testo — the #[Property] attribute |
rasuvaeff/property-testing-phpunit |
You test with PHPUnit — the forAll()->check() trait |
rasuvaeff/property-testing-names (this package) |
Your inputs are people: forms, profiles, auth, validators, reports |
Requirements
- PHP 8.3 – 8.5
ext-mbstringrasuvaeff/property-testing-core^0.1 || ^0.2
Installation
composer require --dev rasuvaeff/property-testing-names
Usage
Every factory returns an ArbitraryInterface — a recipe for values, not a
value. The comments below show what each one generates:
use Rasuvaeff\PropertyTesting\Names\Names; use Rasuvaeff\PropertyTesting\Names\Gender; $firstNames = Names::first(); // 'Ian', 'Emma', … $surnames = Names::last(locale: 'ru'); // 'Попов', 'Иванова', … $femaleNames = Names::first('ru', Gender::Female); // 'Мария', 'Ольга', … $patronymics = Names::middle('ru'); // 'Ивановна', 'Петрович', … $displayNames = Names::full('ru', middle: true); // 'Иван Иванович Иванов' $people = Names::person('ru', middle: true); // PersonName objects
The runner draws from them; nothing is generated until it does. To see values while debugging, sample explicitly:
Gen::sample($displayNames, 3, 6); // fixed seed → the same three names every time
Inside a property test the factories go into the generators method, exactly like the core ones:
use Rasuvaeff\PropertyTesting\ArbitraryInterface; use Rasuvaeff\PropertyTesting\Names\Names; use Rasuvaeff\PropertyTesting\Property; use Testo\Assert; #[Property(runs: 300)] public function displayNameFitsTheColumn(string $first, string $last): void { Assert::true(mb_strlen((new Profile($first, $last))->displayName()) <= 255); } /** @return array<string, ArbitraryInterface> */ public static function displayNameFitsTheColumnGenerators(): array { return [ 'first' => Names::first(locale: 'ru'), 'last' => Names::last(locale: 'ru'), ]; }
API
| Factory | Returns | Notes |
|---|---|---|
Names::first(string $locale = 'en', ?Gender $gender = null) |
ArbitraryInterface<non-empty-string> |
Without a gender the male and female lists are merged |
Names::last(string $locale = 'en', ?Gender $gender = null) |
ArbitraryInterface<non-empty-string> |
Inflected per gender where the locale requires it |
Names::middle(string $locale, ?Gender $gender = null) |
ArbitraryInterface<non-empty-string> |
Patronymics; the locale is required because the dataset is not universal |
Names::full(string $locale = 'en', ?Gender $gender = null, bool $middle = false) |
ArbitraryInterface<non-empty-string> |
First [Middle] Last, rendered from person() |
Names::person(string $locale = 'en', ?Gender $gender = null, bool $middle = false) |
ArbitraryInterface<PersonName> |
The parts, kept together |
PersonName is a final readonly class with $first, $middle (nullable),
$last, $gender and three display forms:
| Method | en |
ru |
|---|---|---|
full() |
John Smith |
Иван Иванович Иванов |
initialLast() |
J. Smith |
И. Иванов |
lastInitials() |
Smith J. |
Иванов И. И. |
Any other form is one Gen::map() away:
Gen::map(Names::person(), static fn (PersonName $p): string => $p->last . ', ' . $p->first);
Locales
| Locale | Given names | Surnames | Patronymics |
|---|---|---|---|
en |
50 male + 50 female | 100, shared by both genders | — |
ru |
50 male + 50 female | 50 + 50, index-aligned pairs | 40 + 40, index-aligned pairs |
An unregistered locale raises InvalidArgumentException when the arbitrary is
built, not when it first generates a value; the same is true for asking
en for middle names. Locale tags are matched literally: 'EN', 'en-US' and
'en ' are all unknown.
Gender consistency
Names::first() and Names::last() are independent draws — combining them by
hand can produce Мария Иванов, which no Russian form renders. When the parts
must agree, draw them together:
$person = Names::person('ru', middle: true); // Мария Ивановна Иванова
Gender has two cases, Male and Female, declared in that order because
shrinking walks toward the first case.
Security
The lists are synthetic test data: they are not a register of real people and make no claim of cultural completeness. Generated values are printable UTF-8 without control characters, so they are safe to embed in test reports and failure messages — but they are still generated input, and code under test should validate them like any other user data.
Dataset changes alter the values a given seed produces, so they ship as minor releases and are listed in CHANGELOG.md.
Examples
Runnable scripts live in examples/.
Development
make build # validate + normalize + require-checker + cs + psalm + test make cs-fix make psalm make test make test-coverage make mutation make release-check
No PHP on the host is required — every target runs in the composer:2 Docker
image.
License
BSD-3-Clause. See LICENSE.md.