Skip to content

Support PHP 8 Attributes as step definition annotations (#[Given], #[When], #[Then]) #289

Description

@JerrySLau

🤔 What's the problem you're trying to solve?

Behat 3.x introduced first-class support for PHP 8 Attributes as an alternative to docblock annotations for declaring step definitions and hooks (see Behat docs):

use Behat\Step\Given;
use Behat\Step\When;
use Behat\Step\Then;

final class TestRepositoryContext implements Context
{
    #[Given('a test repository instance exists')]
    public function assertRepositoryInjected(): void { ... }

    #[When('I request the entity class name managed by the repository')]
    public function requestEntityClassName(): void { ... }

    #[Then('I should get the fully qualified class name of TestEntity')]
    public function assertEntityClassNameIsTestEntity(): void { ... }
}

The cucumber/language-service currently only recognises docblock annotations (@Given, @When, @Then) as step definition markers when scanning PHP glue files. PHP 8 Attribute-based definitions are completely invisible to the language service, which produces two problems:

  1. All Gherkin steps are underlined as "undefined" in the editor, even though they are correctly wired at runtime.
  2. "Go to step definition" and autocomplete do not work — the language service cannot resolve the link between a .feature step and its implementing method.

This makes Attribute-based Behat projects essentially broken from an IDE-assistance perspective, despite being fully functional at test execution time.

✨ What's your proposed solution?

Extend the PHP step definition parser in cucumber/language-service to recognise PHP 8 Attributes from the following namespaces alongside the existing docblock strategy:

Attribute class Equivalent docblock
Behat\Step\Given @Given
Behat\Step\When @When
Behat\Step\Then @Then
Behat\Step\Step (generic)

The pattern to match in PHP source files:

// Fully-qualified in use statement, then short form on method:
use Behat\Step\Given;
#[Given('some step text')]
public function someMethod(): void { ... }

// Or fully-qualified inline:
#[\Behat\Step\Given('some step text')]
public function someMethod(): void { ... }

The step expression (the string argument to the attribute constructor) should be extracted exactly the same way as the pattern in a docblock annotation, so all existing expression parsing (Cucumber Expressions, regex) continues to apply unchanged.

⛏ Have you considered any alternatives or workarounds?

  • Keeping docblock annotations: Reverting to /** @Given('...') */ works for the language service but goes against the direction of modern PHP (attributes are the idiomatic, statically-analysable mechanism since PHP 8.0) and produces a split-style codebase when the rest of the project uses attributes.
  • Duplicating both forms: Writing both a docblock and an attribute on every method is noisy and error-prone.
  • Custom cucumber.glue regex override: The extension does not currently expose a hook to customise how step definitions are extracted from source files.

None of these are satisfying for a project that has deliberately adopted PHP 8 Attributes as its annotation standard.

📚 Any additional context?

  • Behat version that introduced Attribute support: Behat 3.x (PHP 8 attributes). The official Behat docs now show attributes as the primary example.
  • Relevant Behat namespaces: Behat\Step\Given, Behat\Step\When, Behat\Step\Then, Behat\Step\Step; hooks use Behat\Hook\BeforeScenario, Behat\Hook\AfterScenario, etc.
  • PHP version floor: PHP 8.0+ (attributes are not available on PHP 7.x, so the parser can safely assume PHP 8 syntax when it encounters #[).
  • Existing language-service PHP parser: The fix is likely localised to the PHP step definition extractor — the attribute argument string is a plain PHP string literal in the same position as the docblock pattern string, so the expression-parsing layer should need no changes.
  • Impact: Any Behat project targeting PHP 8+ that follows the current Behat documentation will hit this issue out of the box.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions