Upgrading
This page collects breaking-change notes between minor versions. For migrating from vierge-noire/cakephp-fixture-factories, see the migration guide.
v2 migration helpers
Version 2 targets projects already on 1.4.x. If you're still on 1.3.x, upgrade to 1.4.x first, then move to v2.
Behavior changes since 1.4.x
setGenerator()now scopes to the calling factory by default. TheFixtureFactories.instanceLevelGeneratorconfig defaults totruein v2 (wasfalsein 1.x). Calls likeArticleFactory::new()->setGenerator('dummy')no longer mutate the global default — they return a clone with the override scoped to that factory. If you relied on the global mutation, either setConfigure::write('FixtureFactories.instanceLevelGenerator', false)in your test bootstrap to restore the 1.x behavior, or use the explicit staticBaseFactory::setDefaultGenerator($type)instead.setDefaultTemplate()/setDefaultData()are no longer wired up. v2 only consultsdefinition(GeneratorInterface $generator): arrayon the factory class. Run the bundled Rector config (below) to migrate; otherwise factories silently produce empty data.- The standalone
bin/migrate-factory-annotations.phpshim is gone. Annotation upkeep now lives entirely in the IDE-helper integration: installdereuromark/cakephp-ide-helperand runbin/cake annotate classes(orannotate all) —FactoryAnnotatorTaskis auto-registered when the plugin boots. - The bake command's default-data guesser was extracted into a dedicated class. The bundled
BakeFixtureFactoryCommandno longer carries the protected$mapproperty or theguessDefault()method — both moved toCakephpFixtureFactories\Codegen\DefaultDataGuesser. Subclasses that overrode the old hooks should either swap in a custom guesser by overridinggetDefaultDataGuesser()in the command subclass, or useDefaultDataGuesser::setMap()/mergeMap()to inject project-specific column conventions. The Configure keysFixtureFactories.defaultDataMapandFixtureFactories.columnPatternsstill work unchanged for the common case. FactoryTransactionStrategyis eager by default again. v1.3.0 (PR #23) flipped the strategy from eager to lazy: a connection only joined the rollback set after a Factory persisted on it. That silently broke tests that mixed Factory build with direct$table->save($entity)(the testFind / testValidationDefault pattern) — the direct save ran outside any transaction and leaked across test methods. v2 restores the eager default:setupTest()opens a transaction on the primary test connection (testby default; configurable via theprotected string $primaryConnectionproperty) up-front. Additional connections are still tracked lazily viaensureTransaction()fromBaseFactory::save()/saveMany(), so multi-database setups still skip transactions on connections they never write to. Suites that explicitly want the 1.3.0+ lazy behaviour opt in viaLazyFactoryTransactionStrategy::class(whole suite) orLazyTransactionTrait(per test class).- DummyGenerator backend now requires
johnykvsky/dummygenerator:^0.2.1(was^0.1.0 || ^0.2.0). v0.1.x is removed entirely — the deprecation notice the adapter used to emit on every test boot is gone, the legacyDefinitionContainerBuildercode path with it. The v2 adapter only constructsDummyGenerator::create()and callswithDefinition(RandomizerInterface::class, new XoshiroRandomizer($seed))directly. Tied to that: v2 now forwards theoptional()float weight toboolean()directly (added in v0.2.1), so weights below 0.005 — e.g.optional(0.001)for 0.1% chance — fire at the requested rate instead of silently rounding to 0%. Projects on^0.2.0need to bump to^0.2.1(a patch release); projects still on^0.1.xneed to upgrade to^0.2.1. enumElement()alias is removed. The Faker-specific name was a back-compat alias forenumCase(); both adapters now expose onlyenumCase()(returns the case) andenumValue()(returns the backed-enum scalar). Thetrigger_error(E_USER_DEPRECATED)the dummy adapter used to emit on everyenumElement()call underdebug = trueis gone with it. Callers that still useenumElement(EnumClass::class)should rename toenumCase(EnumClass::class)— the bundled rector config does not rewrite this rename, so it's a manual one-line change per call site.
For the v2 step, the package ships a Rector config to help with the mechanical API rename work:
vendor/bin/rector process tests --config vendor/dereuromark/cakephp-fixture-factories/rector.phpThe bundled rules cover the safe, mechanical call-site changes:
Factory::make(...)toFactory::new(...)Factory::make($data, $n)toFactory::new($data)->count($n)- nested helper-body calls such as
EventFactory::make($parameters, $n) setDefaultTemplate()wrappers todefinition(GeneratorInterface $generator)getEntity()tobuild()getEntities()tobuildMany()persistEntity()tosave()persistEntities()tosaveMany()patchData(...)tostate(...)(in factory helper methods such asasAdmin())- static query helpers like
Factory::find()toFactory::query() Factory::get($id, $options)toFactory::table()->get($id, $options)- Faker-style property access on the generator to a method call:
$generator->nameto$generator->name()(also coversoptional()/unique()chains and$this->getGenerator()->name)
It intentionally does not rewrite deprecated persist() calls, because that return type is shape-dependent and needs a human choice between save() and saveMany().
Typical before/after replacements look like this:
- $article = ArticleFactory::make(['title' => 'Foo'])->persistEntity();
+ $article = ArticleFactory::new(['title' => 'Foo'])->save();
- $articles = ArticleFactory::make()->setTimes(3)->persistEntities();
+ $articles = ArticleFactory::new()->count(3)->saveMany();
- $article = ArticleFactory::get($id, ['contain' => ['Authors']]);
+ $article = ArticleFactory::table()->get($id, ['contain' => ['Authors']]);
- $published = ArticleFactory::find('published')->all();
+ $published = ArticleFactory::query()->find('published')->all();If commit-phase tests block the upgrade
The recommended FactoryTransactionStrategy is still the best default for v2, but some older suites rely on real commit behavior during the test itself. If your tests depend on Model.afterSaveCommit, commit-triggered listeners/behaviors, or rows being durably written before assertions run, CakePHP's Eager fixture strategy can be a practical temporary fallback.
That is mainly a migration-pressure valve: it lets you move to the v2 factory API first and defer the heavier refactor of commit-sensitive tests. The tradeoff is that you give up the transaction-based cleanup and automatic unique-state reset provided by FactoryTransactionStrategy.
Custom factory helpers that wrap field overrides also flip from patchData() to state():
public function asAdmin(): static
{
- return $this->patchData(['role_id' => self::ROLE_ADMIN]);
+ return $this->state(['role_id' => self::ROLE_ADMIN]);
}Factory subclass @extends BaseFactory<TEntity> annotations are expected to already be in place from the 1.4.x upgrade step.
1.3 → 1.4
These notes are kept here as historical context for the required pre-v2 upgrade path. Complete the 1.4.x migration before adopting v2.
Symmetric persist/get API
persist() is now deprecated. Its return type is the union TEntity|iterable<TEntity>, which means callers always have to narrow before they can use the result. 1.4 introduces two typed replacements:
| Before (1.3.x) | After (1.4.0) | When to use |
|---|---|---|
persist() (single row) | persistEntity() | Factory configured via make() or make([...singleRow]) |
persist() (multiple) | persistEntities() | setTimes(n), make([[...], [...]]), multi-entity factories |
persistEntity() keeps a throw-if-multiple safety check; if the factory ends up producing more than one entity, you get a clear runtime error instead of a silently-narrowed return value. persistEntities() always returns an array<TEntity>.
The in-memory side is now symmetric: getEntity() and getEntities() are no longer marked deprecated. They form the non-persisted counterpart to the persist pair.
All four methods are typed through the TEntity template on BaseFactory, so an IDE resolves return types to the concrete entity class once your factory declares the template parameter (see below).
persist() was removed in v2. Use save() or saveMany() instead.
persist() call sites
Replace each call site with persistEntity() or persistEntities() based on how the factory was configured:
- $invoice = InvoiceFactory::make()->persist();
+ $invoice = InvoiceFactory::make()->persistEntity();
- $invoices = InvoiceFactory::make()->setTimes(5)->persist();
+ $invoices = InvoiceFactory::make()->setTimes(5)->persistEntities();Factory subclass annotations
To get sharp IDE autocomplete and PHPStan/Psalm type resolution on getEntity(), getEntities(), persistEntity() and persistEntities(), declare the template parameter on each Factory subclass:
/**
* extends \CakephpFixtureFactories\Factory\BaseFactory<\App\Model\Entity\Invoice>
*/
class InvoiceFactory extends BaseFactory(The leading-backslash FQN form is the safest because it does not depend on a use statement being present, and matches what SlevomatCodingStandard's FullyQualifiedClassNameInAnnotation sniff enforces.)
If your project already had hand-rolled method annotations like
method getEntity()
method getEntities()
method persist()on each Factory subclass, replace each block with the single extends line above. The IDE-resolved types are equivalent and the new persistEntity() / persistEntities() methods are picked up automatically.
Bake
The bake fixture_factory template now emits the extends form by default; freshly-baked factories require no manual annotation work.
Migrating an existing project
The bake template only helps with new factories. For existing ones, use FactoryAnnotatorTask (if you also use dereuromark/cakephp-ide-helper 2.17 or newer) — the task is auto-registered during plugin bootstrap, declares its own scan path (tests/Factory/) via PathAwareClassAnnotatorTaskInterface, and runs as part of the standard bin/cake annotate classes (or annotate all) command. It keeps your factory annotations correct over time, not just on a one-time upgrade.
After updating annotations, verify with your usual quality gates (phpunit, phpstan, phpcs).