Code Generation

PhpSpec generates specs, classes, interfaces, method stubs, and step definitions to accelerate the BDD workflow.

The describe Command

Generate a spec for a class:

$ vendor/bin/phpspec describe App/Calculator

Creates spec/App/Calculator.spec.php with a template:

<?php

use App\Calculator;

describe(Calculator::class, function() {
    this("calculator", fn() => new Calculator());
    it("instantiates", fn() => expect($this->calculator)->toBeAnInstanceOf(Calculator::class));
});

With a Method Example

$ vendor/bin/phpspec describe App/Calculator -e add

Generates a spec that includes an example for the add method.

Path Convention

The argument uses / as separator: App/Calculator becomes spec/App/Calculator.spec.php. This maps to the namespace App\Calculator.

A class under none of the mapped namespaces

When the project maps namespaces to directories (see below) and the name is under none of them, describe asks which mapped namespace to put it under, or to keep it as written:

$ vendor/bin/phpspec describe Acme/Thing

Acme\Thing is under none of the mapped namespaces: Brew\, Another\. Describe:
  [1] Brew\Acme\Thing
  [2] Another\Acme\Thing
  [0] Acme\Thing, as written
  >

Enter takes the first. A default_namespace in the config answers for every such name, so describe Thing becomes Brew\Acme\Thing without a question, while a name already under a mapping stays as written. With nobody to answer (-n, --format=agent) and no default, nothing is written and the exit code is 1; the agent receipt carries the error and a remedy.

Where Generated Classes Go

A class is written where its own namespace lives, mapping by mapping. The mappings come from:

  1. The phpspec config, when it states a layout: the autoload map, and src_path with psr4_prefix (or a suite's src with namespace) as one more mapping. The prefix stands for the directory, so App\ mapped to src puts App\Model\User in src/Model/User.php.
  2. Otherwise composer.json: every entry under autoload.psr-4, the first directory of an entry that lists several.

When mappings nest, the longest prefix wins: with App\ and App\Legacy\ both mapped, App\Legacy\Order goes where App\Legacy\ points. A class under no mapping goes below src_path (src by default) with its whole name as directories: Acme\Thing in src/Acme/Thing.php. The next run finds a class at either place without a composer dump.

The exemplify Command

Add an example for a single method to a spec (creating the spec first if it doesn't exist):

$ vendor/bin/phpspec exemplify App/Calculator add

Both describe -e and exemplify are idempotent: an example for a method that is already present is not duplicated.

Machine-Readable Receipts (--format=agent)

describe and exemplify accept --format=agent, which replaces their prose with a one-line JSON receipt, so a coding agent can scaffold without parsing English:

$ vendor/bin/phpspec describe App/Calculator --format=agent
{"v":2,"action":"describe","class":"App\\Calculator","spec":"spec/App/Calculator.spec.php","created":true}

created (and exemplify's added) report whether anything changed. See Coding Agents for the full contract.

Auto-Class Generation

When a spec describes a class that doesn't exist, the run says so in place of the spec's tree and error detail, and offers the class in the same breath:

Looks like you are trying to spec App\Calculator,
a class that doesn't exist yet.

Would you like me to generate that class for you? [Y/n]

A spec that needs some other class than the one it describes reads Looks like App\Basket needs App\Coupon, a class that doesn't exist yet. and offers a spec for it before the class, the way a class missing from a step is offered: outside-in says what a class is for before the class exists. If confirmed, ClassGenerator creates the class and names the file, src/App/Calculator.php:

<?php

namespace App;

class Calculator
{
}

A class whose name ends in Exception extends Exception, imported with a use line, so it can be thrown the moment it exists.

Interface Generation

When a mock references a type that doesn't exist, PhpSpec offers to generate it as an interface, naming the file it would write:

Do you want me to create interface App\UserRepository in src/App/UserRepository.php? [Y/n]

If confirmed, InterfaceGenerator creates an interface at src/App/UserRepository.php:

<?php

namespace App;

interface UserRepository
{

}

Method Addition to Interfaces

When a spec calls a method on a double that its interface does not define, the error names the interface, Call to undefined method App\UserRepository::find(), and PhpSpec offers to add the method:

Do you want me to add method find() to interface App\UserRepository? [Y/n]

This appends the method signature to the interface.

Method Stub Generation

When specs use methods that don't exist on a class, MethodStubGenerator can add method stubs:

// If Calculator::add() doesn't exist:
expect($calc->add(2, 3))->toBe(5);

PhpSpec generates:

public function add()
{
}

--fake Mode

With the --fake flag, PhpSpec goes beyond empty stubs and generates method bodies with hardcoded return values based on your spec expectations:

$ vendor/bin/phpspec run --fake

Given the spec:

expect($calc->add(2, 3))->toBe(5);

PhpSpec generates:

public function add()
{
    return 5;
}

The --fake flag works by extracting the expected values from matcher results (via fakeExpression metadata) and using them as return values. This creates a quick feedback loop: write spec, run with --fake, get passing tests immediately, then replace the faked implementation with real logic.

The value comes back exactly as the spec wrote it: 90.0 stays a float and 'He said "hello"' keeps its quotes. The call can be on a variable, a this(), or a subject built inline, expect((new Greeter('Ann'))->greet()).

Two things are never faked: a return no literal expresses (an object, or an array holding one), and a method whose result the expectation went on to call something on, as in expect(Oops::for('x')->getMessage()), where the value belongs to getMessage() and not to for().

Nothing is written without an answer

Everything above is offered, never assumed. PhpSpec asks [Y/n] and writes only what you say yes to, where pressing Enter takes the default and says yes.

A run with nobody to answer writes nothing at all. That covers --no-interaction, a CI job, an editor or agent shelling out, and a terminal whose input has already ended: in each case the question is printed with what it would have created, followed by the way to accept it, and no file appears.

Looks like you are trying to spec App\Basket,
a class that doesn't exist yet.

Would you like me to generate that class for you?
  Nothing was written: there is nobody to answer. Run with --accept-offers to create classes.

Silence is not consent: a run that cannot be answered must never leave code in a source tree that nobody agreed to.

Applying Offers Non-Interactively (--accept-offers)

--accept-offers is how you say yes in advance. It applies every pending offer (missing classes, interfaces, methods, and feature steps) in one non-interactive pass, then exits 0:

$ vendor/bin/phpspec run --accept-offers            # generate all missing code, no prompts
$ vendor/bin/phpspec run --accept-offers --fake     # ...and fill empty methods with spec'd returns

This is the scripted counterpart to the prompts, built for --format=agent consumers and CI, where an agent reads the run's offers and then has PhpSpec generate them. See Coding Agents for the offer format and the full loop.

Generating From an Instruction (generate)

The generators above are deterministic: they emit fixed stubs (toBe(null), empty method bodies). When a spec is red because an existing method needs real behaviour (not because a class or method is missing), the generate command turns a natural-language instruction into one file edit (a spec example or a piece of implementation code) authored by the AI, shown as a diff, and written after you confirm:

$ vendor/bin/phpspec generate an example for App/Calculator add where it sums two numbers
$ vendor/bin/phpspec generate implement Calculator::add to return the sum of its arguments
$ vendor/bin/phpspec generate a scenario in features/user_adds_tasks.feature

It requires an AI provider. A spec edit is checked so it never drops an existing example (specs are grown, not shrunk). When your instruction names a file path, that path is honoured over anything the model picks, and its extension decides the artifact: a .feature path is written as a Gherkin scenario by a deterministic generator (never as a spec), a .spec.php path stays a spec, and a src/…php path stays implementation code.

With no path, the wording routes the request: feature/scenario/story wording becomes a Gherkin skeleton under your configured features_path (no model call), the steps appends step definitions for the last-touched feature to steps.php by parsing it (no model call), spec wording derives the spec path from the class you named, and implement/method wording derives the source path through your configured layout (PSR-4 prefix included). Every exchange is captured to .phpspec/ai/last-request.json for debugging. In pair mode the same thing is /generate <instruction>, with the diff confirmed through the numbered chooser. See Pair Programming & AI and the CLI Reference.

Step Definition Generation

When a run finds undefined steps, PhpSpec offers them all at once, for one steps file: features/steps/steps.php when there is none yet, or the steps file you pick (or a new one you name) when there are:

You have undefined steps. Would you like me to generate the steps for you? [Y/n]
  Step definitions generated at features/steps/steps.php

The generated file contains:

<?php

given('a calculator', function () {
    pending();
});

See Story BDD for more on step definitions.

The BDD Cycle

PhpSpec's code generation creates a natural BDD workflow:

  1. Write a spec: vendor/bin/phpspec describe App/Calculator
  2. Run specs: PhpSpec prompts to generate the missing class
  3. Add examples: write expect($calc->add(2, 3))->toBe(5)
  4. Run again: PhpSpec offers to generate the add() method
  5. Implement: fill in the real logic (or use --fake for a quick pass)
  6. Repeat: add more examples, drive the design through specs

The autoloader is registered in vendor/bin/phpspec:

spl_autoload_register([\PhpSpec\CodeGeneration\Autoloader::class, 'autoload']);