PHPackages                             hotmeteor/spectator - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. hotmeteor/spectator

ActiveLibrary[Testing &amp; Quality](/categories/testing)

hotmeteor/spectator
===================

Testing helpers for your OpenAPI spec

v3.0.2(2mo ago)3051.6M↓29.2%57[2 PRs](https://github.com/hotmeteor/spectator/pulls)1MITPHPPHP ^8.3CI passing

Since May 27Pushed 2mo ago6 watchersCompare

[ Source](https://github.com/hotmeteor/spectator)[ Packagist](https://packagist.org/packages/hotmeteor/spectator)[ GitHub Sponsors](https://github.com/bastien-phi)[ GitHub Sponsors](https://github.com/hotmeteor)[ RSS](/packages/hotmeteor-spectator/feed)WikiDiscussions master Synced 2d ago

READMEChangelog (10)Dependencies (24)Versions (70)Used By (1)

[![](./art/spectator-logo.png)](./art/spectator-logo.png)

Spectator
=========

[](#spectator)

Spectator provides light-weight OpenAPI contract testing tools that work within your existing Laravel test suite.

Write tests that guarantee your API spec never drifts from your implementation.

[![Tests](https://github.com/hotmeteor/spectator/workflows/Tests/badge.svg)](https://github.com/hotmeteor/spectator/workflows/Tests/badge.svg)[![Latest Version on Packagist](https://camo.githubusercontent.com/686d410507aef06c56d0c38b0d55a7de8f7cd904e3236dc14efa2bd426d0fa9e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f767072652f686f746d6574656f722f737065637461746f722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/hotmeteor/spectator)[![PHP from Packagist](https://camo.githubusercontent.com/441e40a2925978fb43e51fc4732bc1d235bb88152eec5dec635e7008a49602c7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f686f746d6574656f722f737065637461746f72)](https://camo.githubusercontent.com/441e40a2925978fb43e51fc4732bc1d235bb88152eec5dec635e7008a49602c7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f686f746d6574656f722f737065637461746f72)

---

What's New in v3
----------------

[](#whats-new-in-v3)

- **PHP 8.3+ and Laravel 12+** — minimum requirements raised to track the modern PHP ecosystem.
- **New artisan commands** — `spectator:validate` lints your spec file; `spectator:coverage` lists every operation defined in the spec; `spectator:routes` cross-references spec operations against Laravel routes; `spectator:stubs` generates skeleton test classes from a spec. All commands support `--format=json` for machine-readable output.
- **PHPUnit coverage extension** — `SpectatorExtension` tracks which spec operations are exercised during a test run and can enforce a minimum coverage threshold in CI.
- **Machine-readable JSON errors** — set `SPECTATOR_ERROR_FORMAT=json` (or call `Spectator::useJsonErrors()`) to get structured `{"errors": [...]}` output from failed assertions instead of ANSI-coloured text.
- **Modern PHP internals** — enums replace string/class constants; first-class callables, `readonly` properties, and `match` expressions throughout.
- **Remote &amp; GitHub spec sources verified** — remote HTTP and private GitHub spec fetching work reliably out of the box.
- **Fluent path-prefix API** — `Spectator::withPathPrefix('v1')` as an alternative to the config key.

---

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12+

---

Installation
------------

[](#installation)

```
composer require hotmeteor/spectator --dev
```

Publish the config file:

```
php artisan vendor:publish --provider="Spectator\SpectatorServiceProvider"
```

---

Configuration
-------------

[](#configuration)

The published config lives at `config/spectator.php`. The most important setting is the spec **source**, which tells Spectator where to find your OpenAPI spec files.

### Local

[](#local)

Specs are read from the local filesystem.

```
SPEC_SOURCE=local
SPEC_PATH=/path/to/specs
```

### Remote

[](#remote)

Specs are fetched over HTTP. Useful for remote-hosted specs or raw GitHub file URLs.

```
SPEC_SOURCE=remote
SPEC_PATH=https://raw.githubusercontent.com/org/repo/main/specs
SPEC_URL_PARAMS="?token=abc123"   # optional query params appended to the URL
```

### GitHub

[](#github)

Specs are fetched from a private GitHub repository using a Personal Access Token.

```
SPEC_SOURCE=github
SPEC_GITHUB_REPO=org/repo
SPEC_GITHUB_PATH=main/specs       # branch + path to the directory
SPEC_GITHUB_TOKEN=ghp_yourtoken
```

### Path Prefix

[](#path-prefix)

If your API is mounted under a prefix (e.g. `/v1`), configure it here so Spectator strips it before matching spec paths.

```
SPECTATOR_PATH_PREFIX=v1
```

Or set it at runtime:

```
Spectator::withPathPrefix('v1');
```

### Error Format

[](#error-format)

By default, validation errors are rendered as human-readable, coloured terminal output. For CI pipelines and LLM toolchains that parse test output programmatically, switch to JSON:

```
SPECTATOR_ERROR_FORMAT=json
```

Or toggle it per test:

```
Spectator::useJsonErrors();   // emit {"errors": [...]}
Spectator::useTextErrors();   // revert to coloured text
```

---

Writing Contract Tests
----------------------

[](#writing-contract-tests)

### What contract testing is

[](#what-contract-testing-is)

**Functional tests** verify that your application behaves correctly — validation passes, controllers respond, events fire.

**Contract tests** verify that your requests and responses conform to your OpenAPI spec. The data doesn't have to be real; the shape does.

The two test types complement each other. Keep them in separate test classes.

### Pointing to a spec

[](#pointing-to-a-spec)

Call `Spectator::using()` with the spec filename before making any requests. You can call it once in `setUp()` or per test.

```
use Spectator\Spectator;

class UserApiTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        Spectator::using('Api.v1.yml');
    }

    #[Test]
    public function test_using_different_spec(): void
    {
        Spectator::using('OtherApi.v1.yml');
        // ...
    }
}
```

### Making assertions

[](#making-assertions)

Spectator adds these methods to Laravel's `TestResponse`:

MethodDescription`assertValidRequest()`Assert the request matches the spec.`assertInvalidRequest()`Assert the request does **not** match the spec.`assertValidResponse(?int $status)`Assert the response matches the spec (optionally at a specific status code).`assertInvalidResponse(?int $status)`Assert the response does **not** match the spec.`assertValidationMessage(string $message)`Assert the validation error message contains the given string.`assertErrorsContain(string|array $errors)`Assert one or more strings appear in the validation errors.`assertPathExists()`Assert the requested path exists in the spec.`dumpSpecErrors()`Dump current spec errors without failing (useful for debugging).### A typical contract test

[](#a-typical-contract-test)

```
use Spectator\Spectator;

class UserApiTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        Spectator::using('Api.v1.yml');
    }

    #[Test]
    public function test_create_user(): void
    {
        $this->postJson('/users', ['name' => 'Alice', 'email' => 'alice@example.com'])
            ->assertValidRequest()
            ->assertValidResponse(201);
    }

    #[Test]
    public function test_missing_required_field_is_invalid(): void
    {
        $this->postJson('/users', ['name' => 'Alice'])   // missing email
            ->assertInvalidRequest()
            ->assertValidationMessage('required');
    }
}
```

### Mixing with functional tests

[](#mixing-with-functional-tests)

You can chain Spectator assertions with Laravel's built-in assertions, but keeping concerns separate is cleaner:

```
// Works, but mixes concerns
$this->actingAs($user)
    ->postJson('/posts', ['title' => 'Hello'])
    ->assertCreated()
    ->assertValidRequest()
    ->assertValidResponse(201);
```

### Deactivating Spectator for a test

[](#deactivating-spectator-for-a-test)

```
Spectator::reset();
```

### Debugging errors

[](#debugging-errors)

When a validation fails, Spectator renders the schema with errors annotated inline:

```
---

The properties must match schema: data

object++ dumpSpecErrors()
    ->assertValidRequest();
```

---

Artisan Commands
----------------

[](#artisan-commands)

### `spectator:validate`

[](#spectatorvalidate)

Validate that a spec file parses without errors. Useful as a pre-test lint gate in CI.

```
php artisan spectator:validate --spec=Api.v1.yml
php artisan spectator:validate --spec=Api.v1.yml --format=json
```

Text output:

```
✔ Api.v1.yml is valid.

```

JSON output (`--format=json`):

```
{
    "valid": true,
    "spec": "Api.v1.yml",
    "errors": []
}
```

Returns exit code `0` on success, `1` on failure.

### `spectator:coverage`

[](#spectatorcoverage)

List every operation defined in the spec. Useful for auditing coverage gaps.

```
php artisan spectator:coverage --spec=Api.v1.yml
php artisan spectator:coverage --spec=Api.v1.yml --format=json
```

Text output:

```
Operations in Api.v1.yml:

 ────── ───────────────
  GET    /users
  POST   /users
  GET    /users/{id}
 ────── ───────────────

3 operations

```

JSON output (`--format=json`):

```
{
    "spec": "Api.v1.yml",
    "operations": [
        { "method": "GET", "path": "/users" },
        { "method": "POST", "path": "/users" },
        { "method": "GET", "path": "/users/{id}" }
    ]
}
```

### `spectator:routes`

[](#spectatorroutes)

Cross-references spec operations against registered Laravel routes. Surfaces which operations are matched, which are missing from the app, and which routes have no spec entry.

```
php artisan spectator:routes --spec=Api.v1.yml
php artisan spectator:routes --spec=Api.v1.yml --format=json
```

Text output:

```
Routes in Api.v1.yml:

 ──────── ──────── ───────────────────
  Status   Method   Path
 ──────── ──────── ───────────────────
  ✔        GET      /users
  ✔        POST     /users
  ✗        DELETE   /users/{id}
  ⚠        GET      /internal
 ──────── ──────── ───────────────────

Matched: 2  |  Unimplemented: 1  |  Undocumented: 1

```

- `✔ matched` — in spec and a Laravel route exists
- `✗ unimplemented` — in spec, no matching Laravel route
- `⚠ undocumented` — Laravel route exists, not in spec

#### Scoping the comparison

[](#scoping-the-comparison)

If your spec only documents a subset of the app's routes (e.g. the public `/api/v2/*` surface), every internal admin/web/webhook route otherwise shows up as `undocumented` and drowns the signal. Two flags narrow the Laravel side of the comparison:

- `--prefix=api/v2` — only consider routes whose URI starts with the given prefix. Leading/trailing slashes are normalised.
- `--middleware=api` — only consider routes that have the given middleware. Both group aliases (`api`, `web`) and fully-qualified class names work.

Both can be combined (AND) and only affect the Laravel-routes side — spec operations are still listed as you wrote them. If neither is set, behavior is unchanged.

```
php artisan spectator:routes --spec=Api.v1.yml --prefix=api/v2
php artisan spectator:routes --spec=Api.v1.yml --middleware=api
php artisan spectator:routes --spec=Api.v1.yml --prefix=api/v2 --middleware=api
```

### `spectator:stubs`

[](#spectatorstubs)

Generates skeleton test classes from a spec. Groups operations by tag (fallback: first path segment) and creates one class per group with one `test_` method per operation. Each method body calls `$this->markTestIncomplete(...)` so the generated file is immediately runnable.

```
php artisan spectator:stubs --spec=Api.v1.yml
php artisan spectator:stubs --spec=Api.v1.yml --output=tests/Contract --namespace="Tests\\Contract"
php artisan spectator:stubs --spec=Api.v1.yml --force
```

OptionDefaultDescription`--spec`—Spec filename (required).`--output``tests/Contract`Directory to write generated classes to.`--namespace``Tests\Contract`PHP namespace for generated classes.`--base-class``Tests\TestCase`Parent class for generated test classes.`--force``false`Overwrite existing files.Example generated class:

```
namespace Tests\Contract;

use Spectator\Spectator;
use Tests\TestCase;

class UsersContractTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        Spectator::using('Api.v1.yml');
    }

    public function test_get_users(): void
    {
        $this->markTestIncomplete('Implement: GET /users');
    }

    public function test_post_users(): void
    {
        $this->markTestIncomplete('Implement: POST /users');
    }
}
```

---

CI &amp; AI Integration
-----------------------

[](#ci--ai-integration)

### Validating specs in CI

[](#validating-specs-in-ci)

Add `spectator:validate` as an early CI step to catch malformed specs before tests run:

```
# GitHub Actions example
- name: Validate OpenAPI spec
  run: php artisan spectator:validate --spec=Api.v1.yml --format=json
```

### Machine-readable error output

[](#machine-readable-error-output)

Set `SPECTATOR_ERROR_FORMAT=json` in your CI environment to make validation errors parseable by log aggregators and LLM agents:

```
SPECTATOR_ERROR_FORMAT=json
```

With this setting, a failed assertion produces a JSON error body instead of ANSI-coloured text:

```
{
    "errors": [
        "The data (null) must match the type: string"
    ]
}
```

### Feeding errors to an LLM

[](#feeding-errors-to-an-llm)

The JSON error format is designed for toolchains that analyse test output programmatically. Parse `{"errors": [...]}` from test output and pass it directly to your LLM workflow for root-cause analysis or spec repair suggestions.

### Contract coverage tracking

[](#contract-coverage-tracking)

`SpectatorExtension` is a PHPUnit 11 extension that tracks which spec operations are exercised during a test run and prints a coverage summary when the suite finishes.

Enable it in `phpunit.xml`:

```

```

Example output at suite end:

```
Spectator Coverage
──────────────────────────────────────────
 Spec          Operations   Covered   %
──────────────────────────────────────────
 Api.v1.yml    6            5         83%
──────────────────────────────────────────

```

When `min_coverage` is set and not met, the extension causes PHPUnit to exit with code `1`, failing the CI job.

---

Upgrading
---------

[](#upgrading)

Please read [UPGRADE.md](UPGRADE.md) for a full list of breaking changes between versions.

---

Core Concepts
-------------

[](#core-concepts)

Spectator registers a middleware that intercepts every test request, matches it against the loaded spec's `PathItem`, and validates both the request and the response. Captured exceptions are stored on the `RequestFactory` singleton so assertions can read them after the response is returned.

### Key dependencies

[](#key-dependencies)

- [`cebe/php-openapi`](https://github.com/cebe/php-openapi) — parses OpenAPI 3.x specs into typed objects
- [`opis/json-schema`](https://github.com/opis/json-schema) — validates request/response data against JSON Schema

---

Sponsors
--------

[](#sponsors)

A huge thanks to all our sponsors who help push Spectator development forward!

If you'd like to become a sponsor, please [see here for more information](https://github.com/sponsors/hotmeteor). 💪

Credits
-------

[](#credits)

- Created by [Adam Campbell](https://github.com/hotmeteor)
- Maintained by [Bastien Philippe](https://github.com/bastien-phi), [Jarrod Parkes](https://github.com/jarrodparkes), and [Adam Campbell](https://github.com/hotmeteor)
- Inspired by [Laravel OpenAPI](https://github.com/mdwheele/laravel-openapi) package by [Dustin Wheeler](https://github.com/mdwheele)
- [All Contributors](../../contributors)

[ ![](https://camo.githubusercontent.com/9a9777f15711f8751302e1f30d15b08ec9c37f558a70cc2c0be3b6e59856083c/68747470733a2f2f636f6e747269622e726f636b732f696d6167653f7265706f3d686f746d6574656f722f737065637461746f72)](https://github.com/hotmeteor/spectator/graphs/contributors)Made with [contributors-img](https://contrib.rocks).

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

70

—

ExcellentBetter than 100% of packages

Maintenance87

Actively maintained with recent releases

Popularity61

Solid adoption and visibility

Community32

Small or concentrated contributor base

Maturity85

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 51.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~32 days

Recently: every ~1 days

Total

67

Last Release

63d ago

Major Versions

v0.8.5 → v1.0.02021-04-21

v1.11.0 → v2.02024-03-19

v2.3.0 → v3.0.02026-04-28

PHP version history (6 changes)v0.1.0PHP ^7.2

v0.5.5PHP ^7.2|^8.0

v0.8.0PHP ^7.3|^8.0

v1.0.0PHP ^7.4|^8.0

v2.0PHP ^8.1

v3.0.0PHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/378585?v=4)[Adam Campbell](/maintainers/hotmeteor)[@hotmeteor](https://github.com/hotmeteor)

---

Top Contributors

[![hotmeteor](https://avatars.githubusercontent.com/u/378585?v=4)](https://github.com/hotmeteor "hotmeteor (115 commits)")[![bastien-phi](https://avatars.githubusercontent.com/u/10199039?v=4)](https://github.com/bastien-phi "bastien-phi (53 commits)")[![jarrodparkes](https://avatars.githubusercontent.com/u/1331063?v=4)](https://github.com/jarrodparkes "jarrodparkes (7 commits)")[![hirotaka056](https://avatars.githubusercontent.com/u/94840601?v=4)](https://github.com/hirotaka056 "hirotaka056 (4 commits)")[![matthewtrask](https://avatars.githubusercontent.com/u/4731244?v=4)](https://github.com/matthewtrask "matthewtrask (4 commits)")[![beblife](https://avatars.githubusercontent.com/u/9271492?v=4)](https://github.com/beblife "beblife (4 commits)")[![tsterker](https://avatars.githubusercontent.com/u/1156230?v=4)](https://github.com/tsterker "tsterker (4 commits)")[![gdebrauwer](https://avatars.githubusercontent.com/u/22586858?v=4)](https://github.com/gdebrauwer "gdebrauwer (3 commits)")[![jerredhurst](https://avatars.githubusercontent.com/u/13042804?v=4)](https://github.com/jerredhurst "jerredhurst (2 commits)")[![SudoGetBeer](https://avatars.githubusercontent.com/u/7324694?v=4)](https://github.com/SudoGetBeer "SudoGetBeer (2 commits)")[![DeepDiver1975](https://avatars.githubusercontent.com/u/1005065?v=4)](https://github.com/DeepDiver1975 "DeepDiver1975 (2 commits)")[![laravel-shift](https://avatars.githubusercontent.com/u/15991828?v=4)](https://github.com/laravel-shift "laravel-shift (2 commits)")[![jransijn](https://avatars.githubusercontent.com/u/61250138?v=4)](https://github.com/jransijn "jransijn (1 commits)")[![lentex](https://avatars.githubusercontent.com/u/9319316?v=4)](https://github.com/lentex "lentex (1 commits)")[![paulund](https://avatars.githubusercontent.com/u/1306197?v=4)](https://github.com/paulund "paulund (1 commits)")[![pH-7](https://avatars.githubusercontent.com/u/1325411?v=4)](https://github.com/pH-7 "pH-7 (1 commits)")[![philsturgeon](https://avatars.githubusercontent.com/u/67381?v=4)](https://github.com/philsturgeon "philsturgeon (1 commits)")[![skylerkatz](https://avatars.githubusercontent.com/u/7297992?v=4)](https://github.com/skylerkatz "skylerkatz (1 commits)")[![spodmore](https://avatars.githubusercontent.com/u/31791673?v=4)](https://github.com/spodmore "spodmore (1 commits)")[![andrew-demb](https://avatars.githubusercontent.com/u/12499813?v=4)](https://github.com/andrew-demb "andrew-demb (1 commits)")

---

Tags

testinglaravelopenapispectator

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/hotmeteor-spectator/health.svg)

```
[![Health](https://phpackages.com/badges/hotmeteor-spectator/health.svg)](https://phpackages.com/packages/hotmeteor-spectator)
```

###  Alternatives

[spurwork/spectator

Testing helpers for your OpenAPI spec

3051.5k](/packages/spurwork-spectator)[orchestra/testbench

Laravel Testing Helper for Packages Development

2.2k42.5M40.8k](/packages/orchestra-testbench)[orchestra/workbench

Workbench Companion for Laravel Packages Development

8320.3M75](/packages/orchestra-workbench)[osteel/openapi-httpfoundation-testing

Validate HttpFoundation requests and responses against OpenAPI (3+) definitions

1202.1M7](/packages/osteel-openapi-httpfoundation-testing)[guanguans/laravel-soar

SQL optimizer and rewriter for laravel. - laravel 的 SQL 优化器和重写器。

2248.4k](/packages/guanguans-laravel-soar)[api-platform/laravel

API Platform support for Laravel

58170.8k14](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
