Proof of concept for PhpSpec 9#1511
Conversation
47a29bb to
3c692b8
Compare
3c692b8 to
f1d53d0
Compare
|
@torchello @Jean85 @stof @everzet @ciaranmcnulty I also put together a website with the new concept |
|
Questions:
|
|
This looks like a totally different test framework. Maybe it should be released as a different package instead of a new major version of |
| operating-system: ubuntu-latest | ||
| composer-flags: --prefer-lowest | ||
|
|
||
| - name: Run static analysis (psalm) |
There was a problem hiding this comment.
Why removing static analysis from the CI setup ?
There was a problem hiding this comment.
I had stan locally, didn't realise we had psalm until merging. Restored 7e23af2
.github/workflows/build.yml
Outdated
| asset_path: phpspec.phar | ||
| asset_name: phpspec.phar | ||
| asset_content_type: application/zip | ||
| - uses: actions/download-artifact@v4 |
There was a problem hiding this comment.
this should not downgrade the action
composer.json
Outdated
| "symfony/console": "^7.0 || ^8.0", | ||
| "symfony/yaml": "^7.0 || ^8.0", | ||
| "ext-dom": "*", | ||
| "ext-curl": "*", |
There was a problem hiding this comment.
why making ext-curl a mandatory requirement ?
There was a problem hiding this comment.
Moving it to suggest. It is used with some optional AI features 7fecb99
phpstan.neon
Outdated
| @@ -0,0 +1,6 @@ | |||
| parameters: | |||
| level: 1 | |||
There was a problem hiding this comment.
level 1 of phpstan is basically useless. A new codebase should actually start at level 8 or higher.
There was a problem hiding this comment.
restored psalm anyway
| * Supports Background, Scenario, Scenario Outline with Examples tables, DataTables, | ||
| * Doc strings, tags, and And/But step keywords. | ||
| */ | ||
| final class GherkinParser |
There was a problem hiding this comment.
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.
| * 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 |
There was a problem hiding this comment.
is it expected that the dispatcher is all based on global state while not being internal ?
| ], | ||
| handler: function (array $args) use ($filesystem) { | ||
| $path = $args['path']; | ||
| if (!str_starts_with($path, '/')) { |
There was a problem hiding this comment.
this won't work on Windows. Absolute paths don't always start with /
| return "File not found: {$args['path']}"; | ||
| } | ||
|
|
||
| return $filesystem->read($path); |
There was a problem hiding this comment.
shouldn't this protect against directory traversal like ../../../etc/password or /etc/password which are outside the project ?
| * Static registry holding the Browser Client instance. | ||
| * Lazily creates the client from Configuration on first access. | ||
| */ | ||
| class BrowserRegistry |
There was a problem hiding this comment.
shouldn't this class be registered in its own file following PSR-4 ?
There was a problem hiding this comment.
thus, it is currently defined outside the phpspec namespace, which is even worse.
| * Generates PHP class files from a fully qualified class name. | ||
| * Creates the directory structure and writes a minimal class skeleton. | ||
| */ | ||
| final class ClassGenerator |
There was a problem hiding this comment.
Most of these classes should probably be tagged as @internal, to properly define the public API covered by semver.
| */ | ||
| private function yamlEscape(string $value): string | ||
| { | ||
| if (str_contains($value, "\n") || str_contains($value, '"') || str_contains($value, ':')) { |
There was a problem hiding this comment.
A string starting with a single quote will also need escaping, to avoid being parsed as a single-quoted string.
| "php": "^8.2 || ^8.3 || ^8.4 || ^8.5", | ||
| "symfony/console": "^7.0 || ^8.0", | ||
| "symfony/yaml": "^7.0 || ^8.0", | ||
| "ext-dom": "*", |
There was a problem hiding this comment.
isn't ext-dom needed only for the JUnit formatter ? If yes, it should be an optional dependency IMO.
|
|
||
| ## [9.0.0-poc] — Proof of Concept | ||
|
|
||
| ### Breaking Changes |
There was a problem hiding this comment.
another breaking change: breaks compat with all existing phpspec extensions, as it is basically a different project.
| @@ -0,0 +1,35 @@ | |||
| # Changelog — PhpSpec 9 | |||
There was a problem hiding this comment.
why using a separate changelog file ?
| <?php | ||
|
|
||
| /** | ||
| * Sanity-checks a release for consistency |
|
@MarcelloDuarte the website using gray text on a black background is almost unreadable. Please change the website styles to use good contrasting colors. |
@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? |
|
@stof thanks for your feedback. I will go over one by one.
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. |
|
Maybe using |
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
4c20551 to
9a9d91b
Compare
Move version-dependent UndefinedInterfaceMethod suppression from baseline to psalm.xml config to avoid stale baseline entries across different Symfony Console versions.
ef897a4 to
e6b613b
Compare
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.
Introduce DispatcherRegistry as a thin service locator so DSL global functions can still reach the Dispatcher, while all class-level callers receive it via constructor injection. InProcessRunner and CommandDispatcher now swap the registry instance instead of saving/restoring static state.
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
ObjectBehaviorpattern creates friction. Developers must learn a DSL that maps$this->shouldReturn()to assertions and$this->beConstructedWith()to constructors. It's powerful but opaque. Thedescribe/it/expectpattern (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:
Mocking — Built-in, no Prophecy:
Story BDD — Built-in Gherkin support:
Dependencies — Dramatically reduced:
AI-powered pair programming
PhpSpec 9 includes three AI commands —
pair,next, andrefactor— that embed the AI inside the BDD cycle rather than running alongside it. All AI features are opt-in: they require asuggestdependency (papi-ai/papi-core+ a provider package) and anai: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:
The AI accelerates the cycle; the specs guarantee correctness. Neither replaces the other.
pair— Interactive BDD REPLStart an interactive pair programming session:
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: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:
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?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:
refactor— AI-powered, behaviour-preserving refactoringThe command:
AI configuration
Add an
ai:section tophpspec.yaml:Supported providers: Google (Gemini), Anthropic (Claude), OpenAI (GPT). The provider abstraction is behind an interface (
ProviderInterface) — adding new providers is straightforward.What's included
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 installphp bin/phpspec run— all specs passphp bin/phpspec run features/— all feature scenarios passvendor/bin/php-cs-fixer fix --dry-run— code style cleanvendor/bin/phpstan analyse -l 1 src/— static analysis cleanAI 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/openaiThen experiment with the BDD cycle:
php bin/phpspec pair— enter the interactive REPL; try built-in commands (describe App/Greeter,run) without AI firstwrite a feature scenario for greeting users— verify it creates.featureand.steps.phpfilesrun features/→describe App/Greeter→ ask the AI to add methods →rununtil greenphp bin/phpspec pair --prompt "write a spec for a Calculator"— single-prompt mode, verify it generates a spec filephp bin/phpspec next— verify it scans the project and suggests a next step following the scenario-first workflowphp bin/phpspec refactor "App\Greeter"— verify it runs specs, applies a refactoring, and re-runs specs (or reports no refactoring needed)