Skip to content

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 →