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

Peridot Laravel Package

peridot-php/peridot

Peridot is a lightweight, extensible PHP testing framework with a descriptive BDD-style syntax. Write expressive specs using describe/it blocks, custom DSLs, and plugins, with clear reporting for readable, maintainable test suites.

Deep Wiki
Context7

Getting Started

Peridot is an event-driven BDD framework. You write describe/it specs, and the runner emits lifecycle events that reporters and plugins subscribe to. The core is small; most day-to-day leverage comes from the runner, the DSL, and custom reporters/plugins.

Install and first spec

composer require --dev peridot-php/peridot peridot-php/expect
mkdir -p specs

specs/Calculator.spec.php:

<?php
use Calculator;

describe('Calculator', function () {
    beforeEach(function () {
        $this->calc = new Calculator();
    });

    it('adds two numbers', function () {
        expect($this->calc->add(2, 3))->to->equal(5);
    });

    it('rejects non-numeric input', function () {
        expect(function () {
            $this->calc->add('a', 1);
        })->to->throw('InvalidArgumentException');
    });
});

Run:

vendor/bin/peridot
vendor/bin/peridot specs/Calculator.spec.php
vendor/bin/peridot --reporter=dot

Where to look first

  1. vendor/bin/peridot --help — CLI flags (--reporter, --grep, focus/skip behavior, config path).
  2. peridot.php at project root — the config file. It must return a closure that receives the event emitter.
  3. specs/ — default spec discovery directory; files matching *.spec.php are loaded recursively.
  4. Peridot\Reporter\ReporterInterface and Peridot\Reporter\AbstractReporter in vendor/peridot-php/peridot/src — the extension surface most teams end up using.
  5. peridot-php/expect — the assertion library; core Peridot intentionally ships little in the way of assertions.

First real use case

Port one small service or value object into a single spec file. This validates three things at once: Composer autoloading for your app classes, the assertion style your team will use, and CI exit-code behavior. Do this before committing to migrating a larger suite.

Minimal peridot.php:

<?php
return function ($emitter) {
    // Register listeners, reporters, plugins here.
    $emitter->on('runner.start', function () {
        // e.g. bootstrap app environment, set timezone, etc.
    });
};

Implementation Patterns

Spec organization

  • One spec file per subject, named Subject.spec.php.
  • Use nested describe()/context() blocks for state variations, not for grouping unrelated assertions.
  • before() runs once per suite; beforeEach() runs per test. Put expensive bootstrap (container, DB connection, HTTP client) in before, and isolation/rollback in beforeEach.
  • Closures are bound to a per-spec scope object, so $this->foo is the idiomatic way to share fixture state between hooks and tests. Do not expect constructor injection inside specs.
describe('UserRepository', function () {
    before(function () {
        $this->pdo = new PDO('sqlite::memory:');
        $this->repo = new UserRepository($this->pdo);
    });

    beforeEach(function () {
        $this->pdo->exec('BEGIN');
        $this->repo->create(['email' => '[email protected]']);
    });

    afterEach(function () {
        $this->pdo->exec('ROLLBACK');
    });

    it('finds a user by email', function () {
        expect($this->repo->findByEmail('[email protected]'))->to->not->be->null;
    });
});

Red/green iteration

  1. Write a focused spec.
  2. Run only that file: vendor/bin/peridot specs/UserRepository.spec.php.
  3. Run a subset by description: vendor/bin/peridot --grep='finds a user'.
  4. Run the full suite and a CI reporter last.

This is the highest-value daily loop. Keep the file argument in muscle memory; Peridot’s startup is fast enough that per-file runs are cheap.

CI integration

  • Use a deterministic reporter (e.g. TAP or dot) and disable colors: vendor/bin/peridot --reporter=tap --no-colors.
  • Wire it into composer.json so local and CI commands match:
{
    "scripts": {
        "test": "peridot --reporter=dot",
        "test:ci": "peridot --reporter=tap --no-colors"
    }
}
  • Ensure the process returns non-zero on failure; verify once by deliberately breaking a spec.
  • Run Peridot in addition to, not instead of, PHPUnit if you need coverage tooling. Coverage integration with Peridot is dated and best treated as a separate concern.

Data-driven specs

There is no @dataProvider equivalent in core. Use PHP loops to generate specs, or loop inside a single it() and assert per case. The loop-generating approach gives better failure granularity:

$cases = [
    [2, 3, 5],
    [-1, 1, 0],
    [0, 0, 0],
];

foreach ($cases as [$a, $b, $expected]) {
    it("adds {$a} + {$b}", function () use ($a, $b, $expected) {
        expect((new Calculator())->add($a, $b))->to->equal($expected);
    });
}

Note the use clause: outer variables are not automatically visible inside spec closures.

Custom reporters

Reporters are the cleanest integration point for dashboards, JUnit-style output, or team-specific summary formats.

use Peridot\Reporter\ReporterInterface;

class SlackSummaryReporter implements ReporterInterface
{
    protected $emitter;
    protected $output;
    protected $failures = 0;

    public function __construct($emitter, $output)
    {
        $this->emitter = $emitter;
        $this->output = $output;
    }

    public function init()
    {
        $this->emitter->on('test.failed', function ($test, $exception) {
            $this->failures++;
            $this->output->writeln("FAIL: " . $test->getDescription());
        });

        $this->emitter->on('runner.end', function () {
            $this->output->writeln("Failures: {$this->failures}");
        });
    }
}

Register it from peridot.php against the emitter, and keep the reporter’s output going through the provided output object so --no-colors and redirection behave correctly.

Event-driven extensions

The emitter is the public extension seam. Typical lifecycle events include:

  • runner.start / runner.end
  • suite.start / suite.end
  • test.start / test.end
  • test.passed / test.failed / test.skipped (and pending/skipped variants depending on version)

Use runner.start for one-time setup and test.failed/test.passed for reporting. For cross-cutting concerns — environment setup, timing, retries, notifications — prefer listener functions in peridot.php over forking the framework.

Plugins

Peridot has a plugin model for reusable extensions (concurrency, watching, mocking, coverage historically existed in the ecosystem). Treat plugins as version-sensitive: check that the plugin’s composer.json supports your Peridot and PHP versions before adopting it. For most teams, a small peridot.php listener plus a custom reporter covers the same ground with less dependency risk.

Mocking and test doubles

Peridot is assertion-agnostic about doubles. Options:

  • peridot-php/expect for assertions.
  • A standalone mocking library (Mockery, Prophecy) instantiated inside beforeEach() and torn down in afterEach().
  • Hand-rolled test doubles for small interfaces; often simpler than configuring a mocking framework inside the Peridot scope.

Keep doubles in $this-> properties so all tests in the suite share consistent setup.

Gotchas and Tips

Project maturity is the biggest risk

Last release was January 2017, and the opportunity score is low. There is effectively no active maintenance. Consequences:

  • PHP 8 compatibility is not guaranteed; deprecated constructs and old dependency constraints can surface as warnings or fatal errors. Validate on your target PHP version before committing.
  • Security and dependency updates will not arrive upstream.
  • If this is a new project, strongly consider PHPUnit or Pest. If Peridot is already in place, pin the exact version in composer.lock, document it, and budget a migration path.

Config file silently does nothing

peridot.php must return a callable receiving the emitter. Returning null, returning an array, or side-effecting without returning a closure means your listeners/reporters never attach. There is usually no loud error for a malformed config — you just get default behavior. If custom output is missing, check the return statement first.

Scope confusion

$this inside specs is a Peridot scope object, not your test class. Properties set in before/beforeEach persist for the current spec’s tests but are not shared across unrelated specs. Closure variables require explicit use. This trips up developers coming from PHPUnit test classes.

Hook ordering and nesting

before hooks run from outer describe to inner describe; after hooks unwind in reverse. Put cleanup in the matching level. Mixing global bootstrap and per-test database state is the most common source of order-dependent failures.

Focus and skip state leaks into CI

Spec-level focus/skip markers are convenient locally but dangerous if committed. A focused suite can silently reduce CI to one test. Add a guard: grep for focus markers in CI, or run the full suite without focus flags and review reporter counts. Also confirm skipped tests are visible in your chosen reporter.

--grep matches the full description path

The regex applies to the composed description (including parent describe names), not just the leaf text. Anchor patterns (^...) and run a focused file first when debugging unexpected matches.

Reporter output must use the injected output object

Writing directly to echo or STDOUT bypasses formatting and breaks when output is redirected or --no-colors is used. Always use the output abstraction passed to the reporter.

CI exit codes

Verify that a failing spec produces a non-zero exit code with your reporter. Some custom reporter implementations accidentally swallow failures or exit zero from runner.end. A one-time deliberate failure test catches this before it hides real regressions.

File discovery

Files must match the *.spec.php pattern inside the configured spec path. A misplaced file yields “no specs found” rather than an error about that file. If a new spec seems ignored, check the extension and directory before debugging the framework.

Autoloading application code

Spec files are not autoloaded by Magento-style conventions; they are included by the runner. Application classes must be in autoload or autoload-dev in composer.json. If you need a bootstrap (constants, env vars, factories), do it in a listener on runner.start or a dedicated bootstrap file required from peridot.php.

Debugging failing specs

  • Run the single file with --reporter=spec for detailed output.
  • Add var_dump/fwrite(STDERR, ...) inside hooks rather than inside assertions.
  • Remember that PHP warnings/notices may be converted into test failures depending on your error handler. If a spec fails without a clear assertion message, check the emitted exception and error_reporting settings.
  • If an exception is thrown in beforeEach, subsequent tests may report cascading failures; read the first failure, not the last.

Performance

There is no built-in parallelism in core. The historical concurrency plugin adds process isolation complexity and shared-resource hazards (database, ports, temp files). Before reaching for it, move slow fixture creation from beforeEach to before, and isolate integration specs into a separate directory/suite run.

Don’t over-invest in undocumented internals

The event names and reporter interfaces are the stable-ish seams. Classes under Peridot\Core and Peridot\Runner are internal and may change across minor versions. If you extend them directly, pin the version and cover the extension with a smoke test that runs in CI.

Assertion ergonomics

Core Peridot does not give you much beyond the runner and DSL. Standardize early on peridot-php/expect or a native assert()/exception-throwing style, and write it down. Mixed assertion styles make failure output inconsistent and slow down onboarding.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle