Skip to content

Proof of concept for PhpSpec 9#1511

Open
MarcelloDuarte wants to merge 37 commits intophpspec:mainfrom
MarcelloDuarte:phpspec-9-poc
Open

Proof of concept for PhpSpec 9#1511
MarcelloDuarte wants to merge 37 commits intophpspec:mainfrom
MarcelloDuarte:phpspec-9-poc

Conversation

@MarcelloDuarte
Copy link
Copy Markdown
Member

Summary

This is a proof of concept for PhpSpec 9 — a ground-up modernisation of the framework.

Why PhpSpec 9?

PhpSpec has served the PHP community well for over a decade, but the ecosystem has moved on:

  • PHP itself has evolved dramatically. Enums, fibers, readonly classes, first-class callables, named arguments — modern PHP enables patterns that weren't possible when PhpSpec was designed. The current architecture can't take full advantage of these.

  • The dependency surface has grown fragile. Maintaining compatibility across Prophecy, Doctrine Instantiator, Sebastian Exporter, four Symfony packages, and their cascading version matrices is an ever-growing maintenance burden. Every upstream release risks breakage. PhpSpec 9 reduces runtime dependencies from 9 packages to 2 (symfony/console + symfony/yaml).

  • The ObjectBehavior pattern creates friction. Developers must learn a DSL that maps $this->shouldReturn() to assertions and $this->beConstructedWith() to constructors. It's powerful but opaque. The describe/it/expect pattern (used by virtually every other testing framework across languages) is immediately familiar.

  • Story BDD shouldn't require a separate tool. Currently phpspec users need Behat for feature-level testing. PhpSpec 9 has built-in Gherkin support with given()/when()/then() step definitions, unifying Spec BDD and Story BDD in a single tool.

  • AI-assisted development is the new reality for BDD. AI coding assistants can generate classes, methods, and entire modules in seconds — but speed without direction is dangerous. An AI will happily produce code that looks correct but doesn't match the intended behaviour. This is exactly the problem BDD was designed to solve: a spec is a machine-readable contract that validates behaviour, whether a human or an AI writes the implementation. BDD becomes more important with AI, not less. PhpSpec 9 embraces this by making AI a first-class participant in the BDD cycle, not an afterthought.

What changes

Syntax — Jasmine/RSpec-style closures replace ObjectBehavior:

// PhpSpec 8
class CalculatorSpec extends ObjectBehavior
{
    function it_adds_numbers()
    {
        $this->add(2, 3)->shouldReturn(5);
    }
}

// PhpSpec 9
describe(Calculator::class, function () {
    it('adds numbers', function () {
        expect((new Calculator())->add(2, 3))->toBe(5);
    });
});

Mocking — Built-in, no Prophecy:

it('logs messages', function (Logger $logger) {   // auto-mocked
    allow($logger->info())->toReturn(true);
    // ... exercise SUT ...
    expect($logger->info())->toBeCalled()->once();
});

Story BDD — Built-in Gherkin support:

Feature: Calculator
  Scenario: Adding numbers
    Given I have a calculator
    When I add 2 and 3
    Then the result should be 5
given('I have a calculator', function () {
    $this->calculator = new Calculator();
});

Dependencies — Dramatically reduced:

PhpSpec 8 PhpSpec 9
Runtime packages 9 2
Prophecy Required Removed
Doctrine Instantiator Required Removed
Sebastian Exporter Required Removed
Symfony packages 5 (Console, EventDispatcher, Process, Finder, Yaml) 2 (Console, Yaml)

AI-powered pair programming

PhpSpec 9 includes three AI commands — pair, next, and refactor — that embed the AI inside the BDD cycle rather than running alongside it. All AI features are opt-in: they require a suggest dependency (papi-ai/papi-core + a provider package) and an ai: section in your config. Without them, PhpSpec works exactly as it does without AI — zero runtime cost, no API keys needed.

Why AI belongs in the BDD tool

AI assistants generate code fast, but they don't know what behaviour is correct. Specs do. By placing the AI inside PhpSpec, every generated class and method is immediately validated against specs. The BDD cycle becomes the contract that keeps both human and AI-generated code honest:

Feature scenario (failing)
  → AI generates step definitions
    → Spec (failing)
      → AI generates class/method
        → Spec (green) ← the spec decides what "correct" means
      → Step (green)
    → Scenario (green)

The AI accelerates the cycle; the specs guarantee correctness. Neither replaces the other.

pair — Interactive BDD REPL

Start an interactive pair programming session:

bin/phpspec pair

Or send a single prompt:

bin/phpspec pair --prompt "write a spec for a Calculator that adds two numbers"

The pair command launches a REPL where built-in commands (describe, exemplify, run) work without AI, and free-form input routes to the AI assistant when configured. Smart routing detects intent:

> describe App/Calculator                       # runs describe command
> describe what the Loader class does            # routes to AI (natural language)
> run spec/App                                   # runs specs
> run my specs and explain the failures          # routes to AI

The AI is agentic — it has tools to generate specs, features, step definitions, write/update files, run specs, and read project files. It receives your project's directory structure, existing step definitions, the full DSL reference, and matcher/mock syntax as context, so it writes code that matches your project's patterns and reuses existing steps.

A typical pairing flow:

> write a feature scenario for user registration

  ✓ Generated features/scenarios/user_registration.feature
  ✓ Generated features/steps/user_registration.steps.php

> run features/

  ✗ Class App\UserRepository not found

> describe App/UserRepository

  ✓ Generated spec/App/UserRepository.spec.php
  ✓ Generated src/App/UserRepository.php

> now add a findByEmail method that returns a User or null

  ✓ Updated spec/App/UserRepository.spec.php
  ✓ Updated src/App/UserRepository.php

> run

  ✓ All specs pass
  ✓ All scenarios pass

Conversation history is maintained across the session — the AI remembers what you've been working on and builds on previous exchanges. All interactions are logged to .phpspec/pair.log.

next — What should I work on?

bin/phpspec next

Scans your project's source, specs, and features, then suggests the single most impactful next step. It follows the scenario-first workflow — recommends a feature scenario before a spec, a spec before an implementation:

  Analysing project...

  Write a feature scenario for user registration.

  Your project has a UserRepository class with a spec, but no feature
  scenario covering the registration flow. Adding a scenario first will
  drive the step definitions and any missing specs.

refactor — AI-powered, behaviour-preserving refactoring

bin/phpspec refactor "App\Calculator"
bin/phpspec refactor "App\Calculator::sum"

The command:

  1. Runs specs to establish a green baseline (refuses to refactor broken code)
  2. Sends source + spec to the AI, which identifies a single baby-step refactoring
  3. Applies the change and re-runs specs
  4. If specs pass → keeps the change and shows a diff. If specs fail → rolls back automatically
Technique: Extract Method
Description: Extracted validation logic into a validateInput() method

   1   <?php
   2   namespace App;
   3 - class Calculator {
   3 + class Calculator {
   4       public function add(int $a, int $b): int {
   5 -         if ($a < 0 || $b < 0) {
   6 -             throw new \InvalidArgumentException('Negative');
   7 -         }
   5 +         $this->validateInput($a, $b);
              return $a + $b;
          }
  10 +
  11 +     private function validateInput(int $a, int $b): void {
  12 +         if ($a < 0 || $b < 0) {
  13 +             throw new \InvalidArgumentException('Negative');
  14 +         }
  15 +     }
      }

Specs still pass.

AI configuration

Add an ai: section to phpspec.yaml:

ai:
  provider: anthropic          # or google, openai
  model: claude-sonnet-4-20250514  # optional, sensible defaults per provider
  api_key: YOUR_API_KEY

Supported providers: Google (Gemini), Anthropic (Claude), OpenAI (GPT). The provider abstraction is behind an interface (ProviderInterface) — adding new providers is straightforward.

What's included

  • Full spec suite: 86 specs, 1030+ examples, 91%+ coverage
  • Feature scenarios for all major commands
  • Code generation (specs, classes, interfaces, methods)
  • Formatters: Pretty, Dot, TAP, JUnit XML
  • Parallel execution via PHP Fibers
  • AI pair programming, next-step suggestion, and automated refactoring (opt-in)
  • Documentation in docs/

Status

This is a proof of concept for discussion. It demonstrates the direction, not a final implementation. Feedback on the approach, syntax choices, and migration path is welcome.

Test plan

Core

  • composer install
  • php bin/phpspec run — all specs pass
  • php bin/phpspec run features/ — all feature scenarios pass
  • vendor/bin/php-cs-fixer fix --dry-run — code style clean
  • vendor/bin/phpstan analyse -l 1 src/ — static analysis clean

AI pair programming (optional — requires API key)

To try the AI flow, install a provider and add config:

composer require papi-ai/papi-core papi-ai/google   # or papi-ai/anthropic, papi-ai/openai
# phpspec.yaml
ai:
  provider: google       # or anthropic, openai
  api_key: YOUR_API_KEY

Then experiment with the BDD cycle:

  • php bin/phpspec pair — enter the interactive REPL; try built-in commands (describe App/Greeter, run) without AI first
  • In pair mode, ask the AI to generate a feature: write a feature scenario for greeting users — verify it creates .feature and .steps.php files
  • In pair mode, run the feature and let the AI drive the spec/class generation cycle: run features/describe App/Greeter → ask the AI to add methods → run until green
  • php bin/phpspec pair --prompt "write a spec for a Calculator" — single-prompt mode, verify it generates a spec file
  • php bin/phpspec next — verify it scans the project and suggests a next step following the scenario-first workflow
  • php bin/phpspec refactor "App\Greeter" — verify it runs specs, applies a refactoring, and re-runs specs (or reports no refactoring needed)

@MarcelloDuarte MarcelloDuarte force-pushed the phpspec-9-poc branch 5 times, most recently from 47a29bb to 3c692b8 Compare March 15, 2026 14:29
@MarcelloDuarte
Copy link
Copy Markdown
Member Author

@torchello @Jean85 @stof @everzet @ciaranmcnulty I also put together a website with the new concept

https://phpspec-site.fly.dev/

@Jean85
Copy link
Copy Markdown
Contributor

Jean85 commented Mar 16, 2026

Questions:

  1. would this mean rewriting it with AI? What about license concerns?
  2. Such a hard breaking change would stop a lot of users from upgrading; can we ease the transition in some way?

@stof
Copy link
Copy Markdown
Member

stof commented Mar 16, 2026

This looks like a totally different test framework. Maybe it should be released as a different package instead of a new major version of phpspec/phpspec to allow projects to use both at the same time to adopt the new tool little by little.

operating-system: ubuntu-latest
composer-flags: --prefer-lowest

- name: Run static analysis (psalm)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removing static analysis from the CI setup ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had stan locally, didn't realise we had psalm until merging. Restored 7e23af2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I'm fine with using phpstan (this is what I'm using in all my projects, as I find it superior these days, especially when it comes to extensions). but it should be part of the CI

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, reverting it to stan (I prefer it also), restored 7e1284b

Comment thread .github/workflows/build.yml Outdated
asset_path: phpspec.phar
asset_name: phpspec.phar
asset_content_type: application/zip
- uses: actions/download-artifact@v4
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not downgrade the action

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored to v5 9a9d91b

Comment thread composer.json Outdated
"symfony/console": "^7.0 || ^8.0",
"symfony/yaml": "^7.0 || ^8.0",
"ext-dom": "*",
"ext-curl": "*",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why making ext-curl a mandatory requirement ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving it to suggest. It is used with some optional AI features 7fecb99

Comment thread phpstan.neon Outdated
@@ -0,0 +1,6 @@
parameters:
level: 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

level 1 of phpstan is basically useless. A new codebase should actually start at level 8 or higher.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restored psalm anyway

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* Supports Background, Scenario, Scenario Outline with Examples tables, DataTables,
* Doc strings, tags, and And/But step keywords.
*/
final class GherkinParser
Copy link
Copy Markdown
Member

@stof stof Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't write another Gherkin parser. Use cucumber/gherkin instead to use the official parser that is tested against the Gherkin shared testsuite (or use behat/gherkin if you prefer, but that would be better to use the official parser to be future-proof).

The current parser implementation is not spec compliant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. 03d5dd0

* Manages a scope stack to track the current context/example registry during spec
* file loading, and dispatches events to registered subscribers and listeners.
*/
final class Dispatcher
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it expected that the dispatcher is all based on global state while not being internal ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is pretty common for function based testing frameworks to keep the state somewhere global. However we can use a slightly more OO approach for that f997733

Comment thread src/PhpSpec/Ai/AiTools.php Outdated
],
handler: function (array $args) use ($filesystem) {
$path = $args['path'];
if (!str_starts_with($path, '/')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't work on Windows. Absolute paths don't always start with /

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 6c41347

return "File not found: {$args['path']}";
}

return $filesystem->read($path);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this protect against directory traversal like ../../../etc/password or /etc/password which are outside the project ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a wrapper

Comment thread src/PhpSpec/Browser/functions.php Outdated
* Static registry holding the Browser Client instance.
* Lazily creates the client from Configuration on first access.
*/
class BrowserRegistry
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this class be registered in its own file following PSR-4 ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thus, it is currently defined outside the phpspec namespace, which is even worse.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 5337cfb

* Generates PHP class files from a fully qualified class name.
* Creates the directory structure and writes a minimal class skeleton.
*/
final class ClassGenerator
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these classes should probably be tagged as @internal, to properly define the public API covered by semver.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed f66698d

Comment thread src/PhpSpec/Report/Formatter/Tap.php Outdated
*/
private function yamlEscape(string $value): string
{
if (str_contains($value, "\n") || str_contains($value, '"') || str_contains($value, ':')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A string starting with a single quote will also need escaping, to avoid being parsed as a single-quoted string.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 9027ce9

Comment thread composer.json Outdated
"php": "^8.2 || ^8.3 || ^8.4 || ^8.5",
"symfony/console": "^7.0 || ^8.0",
"symfony/yaml": "^7.0 || ^8.0",
"ext-dom": "*",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't ext-dom needed only for the JUnit formatter ? If yes, it should be an optional dependency IMO.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 671aa6e

Comment thread CHANGES-v9.md Outdated

## [9.0.0-poc] — Proof of Concept

### Breaking Changes
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another breaking change: breaks compat with all existing phpspec extensions, as it is basically a different project.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intentional — the extension API has changed significantly (event dispatcher, formatter interface, etc.). We'll document a migration guide for extension authors. The new extension interfaces (ListenerExtension, MatcherExtension, FormatterExtension, CommandExtension) are simpler than the current system. Releasing as a separate package (per your earlier suggestion) also avoids breaking existing extensions that target phpspec/phpspec ^8.

Comment thread CHANGES-v9.md Outdated
@@ -0,0 +1,35 @@
# Changelog — PhpSpec 9
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why using a separate changelog file ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ef8d11b — consolidated into CHANGES.md, removed CHANGES-v9.md

Comment thread check-release.php
<?php

/**
* Sanity-checks a release for consistency
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ius this script gone ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ef8d11b — restored check-release.php adapted for the v9 structure

@stof
Copy link
Copy Markdown
Member

stof commented Mar 16, 2026

@MarcelloDuarte the website using gray text on a black background is almost unreadable. Please change the website styles to use good contrasting colors.

@MarcelloDuarte
Copy link
Copy Markdown
Member Author

Questions:

  1. would this mean rewriting it with AI? What about license concerns?
  2. Such a hard breaking change would stop a lot of users from upgrading; can we ease the transition in some way?

@Jean85 thanks for the feedback! 🙏

On the AI question — the code is original, written with AI as a development tool (same as using an IDE with autocomplete). No third-party code was incorporated, so no license concerns. I kept Konstantin in the copyright and added Ciaran for his longstanding contributions as a maitainer.

On the migration — we'd provide a migration tool to mechanically convert ObjectBehavior specs to the new syntax, and maintain 8.x with security/compatibility fixes during a transition period. Were you thinking of anything beyond that?

@MarcelloDuarte
Copy link
Copy Markdown
Member Author

@stof thanks for your feedback. I will go over one by one.

This looks like a totally different test framework. Maybe it should be released as a different package instead of a new major version of phpspec/phpspec to allow projects to use both at the same time to adopt the new tool little by little.

That is an excellent idea. Releasing this as phpspec/phpspec-9 or phpspec/phpspec-next makes the migration story much better. It would help with the issues raised by @Jean85 as well.

The downside is it fragments the brand/community a little, but for adoption it's arguably the pragmatic choice. If the adoption takes off, we could move to phpspec/phpspec on 9.1 to counter that.

@stof
Copy link
Copy Markdown
Member

stof commented Mar 16, 2026

Maybe using phpspec/runner could avoid the need to rename the package in the future by having an acceptable name.

Restores Psalm (level 4 with baseline) as the static analysis tool,
matching the upstream project's tooling choice.
checkout@v4 → v5, download-artifact@v4 → v5
Move version-dependent UndefinedInterfaceMethod suppression from
baseline to psalm.xml config to avoid stale baseline entries across
different Symfony Console versions.
Only needed by the optional AI provider packages, not by core.
Rewrite the hand-rolled Gherkin parser as a thin adapter over
cucumber/gherkin v39, the spec-compliant parser tested against the
Cucumber shared test suite. This adds i18n support, Rules, and
correct handling of all Gherkin edge cases.

The internal node types (FeatureNode, ScenarioNode, StepNode, etc.)
are unchanged — only the parsing layer is swapped.
Remove unused FeatureChild import and fix fn() spacing.
Cover the parse-error and null-document branches to satisfy the
90% coverage threshold.
Extract global state from Dispatcher into a separate @internal
DispatcherRegistry service locator. The Dispatcher is now a clean
instance with no static properties; all call sites go through
DispatcherRegistry::dispatcher() to access the singleton.

This addresses the review feedback about the Dispatcher being
based on global state while not being internal.
Switch static analysis from Psalm to PHPStan, running at level 8
with zero errors. Fixes all type issues across the codebase
including missing type annotations, nullable safety, return type
mismatches, and undefined method calls.

Adds GeneratedDouble interface for type-safe access to eval'd
test double methods.
The PHPStan level 8 type fixes added guard clauses and instanceof
checks that created uncovered branches. Add specs covering error
capture, result classification, mock lifecycle, formatters, and
context hooks to bring coverage from 89.6% back above 90%.
Use DIRECTORY_SEPARATOR and a regex for Windows drive letters
(e.g. C:\) instead of hardcoding '/'.
Resolve path segments and verify the resolved path stays within
the project root before reading files or listing directories.
Blocks ../../../etc/passwd and absolute paths outside the project.
Was defined inline in functions.php outside any namespace. Now
lives in PhpSpec\Browser\BrowserRegistry following PSR-4.
Tag all implementation-detail classes as @internal, leaving only
the public API (DSL functions, Expectation matchers, extension
interfaces, Configuration, and StoryBDD user-facing types)
uncovered by the annotation.
ClassGenerator::resolveFqcn() now accepts an optional psr4_prefix
parameter. When set, matching namespace segments are stripped from
the directory path so App\Model\User generates src/Model/User.php
instead of src/App/Model/User.php.

Configurable via phpspec.yaml:
  psr4_prefix: App
or per suite:
  suites:
    default:
      namespace: App
      src: src
Traits cannot be instantiated and enums cannot be extended,
so attempting to mock them should fail early with a clear message.
- Use FQCN hash in mock class names to prevent short-name collisions
- Generate mocks in PhpspecDouble namespace to avoid global conflicts
- Prefix internal properties with ______phpspec_ to avoid parent conflicts
- Handle intersection return types by implementing all interfaces
- Fix formatTypeSyntax to recurse into union/intersection members
- Keep ReflectionType objects instead of overwriting with strings
- Include class hash in MockedMethodFor wrapper names for uniqueness
- Reject all internal interfaces (Traversable, DateTimeInterface, etc.)
  not just Throwable
Cover the new code paths from the Mock\Double refactor to restore
coverage above 90%. Also fix trait_exists/enum_exists check in
the initial class validation.
The class has a mutable call count so it cannot be readonly.
The stub configuration methods (toReturn, toThrow, toReturnUsing)
and identity methods (______PhpSpecGetDouble, ______PhpSpecGetMethod)
were duplicated across 4 eval'd class templates. Extract them into
a trait for single-source maintenance.
Replace 14 .view.php template files with a single PrettyViews class
containing static methods with proper type signatures. Removes
TemplateRenderer and the require-based rendering approach.

Each method accepts OutputInterface and typed result objects,
enabling full static analysis and eliminating the separate PHP
tags / extracted variables pattern.
Handle all YAML special characters including single quotes,
indicators (?, :, -, etc.), flow collection markers, and
control characters. Previously only newlines, double quotes
and colons triggered quoting.
ext-dom is only needed for the JUnit XML formatter and Clover
coverage reports, not for core functionality.
Drop XDEBUG_CC_DEAD_CODE flag which causes segfaults on large
suites, and add periodic flush() that drains Xdebug's internal
buffers every 50 examples to prevent memory corruption.
The periodic flush mechanism stopped and restarted Xdebug coverage
between batches, which lost context about previously loaded files
and dropped coverage from 90% to 82%. Revert to the simple
single-session approach which works correctly on CI.

The macOS segfault during local coverage collection is a known
Xdebug/macOS issue unrelated to this code.
Merge CHANGES-v9.md into CHANGES.md and remove the separate file.
Restore check-release.php adapted for the v9 project structure,
verifying version consistency across bin/phpspec, composer.json
branch alias, and CHANGES.md headings.
The parser was hardcoding 'inline.feature' as the URI, so parse
errors didn't identify which file failed. Now the Loader passes
the real file path through to parseString().
Use pickles instead of walking the GherkinDocument AST directly.
Pickles handle Rule children, background merging, and Scenario
Outline expansion automatically. Step keywords are preserved via
AST node ID lookup.
Cover doc strings, data tables, keyword resolution, and tag
filtering to restore coverage above 90%.
Was defined inline in functions.php outside any namespace. Now
lives in PhpSpec\StoryBDD\StoryBDDRegistry following PSR-4.
Replace eager init() call in functions.php with lazy accessors
steps() and hooks() that create registries on first access.
Avoids running initialization when autoloading the file outside
of phpspec execution.
When AI is not configured, the welcome screen and error messages
now indicate that an ai: section in phpspec.yaml is needed, rather
than showing AI examples that won't work.
@MarcelloDuarte
Copy link
Copy Markdown
Member Author

MarcelloDuarte commented Mar 26, 2026

Ok, let me know if we have more questions, feedback. I can redirect this to another repository either:

  • phpspec/runner ---> Stof's suggestion
  • phpspec/future ---> Similar to Python future library (where devs can experiment future features)
  • phpspec/phpspec9 ---> My preference. I don't want to distance too much from the brand and want to move in the direction of the final merge.

@torchello
Copy link
Copy Markdown
Contributor

torchello commented Apr 4, 2026

Hey folks! Sorry, I missed the PR, catching up....

@MarcelloDuarte First: this is impressive work. I need a few days to go through the code properly, but I think this direction is genuinely exciting and worth pushing forward.

New package

I agree this should be released as a new package, and phpspec/phpspec9 sounds good to me.

New style

I'm personally not a fan of describe/it style:

describe(Calculator::class, function () {
    it('adds numbers', function () {
        expect((new Calculator())->add(2, 3))->toBe(5);
    });
});

Mostly because it('adds numbers', function () { ... }) feels less natural to me than function it_adds_numbers().
That said, personal taste is secondary here. Does this new approach significantly simplify the implementation/maintenance of the framework, or is it mostly a syntax/style choice?

AI features

I’m very curious to try this and hear real feedback. AI-oriented tooling fits the current moment and can bring a lot of attention to the framework.

One side note: I expect AI to take over more of the spec and implementation loop over time, so I’d also like us to think about agent-driven workflows, not only human-authored ones. Since the AI features are optional, that feels like a good foundation for experimenting without forcing a particular direction too early.

@MarcelloDuarte
Copy link
Copy Markdown
Member Author

Hey @torchello,

Good to hear from you!

I agree this should be released as a new package, and phpspec/phpspec9 sounds good to me.

👍

Does this new approach significantly simplify the implementation/maintenance of the framework?

The attempt here was to normalise phpspec a bit more with other spec frameworks like jasmin and rspec. And yes, it(...) is a simpler construct to function it_.... This approach removes some of the magic in the previous implementation, e.g. people not understanding $this referred to the SUS was an entry barrier to some.

AI-oriented tooling fits the current moment

Exactly right! And phpspec goals has always been make test a natural part of the development. If people naturally interacts with AI nowadays, this will fit in with current styles and could potentially integrate with IDEs plugins/extensions as well.

I’d also like us to think about agent-driven workflows

Yep. That would be a nice direction to explore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants