Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpunit Matrix Data Provider Laravel Package

apie/phpunit-matrix-data-provider

PHPUnit data provider that generates a matrix of test cases across multiple PHP and Laravel versions in one go. Configure your supported version combinations and let it fan tests out automatically, keeping compatibility suites DRY and every run clearly labeled by environment.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect

composer require --dev apie/phpunit-matrix-data-provider

It's a dev-only dependency with a deliberately tiny surface area — one job: build a PHPUnit data provider that yields the cartesian product of the value sets you give it. Before writing tests, read src/ (it's small) and the README's example; there is essentially one entry point to learn, so you can audit the whole package in five minutes.

First use case: kill a hand-written provider

The canonical refactor is a test where you've been writing rows by hand and keep missing combinations:

// Before: 12 rows you maintain by hand
public function provideUsers(): array
{
    return [
        ['admin',  true,  'active'],
        ['admin',  true,  'blocked'],
        ['admin',  false, 'active'],
        // ... and 9 more
    ];
}

Replace it with the matrix factory. The exact method name is the one thing to copy from the README (the package exposes a static builder/factory) — the shape is:

use Apie\PhpunitMatrixDataProvider\MatrixDataProvider;

public static function provideUsers(): iterable
{
    return MatrixDataProvider::create()
        ->addParameter('role',   ['admin', 'editor', 'viewer'])
        ->addParameter('verified', [true, false])
        ->addParameter('status', ['active', 'blocked'])
        ->getData();
}

#[DataProvider('provideUsers')]
public function testUserCanLogin(string $role, bool $verified, string $status): void { /* ... */ }

First run

vendor/bin/phpunit --filter testUserCanLogin --testdox

--testdox plus --list-tests is how you verify you actually got the 3 × 2 × 2 = 12 cases you expected, and in what order. Do that once before wiring it into a real suite; the ordering assumption (right-most parameter varies fastest in most implementations) is the #1 surprise.


Implementation Patterns

1. Keep the provider dumb, put the matrix in one place per class

Providers run before setUp() / setUpBeforeClass() — no $this, no container, no DB. Treat the matrix as pure data generation and nothing else:

private static function matrix(): MatrixDataProvider
{
    return MatrixDataProvider::create()
        ->addParameter('currency', ['EUR', 'USD'])
        ->addParameter('amount', [0, 1, -1])
        ->addParameter('rounding', [RoundingMode::UP, RoundingMode::DOWN]);
}

2. Merge the matrix with hand-picked edge cases

The matrix gives you coverage of the parameter space, not of weird inputs. Concatenate curated rows after the generated ones, and key them so failures are readable:

public static function provideConversions(): iterable
{
    yield from self::matrix()->getData();
    yield 'overflow'  => [PHP_INT_MAX, 'USD'];
    yield 'nan'       => [NAN, 'EUR'];
}

3. Name your datasets

Numeric keys become #0, #1, #2 in failure output, which is useless when a matrix has 96 rows. Either ensure the package derives names from parameter values (check the README — some implementations stringify the combination), or wrap the output and assign keys yourself. A few lines here pay back on every red build.

4. Use a generator for anything non-trivial

If getData() returns an array, PHPUnit materialises the whole matrix at collection time. For a matrix above ~5k rows, prefer a generator-backed variant (or build the generator yourself around the package's per-parameter sets) so memory stays flat and the test suite reports progress instead of pausing.

5. Share matrices across test classes

PHPUnit 10+ allows #[DataProviderExternal(ProviderClass::class, 'method')]. Put reusable matrices (supported locales, formats, HTTP verbs × status codes) in a tests/Provider/ class and reference them from multiple test cases. This is where the package earns its keep — one definition, several suites.

6. Pair with value-object / serializer tests

The highest-yield target for a matrix provider is a pure function with a small domain: a validator, a normalizer, a DTO → array mapper, a formatter. One matrix of (input, format, locale) replaces dozens of near-duplicate test methods:

#[DataProvider('provideFormats')]
public function testSerializesToAllFormats(Money $money, string $format, string $expectedKey): void
{
    $result = $this->serializer->serialize($money, $format);
    $this->assertArrayHasKey($expectedKey, json_decode($result, true));
}

7. Filter impossibilities in the test, not the provider

Combinations like (guest, isAdmin: true) are logically impossible. Don't contort the provider — assert the invariant and markTestSkipped() (or better, assert the expected exception). Pruning logic inside the provider turns it into a second, untested implementation of your business rules.

8. Cap the matrix in CI

Add a guard test that asserts count(iterator_to_array(provider())) < N. It catches the day someone adds a fourth parameter with 20 values and quietly turns a 30-second suite into a 10-minute one.


Gotchas and Tips

Combinatorial explosion is the whole risk. Costs multiply, they don't add: 4 params × 6 values = 1,296 tests, each with its own setUp, container boot and (if you use it) database transaction. A matrix is only cheap for pure functions. For anything touching the DB, cap it hard or use it once per class, not once per method.

Ordering and indexing. Most cartesian implementations vary the last parameter fastest. If you yield from a matrix and index into it ($data[0]), you are coupled to an implementation detail that isn't a documented contract. Always read values by position in the signature, never by re-indexing the provider output.

PHPUnit version drift. In PHPUnit 10+ data provider methods must be public static and return iterable; @dataProvider annotations are deprecated in favour of #[DataProvider]. If this package targets an older PHPUnit, you'll hit a hard incompatibility. Check its composer.json constraints against your PHPUnit major before adopting — and note that a 0-star, 0-dependent package is unlikely to have been exercised against the newest majors.

Providers run too early. No container, no auth user, no DB connection, no $this. Everything in the provider must be constructible from constants. If you need a factory, put the factory in a static helper, or pass a closure as the data-set value and call it inside the test.

Object state leaks between datasets. PHPUnit does not deep-clone arguments. If you put a mutable object in the matrix (e.g. a DateTimeImmutable is fine, a Request is not), mutations in one test bleed into the next dataset that reuses the same instance. Pass scalars, or pass closures/fn() => new Foo(...).

Unreadable test names. Non-scalar arguments get named as array (...) / Object (...) in the test name. Combined with numeric keys, you get testX with data set #37. Budget an hour to add meaningful keys the first time you use the package in earnest.

Debugging the matrix. Don't debug through PHPUnit — dump it directly:

$data = MyTest::provideUsers();
echo count(iterator_to_array($data, false)); // expected count
print_r(array_slice(iterator_to_array($data, false), 0, 3));

Run it as a one-off script or php -r include. It's faster than squinting at --list-tests output.

Memory on large matrices. iterator_to_array() on a 100k-row generator is itself the problem. Count with a foreach counter, not by materialising.

Extension points are thin. Expect the package to expose a builder plus maybe a filter callback. Don't expect pluggable name-generators or laziness knobs. Wrap it in your own provider class (abstract class MatrixTestCase extends TestCase) that exposes matrix(...) and matrixLazy(...); that wrapper is your real extension point and your escape hatch.

Vendor risk is low but real. Zero stars, zero dependents, single maintainer. The good news: the functionality is a ~10-line function. Keep an inline fallback so a future PHPUnit breakage doesn't block you:

function cartesian(array $sets): iterable
{
    $result = [[]];
    foreach ($sets as $values) {
        $next = [];
        foreach ($result as $row) {
            foreach ($values as $v) { $next[] = [...$row, $v]; }
        }
        $result = $next;
    }
    yield from $result;
}

If you ever need to drop the dependency, swap the call site — the test bodies don't change, which is precisely why adopting this package is a low-risk decision.

Config quirks. No config file, no service provider, no framework coupling — it's a plain library, which is a feature. The only "config" is your composer.json dev constraints and your PHPUnit XML cacheDirectory (data-set naming changed across majors, so a stale cache can make test names look wrong; --do-not-cache-result or clearing .phpunit.cache fixes it).

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings