Mock Data Generator — PHP source
Generate deterministic fake data (names, emails, numbers, dates, booleans, UUIDs, pick-from-list) from a schema and a seed. Reproducible output.
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* mock-data-generator — PHP polyglot showcase port.
*
* CosmoDev polyglot showcase port of mock-data-generator.
* Ported from src/lib/mockData.ts (the canonical TypeScript reference).
*
* Pure mock-data generation — deterministic and seed-stable. Identical
* (schema, count, seed) always yields byte-identical output, so a seed
* reproduces a fixture exactly. There is no time/rand side-effect: the only
* source of "randomness" is a seeded mulberry32 PRNG, which is fast but NOT
* cryptographically secure — mock data is not a secret, so speed wins.
*
* The reference JS does its arithmetic in 32-bit integer semantics (`|0`,
* `>>>`, `Math.imul`). PHP integers are 64-bit, so the PRNG is expressed with
* explicit `& 0xFFFFFFFF` masking and a low-32-bit multiply helper to keep the
* bit stream identical to the sibling ports.
*
* Display source — part of CosmoDev's polyglot tool pages.
*
* Usage:
*
* $rows = generate_mock($schema, 5, 1234);
*/
const FIRST_NAMES = [
'Ava', 'Liam', 'Noah', 'Emma', 'Olivia', 'Aiden', 'Sophia', 'Mason', 'Isabella',
'Lucas', 'Mia', 'Ethan', 'Amelia', 'Leo', 'Harper', 'Ezra', 'Ella', 'Owen',
'Luna', 'Finn', 'Zoe', 'Jude', 'Nora', 'Kai', 'Ruby', 'Theo', 'Ivy', 'Max',
];
const LAST_NAMES = [
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
'Rodriguez', 'Martinez', 'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson',
'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin', 'Lee', 'Perez', 'Thompson',
'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez',
];
/**
* Low 32 bits of a product (emulates JS `Math.imul`).
* The low-32-bit truncation is identical whether operands are read as signed
* or unsigned, so the unsigned mask alone reproduces the reference.
*/
function imul32(int $x, int $y): int
{
return ($x * $y) & 0xFFFFFFFF;
}
/**
* Return a deterministic PRNG closure yielding floats in [0, 1).
*
* The state is captured by reference (`use (&$a)`) and advanced one draw per
* call, mirroring the JS reference's left-to-right consumption order — the
* thing that pins reproducibility. All arithmetic stays within 32 bits via the
* mask, reproducing JS `|0` wraparound exactly.
*/
function mulberry32(int $seed): callable
{
$a = $seed & 0xFFFFFFFF;
return function () use (&$a): float {
$a = ($a + 0x6D2B79F5) & 0xFFFFFFFF;
$t = imul32($a ^ ($a >> 15), 1 | $a);
$t = (($t + imul32($t ^ ($t >> 7), 61 | $t)) ^ $t) & 0xFFFFFFFF;
return ($t ^ ($t >> 14)) / 4294967296.0;
};
}
/**
* True for finite ints/floats only — mirrors JS `Number.isFinite`.
* Rejects null, strings, bool, NaN and INF so a stray value can never
* masquerade as a numeric bound.
*/
function is_finite_number(mixed $value): bool
{
return is_int($value) || (is_float($value) && is_finite($value));
}
/**
* Resolve [min, max] into an ordered integer range with given defaults.
*
* Missing or non-finite bounds collapse to `$def_lo`/`$def_hi` (0/100 for
* integers, 1/99 for usernames). The returned pair is always lo <= hi so the
* caller's span math can never invert.
*
* @return array{0:int,1:int}
*/
function clamp_int(mixed $min, mixed $max, int $def_lo, int $def_hi): array
{
$lo = is_finite_number($min) ? (int) floor((float) $min) : $def_lo;
$hi = is_finite_number($max) ? (int) floor((float) $max) : $def_hi;
if ($lo > $hi) {
[$lo, $hi] = [$hi, $lo];
}
return [$lo, $hi];
}
/**
* Build a v4-shaped UUID from the PRNG stream.
*
* Sets the version nibble (position 12 -> '4') and a variant nibble
* (position 16 -> 8/9/a/b) so the result parses as a legal RFC 4122 v4 UUID,
* even though the bytes are deterministic, not secret.
*/
function uuid_from_rng(callable $rng): string
{
$hex = '';
for ($i = 0; $i < 32; $i++) {
$hex .= dechex((int) ($rng() * 16));
}
$hex[12] = '4';
$hex[16] = '89ab'[(int) ($rng() * 4)];
return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' .
substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20, 12);
}
/**
* Coerce a column name into an identifier-safe key.
* Strips everything outside [A-Za-z0-9_$]; an all-stripped name becomes fieldN.
*/
function sanitize_field_name(mixed $name, int $idx): string
{
$name = is_string($name) ? $name : (string) $name;
$cleaned = preg_replace('/[^A-Za-z0-9_$]/', '', $name);
return $cleaned !== '' ? $cleaned : "field{$idx}";
}
/**
* Parse an ISO YYYY-MM-DD string to UTC milliseconds.
*
* Mirrors JS `Date.parse` of a date-only ISO string (interpreted as UTC
* midnight). The `!` resets unparsed fields to 00:00:00 so the time component
* never leaks the current wall-clock. Empty/unparseable input falls back to
* the default rather than corrupting the stream.
*/
function parse_date_ms(mixed $value, string $fallback = '2000-01-01'): int
{
$text = (is_string($value) && $value !== '') ? $value : $fallback;
$utc = new DateTimeZone('UTC');
$dt = DateTimeImmutable::createFromFormat('!Y-m-d', $text, $utc);
if ($dt === false) {
$dt = DateTimeImmutable::createFromFormat('!Y-m-d', $fallback, $utc);
}
return ((int) $dt->getTimestamp()) * 1000;
}
/**
* Generate `$count` rows of mock data from a schema.
*
* The PRNG is seeded once and consumed left-to-right, row by row and field by
* field — that consumption order is what makes output reproducible: inserting
* or reordering a field shifts every later value. Negative or zero counts
* yield an empty array. This function never throws on valid input.
*
* @param array<int, array<string, mixed>> $schema
* @return list<array<string, mixed>>
*/
function generate_mock(array $schema, int $count, int $seed): array
{
$rng = mulberry32($seed);
$n = max(0, (int) floor($count));
$rows = [];
for ($i = 0; $i < $n; $i++) {
$row = [];
foreach ($schema as $f => $spec) {
$key = sanitize_field_name($spec['name'] ?? '', $f);
$row[$key] = generate_field($spec, $i, $rng);
}
$rows[] = $row;
}
return $rows;
}
/**
* Produce one value for one field.
*
* Each case consumes a fixed number of PRNG draws, keeping the stream aligned
* across rows and identical to the reference implementation. `(int)(rng()*n)`
* truncates toward zero, which equals `Math.floor` for non-negative draws.
*
* @param array<string, mixed> $spec
*/
function generate_field(array $spec, int $index, callable $rng): mixed
{
switch ($spec['type'] ?? '') {
case 'index':
return $index;
case 'firstName':
return FIRST_NAMES[(int) ($rng() * count(FIRST_NAMES))];
case 'lastName':
return LAST_NAMES[(int) ($rng() * count(LAST_NAMES))];
case 'fullName':
$first = FIRST_NAMES[(int) ($rng() * count(FIRST_NAMES))];
$last = LAST_NAMES[(int) ($rng() * count(LAST_NAMES))];
return "$first $last";
case 'username':
$first = strtolower(FIRST_NAMES[(int) ($rng() * count(FIRST_NAMES))]);
[$lo, $hi] = clamp_int($spec['min'] ?? null, $spec['max'] ?? null, 1, 99);
$num = $lo + (int) ($rng() * ($hi - $lo + 1));
return $first . $num;
case 'email':
$first = strtolower(FIRST_NAMES[(int) ($rng() * count(FIRST_NAMES))]);
$last = strtolower(LAST_NAMES[(int) ($rng() * count(LAST_NAMES))]);
return "$first.$last@example.com";
case 'integer':
[$lo, $hi] = clamp_int($spec['min'] ?? null, $spec['max'] ?? null, 0, 100);
return $lo + (int) ($rng() * ($hi - $lo + 1));
case 'number':
$min = is_finite_number($spec['min'] ?? null) ? (float) $spec['min'] : 0.0;
$max = is_finite_number($spec['max'] ?? null) ? (float) $spec['max'] : 1.0;
$lo = min($min, $max);
$hi = max($min, $max);
$value = $lo + $rng() * ($hi - $lo);
// Round to 4 decimals; floor(x + 0.5) reproduces JS Math.round
// (round-half-up toward +inf) across the whole real line.
return floor($value * 10000 + 0.5) / 10000;
case 'boolean':
return $rng() < 0.5;
case 'uuid':
return uuid_from_rng($rng);
case 'date':
$from = parse_date_ms($spec['from'] ?? '', '2000-01-01');
$to = parse_date_ms($spec['to'] ?? '', '2025-12-31');
$lo = min($from, $to);
$hi = max($from, $to);
$ms = $lo + (int) floor($rng() * ($hi - $lo));
// gmdate formats a Unix timestamp in UTC, matching new Date(ms).
return gmdate('Y-m-d', (int) ($ms / 1000));
case 'pick':
$options = $spec['options'] ?? [];
if (count($options) === 0) {
return null;
}
return $options[(int) ($rng() * count($options))];
case 'string':
$raw_len = $spec['length'] ?? null;
$length = is_finite_number($raw_len) ? (int) floor((float) $raw_len) : 8;
if ($length < 1) {
$length = 1;
}
$chars = 'abcdefghijklmnopqrstuvwxyz';
$s = '';
for ($i = 0; $i < $length; $i++) {
$s .= $chars[(int) ($rng() * strlen($chars))];
}
return $s;
default:
return null;
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →