Specification-oriented BDD framework for PHP 8.3+. Describe behaviour. Drive design. Pair with AI. Now phpspec takes agile coding into the AI era.
$ vendor/bin/phpspec pair PhpSpec Pair Programming Mode We're red on App\Calculator: adds two numbers. Type /run and I'll offer to create what's missing. │ /run Looks like you are trying to spec App\Calculator, a class that doesn't exist yet. Do you want me to create class App\Calculator for you? ▸ 1. Yes 2. Yes, and don't ask again: always create classes 3. No Class App\Calculator generated in src/App/Calculator.php │ what's the smallest step now? Thinking... The example calls add(), which does not exist yet. A stub keeps us honest: [MODIFIED] src/App/Calculator.php 7 + public function add(int $a, int $b): int 8 + { 9 + } Apply this offered change? ▸ 1. Yes 2. Yes, and don't ask again: always apply offered changes 3. No ~/code/calc ai: on | provider: anthropic | model: claude-sonnet-5
phpspec 9 replaces the ObjectBehavior base class with a closure-based DSL
inspired by RSpec and Jasmine. No Prophecy. No magic methods. Just clear, expressive specs.
class CalculatorSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType(Calculator::class); } function it_adds_two_numbers() { $this->add(2, 3)->shouldReturn(5); } }
describe(Calculator::class, function () { this('calculator', fn() => new Calculator()); it('is initializable', function () { expect($this->calculator) ->toBeAnInstanceOf(Calculator::class); }); it('adds two numbers', function () { expect($this->calculator->add(2, 3)) ->toBe(5); }); });
One framework. Zero heavy dependencies. Spec BDD and Story BDD unified.
Interactive pair REPL with tool-calling LLMs. next suggests what to spec next. refactor applies behaviour-preserving changes. Anthropic, OpenAI, and Google.
describe, context, it, this, expect: the full closure-based DSL with 30+ built-in matchers, negation, and chaining.
No Prophecy needed. mock(), allow()->toReturn(), toBeCalled()->once(). Argument matchers, type-hinted injection, and fluent call counts.
Write Gherkin .feature files with given/when/then step definitions. Background, Scenario Outline, DataTable, tags, and shared world.
Spec-first workflow: describe, exemplify commands. Auto-generate classes, interfaces, method stubs, and step definitions. --fake mode for instant green.
Pretty, Dot, TAP, JUnit formatters. Filter, random ordering, profiling, stop-on-failure, coverage reports, and quiet mode.
Built-in HTTP client: visit(), get(), post(). Response matchers for status, JSON paths, headers, and redirects.
Plug in custom matchers, formatters, commands, and event listeners. Composer-native packages with auto-discovery support.
--parallel[=N] runs specs across worker processes using Fibers. Result aggregation and ordered output.
A taste of what specs look like in phpspec 9.
$repo = mock(UserRepository::class); allow($repo->find(1))->toReturn('Alice'); allow($repo->find(2))->toReturn('Bob'); expect($repo->find(1))->toBe('Alice'); expect($repo->find(2))->toBe('Bob');
given('a greeting service', function () { $this->greeter = new Greeter(); }); when('I greet {string}', function (string $name) { $this->result = $this->greeter->greet($name); }); then('I should see {string}', function (string $expected) { expect($this->result)->toBe($expected); });
use App\UserService; use App\UserRepository; describe(UserService::class, function () { it('returns the user display name', function (UserRepository $repo) { allow($repo->find(1))->toReturn(['name' => 'Alice']); $service = new UserService($repo); expect($service->getDisplayName(1))->toBe('Alice'); }); it('returns Unknown for missing user', function (UserRepository $repo) { allow($repo->find(999))->toReturn(null); $service = new UserService($repo); expect($service->getDisplayName(999))->toBe('Unknown'); }); });