Skip to content

Setup

Using a different PHP framework?

This plugin is built on top of CakePHP's ORM (cakephp/orm) — the produced objects are CakePHP entities, marshalled by Table::patchEntity(), with associations expressed in Cake's shape. If your project is built on a different framework, prefer the factory library that is canonical for that ecosystem instead of bringing in a second ORM:

  • Laravel → built-in Eloquent factories (Illuminate\Database\Eloquent\Factories\Factory) paired with the RefreshDatabase trait.
  • Symfony / Doctrine (ORM or ODM)zenstruck/foundry.

The section below covers the rarer case of using this plugin with a standalone cakephp/orm install — i.e. you're using Cake's ORM as a library outside the full Cake application stack, not Laravel/Symfony interop.

Standalone cakephp/orm (no full Cake app)

For applications that use cakephp/orm directly (without the rest of the Cake framework), use whatever method your test stack provides for managing the database, or opt for the universal test-database-cleaner.

Define your DB connections in your test bootstrap.php as described in the CakePHP cookbook.

See also: Associations for standalone cakephp/orm.

CakePHP apps

To bake and use factories, load the CakephpFixtureFactories plugin. Factories live under tests/Factory/ and are never called from production code paths, so you want the plugin loaded only when debug is true:

bash
bin/cake plugin load --only-debug CakephpFixtureFactories

This writes config/plugins.php with onlyDebug => true. CakePHP then loads the plugin only when Configure::read('debug') is true — the plugin stays out of production HTTP requests even if dev dependencies are accidentally shipped.

The resulting entry, if you prefer hand-edited config:

php
// config/plugins.php
return [
    'CakephpFixtureFactories' => [
        'onlyDebug' => true,
    ],
];

Composer-keyword recommendations

This plugin declares dev and cli in its composer keywords, so running bin/cake plugin load CakephpFixtureFactories without flags will interactively suggest onlyDebug: true, onlyCli: true, and optional: true. Accepting all three additionally skips loading on web requests (onlyCli) and silences the missing-plugin error in production builds that strip dev deps (optional).

We recommend using migrations for managing the schema of your test DB with the CakePHP Migrations plugin.

Cleaning data between tests

Test data must be cleaned between tests to avoid entity collisions and unexpected results. CakePHP ships with Fixture State Managers and provides the TruncateStrategy (truncate all tables after each test run) as well as the TransactionStrategy (create a transaction and roll it back after each test run).

For fixture-factory–driven test suites, this plugin provides a dedicated strategy that pairs cleanly with factories — see below.

This plugin provides the FactoryTransactionStrategy which automatically:

  • Wraps all database operations in transactions
  • Rolls back after each test (both factory and application data)
  • Resets unique generator state (fixes OverflowException issues)
  • Tracks which tables are written to by fixture factories

Unlike the standard TransactionStrategy, this doesn't require manually listing fixtures — in fact, the strategy works best if you don't use classic $fixtures arrays at all.

CakePHP 5.2+ (global configuration)

In CakePHP 5.2+, configure the fixture strategy globally in config/app.php:

php
'TestSuite' => [
    'fixtureStrategy' => \CakephpFixtureFactories\TestSuite\FactoryTransactionStrategy::class,
],

This applies the strategy to all test cases automatically. No traits needed.

Tip: See config/app.example.php in this plugin for a full list of available configuration options, including generator type, seed, and instance-level generator management.

CakePHP 5.0–5.1 (trait-based)

For older CakePHP versions, use FactoryTransactionTrait. Two patterns:

php
namespace App\Test;

use Cake\TestSuite\TestCase;
use CakephpFixtureFactories\TestSuite\FactoryTransactionTrait;

abstract class AppTestCase extends TestCase
{
    use FactoryTransactionTrait;
}

// Then extend AppTestCase in every test:
class MyTest extends AppTestCase
{
    // Strategy is automatically applied.
}
php
use CakephpFixtureFactories\TestSuite\FactoryTransactionTrait;

class MyTest extends TestCase
{
    use FactoryTransactionTrait;

    public function testSomething(): void
    {
        $article = ArticleFactory::new()->save();
        $this->Articles->save($article);
        // All data rolled back; generator state reset.
    }
}

Prefer the shared AppTestCase route — the per-test trait pattern works too but means repeating yourself.

Benefits

  • No need to manually list $fixtures
  • All data is rolled back - factory data AND application code modifications
  • Faster than truncation strategies
  • Solves unique generator state accumulation - the strategy resets generator state after each test, preventing OverflowException when using unique() modifiers
  • Works seamlessly with nested associations

Note: Table tracking only captures tables written via the factory save methods. The transaction rollback still handles all data modifications regardless of source (factories, controllers, models, etc.) on the primary test connection (eager-wrapped at setup) plus any additional connections a Factory has persisted on during the test (tracked lazily). See "Eager by default" below for the full contract.

Eager by default

FactoryTransactionStrategy is eager on the primary test connection: setupTest() opens a transaction on test (configurable via the $primaryConnection property) up-front, so direct table operations during the test ($table->save($entity), $table->delete($entity), raw inserts via $connection->execute(...)) are also rolled back at teardown — not just operations that go through a Factory's save() / saveMany().

Beyond the primary connection, additional connections are still tracked lazily: ensureTransaction() is called from inside BaseFactory::save() / saveMany() the first time a Factory persists on a given connection. Multi-database setups therefore only pay the transaction cost on connections they actually write to.

This covers the standard CakePHP testing patterns out of the box:

php
// Eager-default makes both of these get correctly rolled back:

public function testValidationDefault(): void
{
    $data = ArticleFactory::new()->build()->toArray();
    $article = $this->Articles->newEntity($data);
    $this->assertTrue((bool)$this->Articles->save($article));   // direct table save — covered
}

public function testFind(): void
{
    ArticleFactory::new()->save();                              // Factory persist — covered
    $this->assertNotNull($this->Articles->find()->first());
}

Opting out

If your suite persists exclusively through Factories and you want to skip the eager begin (multi-connection optimization, performance under heavy parallel test runs):

Whole suite

Point 'fixtureStrategy' at the lazy variant:

php
'TestSuite' => [
    'fixtureStrategy' => \CakephpFixtureFactories\TestSuite\LazyFactoryTransactionStrategy::class,
],

A connection only joins the rollback set when a Factory persists on it.

Single test class

Use LazyTransactionTrait on just the affected class. The rest of the suite stays eager:

php
use CakephpFixtureFactories\TestSuite\LazyTransactionTrait;

class HeavyConnectionTest extends \Cake\TestSuite\TestCase
{
    use LazyTransactionTrait;

    // Eager begin from setupTest() is rolled back via #[Before].
    // Tests in this class run with the lazy contract.
}

If your project's primary test connection is named something other than test, override the $primaryConnection property in a FactoryTransactionStrategy subclass:

php
final class MyEagerStrategy extends \CakephpFixtureFactories\TestSuite\FactoryTransactionStrategy
{
    protected string $primaryConnection = 'test_main';
}

Released under the MIT License.