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:
-
The phpspec config, when it states a layout: the
autoloadmap, andsrc_pathwithpsr4_prefix(or a suite'ssrcwithnamespace) as one more mapping. The prefix stands for the directory, soApp\mapped tosrcputsApp\Model\Userinsrc/Model/User.php. -
Otherwise
composer.json: every entry underautoload.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:
- Write a spec:
vendor/bin/phpspec describe App/Calculator - Run specs: PhpSpec prompts to generate the missing class
- Add examples: write
expect($calc->add(2, 3))->toBe(5) - Run again: PhpSpec offers to generate the
add()method - Implement: fill in the real logic (or use
--fakefor a quick pass) - 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']);