A command-line tool that converts legacy phpspec specs (ObjectBehavior style, phpspec ≥ 8.3)
into the new phpspec 9.0 closure DSL (describe / it / expect).
It parses each spec with nikic/php-parser (using the PHP version that is running the tool) and rewrites the AST — no fragile regex.
composer installRequires PHP 8.2+.
# Convert a single spec
bin/phpspec-migrate spec/App/CalculatorSpec.php
# Convert a whole directory (recurses, picks up *Spec.php)
bin/phpspec-migrate spec/
# Preview without writing anything
bin/phpspec-migrate --dry-run spec/
# Convert even files with constructs that can't be fully translated,
# leaving `// TODO [phpspec-migrate]` markers behind
bin/phpspec-migrate --try-hard spec/
# Write the converted tree elsewhere (mirrors the source sub-directories)
bin/phpspec-migrate --output-dir=spec9 spec/
# Replace the originals in place — deletes each *Spec.php (asks to confirm)
bin/phpspec-migrate --in-place spec/bin/phpspec-migrate --help lists every option.
There is no destination argument; <path> is always the source. By default each
FooSpec.php is written next to the original as Foo.spec.php, leaving the original in
place so you can diff and verify, then delete it. Two flags change this:
--output-dir=DIR— write the converted tree underDIR, mirroring the relative source sub-directories (spec/App/CalculatorSpec.php→DIR/App/Calculator.spec.php). The originals are kept. Good for a reviewable, side-by-side migration.--in-place— writeFoo.spec.phpand delete the originalFooSpec.phpin one step. Because this is destructive it asks for confirmation ([y/N], defaulting to no — pressyto proceed); run it on a clean git branch. Cannot be combined with--output-dir. With--dry-runit neither prompts nor changes anything.
Use --force to overwrite an existing .spec.php target and -q/--quiet to suppress
the report.
The converter reports two kinds of finding:
- Notes — a workaround was applied, but it's lossy or worth a glance (see the limitations below). The file is always converted; the note is printed in the report.
- Issues — a construct that genuinely can't be converted (e.g. an unsupported
Prophecy stub). By default the file is skipped and listed with the reason. Pass
--try-hardto convert it anyway, leaving the construct in place with a// TODO[phpspec-migrate]comment so you can finish it by hand.
| Legacy (≤ 8.3) | phpspec 9.0 |
|---|---|
class FooSpec extends ObjectBehavior |
describe(Foo::class, function () { … }) |
function it_does_x(Dep $d) |
it("does x", function (Dep $d) { … }) |
let() + beConstructedWith($a) |
let('foo', fn(Dep $d) => new Foo($d)) |
beConstructedThrough('make', [$a]) |
Foo::make($a) |
in-example $this->beConstructedWith(...) |
$this->foo = new Foo(...) |
$this->method() |
$this->foo->method() |
$this->m()->shouldReturn($v) |
expect($this->foo->m())->toBe($v) |
shouldHaveType / shouldBeAnInstanceOf / shouldImplement |
toBeAnInstanceOf |
shouldBe / shouldEqual / shouldReturn |
toBe |
shouldBeLike |
toBeLike |
shouldNotXxx |
not()->toXxx |
$d->m()->willReturn($v) |
allow($d->m())->toReturn($v) |
$d->m()->willThrow($e) |
allow($d->m())->toThrow($e) |
$d->m()->shouldBeCalled() |
expect($d->m())->toBeCalled() |
$d->m()->shouldHaveBeenCalled() |
expect($d->m())->toHaveBeenCalled() |
Argument::any() / type() / that() / cetera() / exact() |
any() / type() / callback() / cetera() / value |
shouldThrow(E)->during('m', [$a]) |
expect(fn() => $this->foo->m($a))->toThrow(E::class) |
shouldThrow(E)->during('__construct', [...]) |
expect(fn() => new Foo(...))->toThrow(E::class) |
getMatchers() inline matchers |
addMatcher('toName', $callback) |
These constructs have no clean 1:1 equivalent in the phpspec 9 DSL. The tool applies the following workarounds rather than failing.
A helper method has no place inside the describe() closure, so it is extracted to a
top-level function (same name, parameters, return type and body), emitted after the
describe(...) block:
describe(SomeClass::class, function () {
// ... examples
});
function someHelper() { /* ... */ } // extracted from a private/protected spec methodThe file converts normally and a note is recorded. Caveat: if the helper referenced
$this (the subject or collaborators), there is no $this in a free function — review
those by hand.
Matchers such as Argument::containingString() have no counterpart, so they are
replaced with any():
$repo->find(Argument::containingString('x')) // legacy
allow($repo->find(any())) // convertedThis keeps the spec runnable but is less strict (it matches any argument); a note flags every place where precision was lost so you can tighten it if needed.
There is currently no workaround for stubs like will() / willReturnArgument().
Such files are skipped by default and reported; with --try-hard they are converted with
the offending line left in place under a // TODO [phpspec-migrate] comment for you to
finish manually. (willReturn() and willThrow() are supported.)
PRs welcomed!