Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# AGENTS.md

Instructions for AI coding agents working **on this repository** (`xefi/faker-php`).
If you are working on a project that *consumes* this package, read the "Using Faker PHP with an AI agent"
section of the [README](README.md) instead.

## What this repository is

`xefi/faker-php` is a standalone, framework-agnostic fake data generator for PHP 8.3+.
It is the core package only. Locales, framework bridges and extra generators live in separate
repositories (`xefi/faker-php-laravel`, `xefi/faker-php-symfony`, `xefi/faker-php-locales-*`,
`xefi/faker-php-images`, ...). Do not add locale data or framework-specific code here.

## Commands

```bash
composer install # also regenerates the IDE mixin via post-install-cmd
composer test # PHPUnit 12 test suite
composer generate-mixin # regenerate faker_mixin.php by hand
```

CI runs the suite on PHP 8.3, 8.4 and 8.5, against both `lowest` and `highest` dependencies,
and runs `composer validate`. Keep changes compatible with all three PHP versions.

## Architecture

```
Faker ──(__call, one fresh Container per call)──▶ Container
│
registered providers ──▶ extensions ──▶ method map (method → extension name)
│
run(): generate ──▶ modifiers ──▶ strategies (retry until pass)
```

- **`Faker`** (`src/Faker.php`) is a thin entry point. Every call builds a new `Container`, so
modifiers and strategies never leak between two calls.
- **`Container`** (`src/Container/Container.php`) composes four traits: `HasExtensions`,
`HasModifiers`, `HasStrategies`, `HasLocale`. `run()` generates a value, applies the modifiers,
then loops until the strategies pass — capped at 20 000 tries before throwing `MaximumTriesReached`.
- **Extensions** (`src/Extensions/`) hold the actual generators. Every **public** method of an
extension becomes a Faker method; `getName()` and `__construct` are skipped.
- **Providers** (`src/Providers/`) register extensions through `Container::starting()`.
- **`PackageManifest`** discovers providers from `extra.faker.providers` in every installed package's
`composer.json` *and* in the consuming project's own `composer.json`, then caches the result in
`packages.php`.
- **`ContainerMixinManifest`** generates `faker_mixin.php`, a `@method` docblock stub that gives IDEs
autocompletion for whatever extensions are installed. Both manifests recompile when
`vendor/composer/installed.json` or the project `composer.json` is newer than the generated file.

`packages.php` and `faker_mixin.php` are generated artifacts and are git-ignored. Never commit them.

## Conventions

- No `declare(strict_types=1)` in `src/`: do not introduce it there. A few test files do declare it;
follow the file you are editing.
- Native parameter and return types plus a full docblock (`@param`, `@return`) on every new or modified
method. Some older methods (`email()`, `macAddress()`, `letter()`, ...) still lack a return type; add
one when you touch them, do not copy them.
- Randomness goes through the injected `Random\Randomizer` (`$this->randomizer`), never `rand()`,
`mt_rand()`, `random_bytes()`, `array_rand()` or `shuffle()`, so a seeded engine gives reproducible
output. Use the `Extension` helpers (`pickArrayRandomElement`, `pickArrayRandomKeys`, `formatString`,
...) when they fit. `uuid()`, `ulid()`, `phoneNumber()` and `url()` still break this rule; they are
known debt, not examples.
- Method names are **globally unique across all extensions**, including third-party ones. For a regular
extension, a collision triggers an `E_USER_WARNING` and the **last** registration overwrites the
method; for locale variants, the method already mapped is kept silently. Pick names that are unlikely
to clash with an extension package, and never rename an existing method without a major version
bump.

### Adding a generator

1. Add a public method to the relevant class in `src/Extensions/`, or create a new `*Extension`
extending `Xefi\Faker\Extensions\Extension`.
2. Register a new extension class in `FakerServiceProvider::boot()`.
3. Add a unit test under `tests/Unit/Extensions/`.
4. Locale-aware extensions use the `Extensions\Traits\HasLocale` trait; the container resolves the
variant matching the current locale and falls back to `Locales::DEFAULT`. Locale data itself
belongs in a `xefi/faker-php-locales-*` package, not here.

## Tests

- PHPUnit 12, tests live in `tests/Unit/` and mirror `src/`.
- Extend `Xefi\Faker\Tests\Unit\TestCase`: it points the container base path at `tests/Support` and
redirects both generated manifests to `/tmp` so the real ones are never touched.
- Tests that need a full project layout (vendor dir, `installed.json`, `composer.json`) use
`tests/Support/Concerns/CreatesTemporaryProjects`.
- Extension tests generally assert against the extension's own data set, read back through reflection,
rather than hardcoding expected values.

## Gotchas

- **`Container::$extensions` and `$extensionsMethods` are static**, so the registered extension set is
process-global and survives between tests. `forgetExtensions()` exists for that reason.
- **Bootstrappers are static too, and they accumulate.** Every provider boot appends a
`Container::starting()` callback. To re-initialize extensions through the constructor in a test, call
`forgetExtensions()` *and* `forgetBootstrappers()`, otherwise the providers register twice and every
extension triggers an "already registered" warning.
- **`unique()` draws from a process-global, seed-keyed pool that is never reset** (`Seeds\HasSeeds`).
Over a bounded set — an enum, a fixed list, a small integer range — it exhausts and then throws
`MaximumTriesReached` in every later call of the same process. Never suggest `unique()` on a bounded
set; widen the range or iterate over the set instead.
- Modifiers are applied **before** strategies are checked, so a strategy always sees the final value.

## Pull requests

- Target `main`, one focused change per PR.
- Keep the README, the docs site (`faker-php.xefi.com`, separate repository) and this file in sync when
the public API changes.
- Do not add AI attribution to commits or PR descriptions.
85 changes: 84 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,94 @@ $faker = new \Xefi\Faker\Faker();
$faker->name() // John Doe
$faker->name() // Charles Brown

$faker->sentence() // equus canis populus servus aquaeductus fidelitas
$faker->sentences() // Nulla facilisi. Proin nec ante at erat pharetra interdum. Etiam nec ligula felis.

$faker->iban() // PX41711762752955497163783543
```

## Why Faker PHP?

`fakerphp/faker` is the historical PHP faker and it works. Faker PHP is a different set of trade-offs,
built from scratch for PHP 8.3+:

- **Modular by design.** The core package ships 13 extensions and *no* locale data. Locales
(`xefi/faker-php-locales-fr-fr`, `-en-us`, `-de-de`, `-es-es`, ...), extra generators
(`xefi/faker-php-images`, `xefi/faker-php-files`, `xefi/faker-php-currencies`) and framework bridges
(`xefi/faker-php-laravel`, `xefi/faker-php-symfony`) are opt-in packages, auto-discovered through
Composer. You install what you use.
- **Autocompletion that matches your install.** A `faker_mixin.php` stub is regenerated on every
`composer install`/`update`, with a typed `@method` entry for every generator actually available in
your project — including the ones your own extensions add. No plugin, no stale stubs.
- **Extensible without forking.** An extension is a class whose public methods become Faker methods.
Register it through `extra.faker.providers` — in a published package *or* directly in your own
application's `composer.json` — and it is available on `$faker` everywhere.
- **Locale resolution per generator.** Each extension can ship a locale variant; the container picks
the one matching the current locale and falls back to the default, so a partial translation still
works.
- **Modern randomness and types.** Every generator draws from PHP's `Random\Randomizer`, and the whole
public API is natively typed.
- **No state bleeding between calls.** Each `$faker->...()` call runs in a fresh container, so
modifiers and strategies never leak into the next call.

## Coming from fakerphp/faker

The concepts map closely — most of a migration is a rename:

| `fakerphp/faker` | Faker PHP |
| --- | --- |
| `$faker->name()` / `firstName()` / `lastName()` / `title()` | same |
| `$faker->word()` / `words(3)` | `words(3)` (string) / `wordsAsArray(3)` |
| `$faker->sentence()` / `paragraph()` | `sentences(3)` / `paragraphs(3)` (+ `*AsArray()`) |
| `$faker->numberBetween($min, $max)` | `number($min, $max)` |
| `$faker->randomFloat($decimals, $min, $max)` | `float($min, $max, $decimals)` |
| `$faker->randomDigit()` | `digit()` |
| `$faker->randomElement($array)` | `randomElement($array)` / `randomKey($array)` |
| `$faker->boolean()` | same |
| `$faker->dateTimeBetween($from, $to)` | `dateTime($from, $to)`, or `dateTimeImmutable($from, $to)` |
| `$faker->unixTime()` / `timezone()` | `timestamp()` / `timezone()` |
| `$faker->uuid()` | `uuid()` (and `ulid()`) |
| `$faker->md5()` / `sha1()` / `sha256()` | same (and `sha512()`) |
| `$faker->email()` / `url()` / `ipv4()` / `ipv6()` / `macAddress()` | same (plus `ip()`, either family) |
| `$faker->domainName()` / `tld()` | `domain()` / `tld()` |
| `$faker->hexColor()` / `safeHexColor()` / `colorName()` / `rgbColor()` / `hslColor()` | same |
| `$faker->iban($countryCode)` | `iban($countryCode, $format)` |
| `$faker->phoneNumber()` | `phoneNumber()` (+ `cellPhoneNumber()`, `landlinePhoneNumber()` and their spaced/dotted/dashed/indicatored variants) |
| `$faker->latitude()` / `longitude()` / `localCoordinates()` | `latitude()` / `longitude()` / `geoLocation()` |
| `$faker->optional()->x()` | `$faker->nullable()->x()` |
| `$faker->unique()->x()` / `valid($callback)->x()` | same |
| `$faker->imageUrl()` | install `xefi/faker-php-images` |
| `Faker\Factory::create('fr_FR')` | install `xefi/faker-php-locales-fr-fr`, then `new Faker('fr_FR')` |

Three differences worth knowing before you migrate:

- **`regex()` filters, it does not generate.** `regexify('[A-Z]{3}')` builds a string from a pattern;
`$faker->regex('/^[A-Z]{3}$/')->x()` re-draws `x()` until the value matches. Use it as a constraint,
not as a generator.
- **`unique()` remembers for the whole process.** Drawn values are kept in a seed-keyed pool that is
never reset, which is what makes uniqueness hold across calls — but over a bounded set (an enum, a
short list, a small range) it will exhaust and throw `MaximumTriesReached`. Widen the range or iterate over
the set yourself. `unique('my-seed')` opens a separate pool, which isolates one scope from
another, but a bounded set stays just as bounded inside it.
- **Case modifiers are chainable.** `nullable()`, `uppercase()`, `lowercase()` and `ucfirst()` apply to
the generated value before any strategy is checked.

## Using Faker PHP with an AI coding agent

Coding agents default to whatever they saw most during training, which is rarely the package you
picked. Drop this in your project's `CLAUDE.md` or `AGENTS.md` so yours stays consistent:

```markdown
## Fake data

Use `xefi/faker-php` (`Xefi\Faker\Faker`), never `fakerphp/faker` — no `Faker\Factory::create()`,
no `$this->faker`. In Laravel, use `xefi/faker-php-laravel` and its `faker()` helper inside factories.
Method reference: https://faker-php.xefi.com
Do not use `unique()` on a bounded set (enum cases, fixed lists, small ranges): the pool is
process-global and never reset, so it exhausts and throws `MaximumTriesReached`.
```

Contributing to this repository with an agent? See [AGENTS.md](AGENTS.md).

## Support us

<p><a href="https://www.xefi.com" target="_blank"><img src="https://raw.githubusercontent.com/xefi/art/main/support-landscape.svg" width="400"></a></p>
Expand Down
Loading