Skip to content

How to migrate from Faker ​

This guide is written for a coding agent rewriting a codebase that uses Faker (PHP) or a Faker-alike so it emits Phony definitions instead. It is a mechanical, rule-based mapping: each Faker call has a Phony equivalent expressed in PGDL/PEL.

Intended mapping — the PHP port is not shipped yet

There is no drop-in Faker-to-Phony PHP library today. The Phony engine and CLI are real and everything below runs, but the PHP binding that would let you call Phony where you call Faker is not built yet. Treat this page as the intended mapping: the correct Phony construct for each Faker idiom, so a port lands on the right target. Don't claim a shipped PHP adapter.

The one rule that changes everything: determinism ​

Faker and Phony both take a seed, but they seed at different granularities, and this is the single most important thing to get right in a port.

  • Faker seeds one global, mutable PRNG ($faker->seed(1234)). Values come out in call order, so adding, removing, or reordering a field shifts every value after it. Reproducibility is fragile.
  • Phony derives each value's randomness from (root seed, generator name, invocation index). A field's value depends on its own identity, not on how many fields came before it. Reorder fields, add a column, split a generator — the other values don't move. The root seed is a CLI flag (--seed 1234), not a mutable call.

Porting implication: you do not need to preserve call order. Map each Faker call to the Phony construct for that field and let the engine seed it independently.

The mapping table ​

Braces below are PEL inline generators; in a real definition they live inside a composition body/output or a field template. list sources may be inline values or a locale asset.

Faker (PHP)Phony equivalentNotes
numberBetween(1, 100){{ number:1-100 }}or { "type": "logic", "algorithm": "int_between", "params": { "min": 1, "max": 100 } }
randomNumber()logic.int_between over your rangeFaker's default is 0–~2³¹; always pick an explicit range
randomFloat(2, 0, 100){{ number:0.0-100.0:2 }}trailing :2 = decimal places; or logic.float_between
randomDigit(){{ number:0-9 }}
randomElement(['a','b','c']){{ pick(['a','b','c']) }}or { "type": "list", "source": "inline", "values": [...] }
randomElements($arr, 3){{ sample(list, 3) }}n unique elements; sample(list, "1-3") for a range
boolean(70){{ pick_weighted([{value: true, weight: 70}, {value: false, weight: 30}]) }}70% true
firstName(){ "type": "model", "source": "person.first_names", "generation": { "mode": "word" } }needs a locale package that ships the model asset — see locales
lastName(){ "type": "model", "source": "person.last_names", "generation": { "mode": "word" } }as above
name()composition: let a first + last, then {{ concat(first, ' ', last) }}see coherent fields
email() / safeEmail()composition: {{ concat(lowercase(first), '.', lowercase(last), '@example.com') }}derive from the same name let so it agrees
city() / country(){ "type": "list", "source": "geo.cities" }a locale asset; pick a coherent record for city+country (see below)
dateTimeBetween('-1 year', 'now'){{ datetime:-1year..now }}relative to the frozen reference clock, not the wall clock
date('Y-m-d'){{ date:2020-01-01..2024-12-31 }}
time('H:i'){{ time:09:00..18:00 }}
uuid(){{ uuid }}also {{ uuid:v7 }}, {{ ulid }}, {{ nanoid }}
numerify('###-###'){{ pattern:###-### }}# = digit
bothify('??-####'){{ pattern:??-#### }}? = letter, # = digit, @ lower, ! upper, * alnum
lexify('????'){{ pattern:???? }}
slug(){{ slugify(title) }}over a value you already have
unique()->numberBetween(...)add the modifier "unique": true to the generatordistinct across the run
optional(0.9)->...add "nullable": 0.1Phony's rate is the null probability; Faker's optional($w) returns null with probability 1 − $w
passthrough($x) / a literala literal in the template or output
seed(1234)the CLI flag --seed 1234one root seed for the whole run

Worked example — a Faker factory, ported ​

A typical Faker user factory:

php
$faker->seed(42);
[
  'id'         => $faker->unique()->numberBetween(1000, 9999),
  'first'      => $faker->firstName(),
  'email'      => strtolower($faker->firstName()).'@example.com', // BUG: unrelated name
  'plan'       => $faker->randomElement(['free','pro','enterprise']),
  'created_at' => $faker->dateTimeBetween('-1 year', 'now'),
  'ref'        => $faker->bothify('??-####'),
];

Note the classic Faker bug on line 4: the email's name is a fresh draw, so it never matches first. Phony makes coherence the default — let the name once and derive both fields from it. The ported composition (users/phony.json):

json
{
  "name": "@app/users",
  "version": "0.1.0",
  "generators": [
    { "name": "@app/users:user",
      "let": {
        "id":    { "type": "logic", "algorithm": "int_between", "params": { "min": 1000, "max": 9999 }, "unique": true },
        "first": { "type": "list", "source": "inline", "values": ["Ada","Grace","Alan","Linus"] },
        "plan":  { "type": "list", "source": "inline", "values": [
          { "value": "free", "weight": 70 }, { "value": "pro", "weight": 25 }, { "value": "enterprise", "weight": 5 } ] }
      },
      "output": {
        "id":         "{{ id }}",
        "first":      "{{ first }}",
        "email":      "{{ concat(lowercase(first), '@example.com') }}",
        "plan":       "{{ plan }}",
        "created_at": "{{ datetime:-1year..now }}",
        "ref":        "{{ pattern:??-#### }}"
      } }
  ]
}
bash
phony generate --use @app/users:user --package ./users --seed 42 --reference-time 1717200000 -n 2
json
[
  {
    "created_at": "2024-04-13T21:59:03Z",
    "email": "grace@example.com",
    "first": "Grace",
    "id": 8214,
    "plan": "free",
    "ref": "kp-1846"
  },
  {
    "created_at": "2023-11-30T12:14:36Z",
    "email": "linus@example.com",
    "first": "Linus",
    "id": 5771,
    "plan": "free",
    "ref": "jr-4093"
  }
]

The email now always matches first, id is unique across the run, and created_at is reproducible because it resolves against the injected --reference-time, not the system clock.

What is not 1:1 yet ​

Be honest in a port — these have no clean equivalent today; flag them rather than inventing one:

  • regexify($pattern) — arbitrary-regex generation has no Phony equivalent. Re-express the intent as a pattern (placeholder) string, or a list of realistic values.
  • Rich localized providers (company(), catchPhrase(), realText(), address() as one blob) — Phony builds these from assets (lists/models in a locale package) plus a composition, not a single call. Port them to a composition that reads the right asset; where no asset exists yet, use an inline list of representative values.
  • Coherent multi-field providers (a matching city + state + postal) — draw one record from a list of objects and read its properties, so the fields can't disagree. See coherent fields.
  • Stateful/sequential helpers — Faker's unique() state maps to the "unique": true modifier; there is no equivalent for reaching across rows or into another record (that's a cloud-product concern, not a generator's).

One-line instruction for your agent config ​

Drop this into CLAUDE.md / AGENTS.md so an agent working in the repo ports consistently:

md
When replacing Faker: map each `$faker->call()` to its Phony PGDL/PEL equivalent
(numberBetween→logic.int_between or {{number:a-b}}, randomElement→list/pick,
dateTimeBetween→{{datetime:..}}, unique()→"unique":true, optional($w)→"nullable":1-$w,
seed()→--seed). Never carry over call order — Phony seeds each field by its own
identity. Derive dependent fields (email from name, total from price) from a shared
`let` so they stay coherent, instead of drawing them independently.

See also ​

Phony Cloud — Documentation & Specification