Skip to content

addition: ContainerFactory & ImmutableFactory cycles - #41

Open
remipelhate wants to merge 19 commits into
mainfrom
addition/immutable-factory-cycles
Open

remipelhate wants to merge 19 commits into
mainfrom
addition/immutable-factory-cycles

Conversation

@remipelhate

@remipelhate remipelhate commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

This PR introduces support for cycles in ImmutableFactory instances, similar to Laravel's
Sequence implementation in Model Factories.

Prior to this PR, creating instances for (e.g.) all cases of an enum required initialising
multiple factory instances:

[
    new SomeFactory()
        ->state(['name' => 'First'])
        ->makeOne(),
    new SomeFactory()
        ->state(['name' => 'Middle'])
        ->makeOne(),
    new SomeFactory()
        ->state(['name' => 'Last'])
        ->makeOne(),
];

While not inherently bad, this requires initialising multiple instances. More importantly, it wasn't possible to define cycling of values within an ImmutableFactory.

After this PR, the above can be rewritten as:

$names = new Cycle('First', 'Middle', 'Last');

new SomeFactory()
    ->state(['name' => $names])
    ->times($names->count())
    ->makeMany();

Changes

Faker Cycle provider

A new Cycle Faker provider is automatically registered (once) on both default and injected Faker instances of ImmutableFactory. It returns values from a list in order, wrapping back around to the first value once the end is reached:

$faker->cycle(['a', 'b', 'c']); // 'a'
$faker->cycle(['a', 'b', 'c']); // 'b'
$faker->cycle(['a', 'b', 'c']); // 'c'
$faker->cycle(['a', 'b', 'c']); // 'a' again

The queue is keyed by the serialized option list, so each distinct list cycles independently.

Cycle and Defer deferred attributes

  • src/Factories/Cycle.php: a DeferredAttribute implementation that delegates to the Faker cycle() provider when the factory resolves it, with a count() helper so the number of options can drive times().
  • src/Factories/Defer.php: wraps any callable as a DeferredAttribute, so custom closures work in factory definitions and states without the factory resolving arbitrary Closure values.
  • src/Factories/DeferredAttribute.php: the interface both implement an invokable that receives the ImmutableFactory instance and returns the attribute value.

ImmutableFactory rework

resolveValue() now only resolves DeferredAttribute instances. It also handles nested ContainerFactory and ImmutableFactory values.
A new ContainerFactory has been extracted from ImmutableFactory with the container methods (rawMany(), rawCollection(), makeMany(), makeCollection()). This allows ImmutableFactory to be focussed on definitions and states, while ContainerFactory focusses on the amount of instances to create.

ImmutableFactory::times() returns an instance of ContainerFactory, which on its turn uses the original ImmutableFactory instance as mixin. This means that any method defined on ImmutableFactory can be used directly on ContainerFactory as well:

new SomeFactory() // extends ImmutableFactory
    ->times(10) // returns ContainerFactory
    ->state(['some' => 'State']) // proxies to ImmutableFactory
    ->raw() // proxies to ImmutableFactory

Warning

Breaking changes

While these changes are technically breaking, the likelihood of impact is rather low. Container methods only had effect when times() was called and ContainerFactory proxies any existing methods to ImmutableFactory, keeping method call chains unaffected. Impact is mostly expected on:

  • ImmutableFactory (or implementations of it) type hints where the provided instances call times(). These will have to be changed to ContainerFactory.
  • ImmutableFactory (or implementations of it) return types where the returned instances call times(). These will have to be changed to ContainerFactory.
  • Access of the removed ImmutableFactory::$count property.

@remipelhate remipelhate self-assigned this Nov 13, 2026
@remipelhate
remipelhate marked this pull request as draft November 13, 2026 11:38
@remipelhate remipelhate added type: addition concern: breaking Deprecated functionality has been removed or other breaking changes were introduced. labels Nov 13, 2026
@remipelhate
remipelhate requested a review from fowbi November 13, 2026 15:48
@remipelhate
remipelhate marked this pull request as ready for review November 13, 2026 15:48
@remipelhate
remipelhate force-pushed the addition/immutable-factory-cycles branch from 879eb0d to dfb62cf Compare November 13, 2026 16:06
This method does not exist on lower PhpUnit versions
times() previously accepted 0 as a valid argument and it makes sense to still support it whenever we’re dealing with dynamic input (e.g. through data providers).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concern: breaking Deprecated functionality has been removed or other breaking changes were introduced. type: addition

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant