Skip to content

Let the user build its own randomizer engine to fix the seed and get reproductible results.#83

Open
sukei wants to merge 1 commit into
xefi:mainfrom
sukei:feature/seed
Open

Let the user build its own randomizer engine to fix the seed and get reproductible results.#83
sukei wants to merge 1 commit into
xefi:mainfrom
sukei:feature/seed

Conversation

@sukei

@sukei sukei commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Sometimes, its useful to fix a random seed to get reproductible results. However, its not possible yet. This PR tries to address this issue.

I would also love to discuss the static nature of things because it is harder to have two Faker instances at the same time. Its not that much desirable, but force the consumer to do weird reset things to fix the seed. This can be seen on the extension TestCase I changed for this purpose.

If this PR goes further, I will gladly make a PR to the documentation project too.

Summary by CodeRabbit

  • New Features

    • Added support for providing a custom random engine when creating faker instances and containers.
    • Random-dependent features can now produce repeatable results when initialized with the same engine.
  • Bug Fixes

    • Improved consistency in generated values by using the shared engine for extensions, modifiers, and provider resolution.
    • Updated test setup to use deterministic random behavior for more stable results.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces an optional Random\Engine parameter that propagates from Faker through Container into extension and modifier randomizers, enabling seedable/deterministic random output. Extension::__construct is made final. Tests are updated to use deterministic engines (Mt19937) and verify seedability.

Changes

Engine injection and propagation

Layer / File(s) Summary
Container engine storage and accessor
src/Container/Container.php
Adds Random\Engine import, private ?Engine $engine property, constructor parameter ?Engine $engine = null, and new getEngine(): ?Engine accessor.
Faker constructs Container with stored engine
src/Faker.php
Adds Random\Engine import and $engine property; constructor accepts optional ?Engine; __call instantiates Container with the stored engine.
Extension resolution and modifiers use the engine
src/Container/Traits/HasExtensions.php, src/Container/Traits/HasModifiers.php, src/Extensions/Extension.php, src/Providers/Provider.php, tests/Support/Extensions/NumberTestExtension.php
resolveExtensions/resolve accept and forward an engine into Randomizer construction; nullable() uses new Randomizer($this->engine); Provider::extensions() passes $container->getEngine(); Extension::__construct made final; test extension uses $this->randomizer->getInt() instead of rand().
Tests updated for seeded Container and seedability
tests/Unit/Extensions/TestCase.php, tests/Unit/Extensions/InternetExtensionTest.php, tests/Unit/Extensions/HashExtensionTest.php
TestCase constructs Container with Mt19937(19937); InternetExtensionTest passes null as first Container argument; HashExtensionTest adds seedable-output equality tests for SHA1/SHA256/SHA512/MD5.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Faker
    participant Container
    participant Provider
    participant HasExtensions
    participant Extension

    Faker->>Container: new Container(engine)
    Container->>Container: store engine, getEngine()
    Faker->>Container: __call(method, args)
    Container->>Provider: extensions()
    Provider->>Container: resolveExtensions(extensions, getEngine())
    Container->>HasExtensions: resolve(extension, engine)
    HasExtensions->>Extension: new Extension(new Randomizer(engine))
    Extension-->>Container: extension instance ready
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding a custom randomizer engine for reproducible Faker results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Container/Container.php (1)

61-74: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Reset the static extension cache when the engine changes
Container::__construct() only boots and resolves extensions while static::$extensions is empty, so the first Container created in a process fixes the Randomizer used by every later extension instance. That makes Faker::__call() reuse the first engine’s extensions even when a different $engine is passed, which breaks concurrent seeded Faker instances. Consider invalidating the static cache on engine changes or avoiding the static extension state here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Container/Container.php` around lines 61 - 74, The Container constructor
is reusing a static extension cache across different Engine instances, so later
Faker calls can keep the first engine’s Randomizer and extensions. Update
Container::__construct(), areExtensionsInitialized(), and the
static::$extensions handling so the cache is invalidated or rebuilt whenever a
different $engine is passed, or remove the shared static state from extension
bootstrapping entirely. Make sure Faker::__call() and extension initialization
always reflect the current engine instead of the first one created in the
process.
🧹 Nitpick comments (3)
tests/Unit/Extensions/HashExtensionTest.php (1)

14-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Seedability tests only prove same-seed reproducibility, not seed sensitivity.

Each *IsSeedable test compares two createFresh() calls, which both hardcode Mt19937(19937) (per TestCase::createFresh()), so equality is guaranteed regardless of whether the engine is actually wired through correctly to the hash extension's randomizer. A complementary assertion that two different seeds produce different output would more directly validate the new seeding feature end-to-end.

Also applies to: 29-35, 44-50, 59-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Extensions/HashExtensionTest.php` around lines 14 - 20, The
seedability tests in HashExtensionTest only verify that two identical
createFresh() calls match, so they do not prove the seed is actually affecting
the hash extension. Update the *IsSeedable tests that call
createFresh()->sha1(), ->sha256(), ->sha384(), and ->sha512() to also compare
outputs from two different seeds and assert they differ, using
TestCase::createFresh() and the relevant hash methods to confirm the seed is
wired through end-to-end.
tests/Unit/Extensions/TestCase.php (1)

19-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixed-seed $this->faker weakens fuzz-style assertions across the suite.

$this->faker (the shared instance used across all Extension test subclasses) is now always constructed with the same hardcoded Mt19937(19937) seed, both in setUp() and createFresh(). Any test that still exercises $this->faker in a loop to fuzz-check output formats (e.g. InternetExtensionTest::testUnsecureUrl/testSecureUrl, which weren't migrated to a local unseeded Container) will now generate the exact same sequence of values on every run, forever. This removes the randomized-input coverage those loops were designed to provide — a latent bug only triggered by a value outside seed 19937's sequence would never be caught by CI.

Consider either:

  • Using an unseeded engine (null) for the shared $this->faker by default, and only seeding explicitly in tests that need reproducibility (as done for most InternetExtensionTest methods), or
  • Varying the seed per test run (e.g., from a random value logged on failure) to retain both reproducibility-on-failure and broad fuzz coverage over time.
Illustrative fix: keep shared faker unseeded, opt-in to seeding where needed
     protected function setUp(): void
     {
         parent::setUp();

         (new FakerServiceProvider())->boot();

-        $this->faker = new Container(new Mt19937(19937), false);
+        $this->faker = new Container(null, false);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Extensions/TestCase.php` around lines 19 - 31, The shared faker in
TestCase is being created with a fixed Mt19937(19937) seed in both setUp() and
createFresh(), which removes fuzz coverage for subclasses like
InternetExtensionTest that still rely on $this->faker. Update TestCase so the
default shared Container is unseeded or otherwise varies per run, and keep
explicit seeding only in tests that need deterministic output. Make sure
createFresh() and the setUp() initialization stay aligned so subclasses continue
to get the intended faker behavior.
src/Container/Traits/HasModifiers.php (1)

28-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the public getEngine() accessor over the raw property.

HasModifiers implicitly relies on the host class declaring a $engine property with that exact name/visibility. Since Container::getEngine() was just added for this purpose, use it here instead of reaching directly into $this->engine, decoupling the trait from the host's internal property layout.

♻️ Proposed fix
-        $this->modifiers[] = new NullableModifier(new Randomizer($this->engine), $weight);
+        $this->modifiers[] = new NullableModifier(new Randomizer($this->getEngine()), $weight);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Container/Traits/HasModifiers.php` around lines 28 - 33, The
HasModifiers::nullable method is reaching directly into the host’s $engine
property instead of using the public accessor. Update nullable() to obtain the
engine via getEngine() when creating the Randomizer, so the trait depends on
Container::getEngine() rather than a specific property layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Container/Container.php`:
- Around line 61-64: The Container constructor parameter order was changed in a
way that breaks positional callers, so restore the public API by keeping the
boolean parameter first and the Engine parameter last in Container::__construct.
Update the constructor signature and any internal usage so
$shouldBuildContainerMixin remains the first argument and $engine is appended
afterward, preserving existing calls like new Container(false).

In `@src/Extensions/Extension.php`:
- Around line 12-15: The final constructor on Extension blocks third-party
subclasses that override __construct(), so update Extension::__construct to
allow inheritance while preserving the Randomizer injection contract used by
resolve(). If subclassing is intended as public API, also add the appropriate
semver-major/changelog note and ensure any extension instantiation path remains
compatible with existing subclass constructors.

---

Outside diff comments:
In `@src/Container/Container.php`:
- Around line 61-74: The Container constructor is reusing a static extension
cache across different Engine instances, so later Faker calls can keep the first
engine’s Randomizer and extensions. Update Container::__construct(),
areExtensionsInitialized(), and the static::$extensions handling so the cache is
invalidated or rebuilt whenever a different $engine is passed, or remove the
shared static state from extension bootstrapping entirely. Make sure
Faker::__call() and extension initialization always reflect the current engine
instead of the first one created in the process.

---

Nitpick comments:
In `@src/Container/Traits/HasModifiers.php`:
- Around line 28-33: The HasModifiers::nullable method is reaching directly into
the host’s $engine property instead of using the public accessor. Update
nullable() to obtain the engine via getEngine() when creating the Randomizer, so
the trait depends on Container::getEngine() rather than a specific property
layout.

In `@tests/Unit/Extensions/HashExtensionTest.php`:
- Around line 14-20: The seedability tests in HashExtensionTest only verify that
two identical createFresh() calls match, so they do not prove the seed is
actually affecting the hash extension. Update the *IsSeedable tests that call
createFresh()->sha1(), ->sha256(), ->sha384(), and ->sha512() to also compare
outputs from two different seeds and assert they differ, using
TestCase::createFresh() and the relevant hash methods to confirm the seed is
wired through end-to-end.

In `@tests/Unit/Extensions/TestCase.php`:
- Around line 19-31: The shared faker in TestCase is being created with a fixed
Mt19937(19937) seed in both setUp() and createFresh(), which removes fuzz
coverage for subclasses like InternetExtensionTest that still rely on
$this->faker. Update TestCase so the default shared Container is unseeded or
otherwise varies per run, and keep explicit seeding only in tests that need
deterministic output. Make sure createFresh() and the setUp() initialization
stay aligned so subclasses continue to get the intended faker behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6a98295-6a3f-441c-8065-61cb43f831a1

📥 Commits

Reviewing files that changed from the base of the PR and between 772724b and 5004b6d.

📒 Files selected for processing (10)
  • src/Container/Container.php
  • src/Container/Traits/HasExtensions.php
  • src/Container/Traits/HasModifiers.php
  • src/Extensions/Extension.php
  • src/Faker.php
  • src/Providers/Provider.php
  • tests/Support/Extensions/NumberTestExtension.php
  • tests/Unit/Extensions/HashExtensionTest.php
  • tests/Unit/Extensions/InternetExtensionTest.php
  • tests/Unit/Extensions/TestCase.php

Comment thread src/Container/Container.php
Comment thread src/Extensions/Extension.php

@nikophil nikophil left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi!

I was currently opening an issue in this way! (see zenstruck/foundry#1128)

Don't you think this should also be useful to reset the engine? maybe in order to reset the seed?

maybe instead of giving an instance of Random\Engine to the Container, let's pass a callable(): Random\Engine?

so we can easily reset it?

protected Randomizer $randomizer;

public function __construct(Randomizer $randomizer)
final public function __construct(Randomizer $randomizer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this creates a BC break, given that HasExtenisons trait allows to pass extensions instances, this seems needed, but that would prevent any service injection. What if we want an extension that uses a shared clock for example?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed this point with coderabbitai. The current implementation is doing new $extension(new Randomizer()) calls. So the contract have to be fixed in order to work reliably. That being said, with a fresh redesign of that part we could move toward custom constructors.

@martinsoenen martinsoenen Jul 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you did this, but it can breaks some extensions if someone modified the constructor. I need to talk about this with @GautierDele too

@sukei

sukei commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Don't you think this should also be useful to reset the engine? maybe in order to reset the seed?

I would prefer having isolated faker instances with their own settings instead of resetting existing ones. Thats why I propose to refactor the static nature of the library.

@nikophil

nikophil commented Jul 8, 2026

Copy link
Copy Markdown

I would prefer having isolated faker instances with their own settings instead of resetting existing ones. Thats why I propose to refactor the static nature of the library.

yeah I understand, and totally agree, but it seems odd to re-instantiate all the extensions when we only need to reset the randomizer

@sukei

sukei commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

yeah I understand, and totally agree, but it seems odd to re-instantiate all the extensions when we only need to reset the randomizer

If we are looking for performance optimization, I think we could instantiate them lazily at call time. There is already an algorithm that collect method names to build the mixin. We could generate a map of $methodName => $extensionClass to help doing this.

@nikophil

nikophil commented Jul 8, 2026

Copy link
Copy Markdown

Actually, we're trying to find a way to allow to use this package in Foundry (see zenstruck/foundry#1128) and being able to call $faker->resetSeed() or something like this would be super-helpful

@sukei

sukei commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

we're trying to find a way to allow to use this package in Foundry

I understand your use-case, you try to align with FakerPHP. Using a proper DI, we could have a randomizer service (which wrap a PHP Radomizer instance) that can expose convenient methods to randomize things and be reseedable. But honestly, I think it won't land without a proper refactor of the core architecture.

Note that I'm nobody here, I just want to replace my own usage of FakerPHP for something more actual. I'm just trying to improve the current implementation to fit my needs, but it seems to be a lot of work.

@martinsoenen

Copy link
Copy Markdown
Contributor

Hi, I will take some time during this week for this subject :)

@martinsoenen

Copy link
Copy Markdown
Contributor

Well, I also would prefer the ->resetSeed() function because it seems easier to use with frameworks. But this can also return a new Faker instance. We need to do more tests with the Laravel and Symfony integrations, and keep in memory the changes it can cause to Foundry

@nikophil

Copy link
Copy Markdown

I frankly think that a shared Randomizer instance would greatly simplify this

@sukei

sukei commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

I think that we should create our own Randomizer class (that may wrap the native one) but that can expose more convenient methods such as the setSeed()|resetSeed()|seed() or whatever name we choose. This instance would be mutable and shared.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants