behat/gherkin
Behat's PHP Gherkin parser for reading .feature files, with support for 40+ native languages and a clean, extensible architecture. Parse features, scenarios, backgrounds, outlines, and examples into structured objects for your test suite.
What the package actually is. behat/gherkin is a pure PHP Gherkin parser — a lexer, parser, and node-tree library that turns .feature files into an AST (FeatureNode, ScenarioNode, StepNode, ExampleNode, TableNode, etc.). It does not execute tests. It has no Laravel coupling, no service provider, no config, no runtime dependencies on the framework. In a Laravel codebase it is either (a) a transitive dependency pulled in by Behat, or (b) a leaf library you call directly to build tooling: feature-file linting, spec-to-ticket sync, requirement traceability, converting Gherkin into test management exports, or generating documentation from specs.
Architecture fit. Strong if the use case is genuine. The API surface is small and stable: Lexer + Parser (both implementing ParserInterface), a keywords/dialect provider, and a filter set (TagFilter, NameFilter, LineFilter, LineRangeFilter, RoleFilter). It is a library that respects the boundary — it emits an object graph and gets out of the way. That makes it trivially wrappable behind a domain adapter in app/Support/Gherkin or a small package, which is exactly how a TPM should want a third-party parser consumed. For the majority of Laravel applications — CRUD apps, APIs, queue workers — there is no reason to install it. Frame the decision as "do we have a Gherkin-processing capability requirement," not "should we adopt this library."
Integration feasibility. High on the technical axis: composer require behat/gherkin, MIT license, supports current PHP 8.x (8.5 support landed in 4.15), internally held to PHPStan level 10 with an automated backwards-compatibility check in CI. The friction is semantic, not mechanical. Three compatibility modes now exist: LEGACY (still the default and explicitly not recommended for new projects), GHERKIN_32, and GHERKIN_42. Mode choice changes parsed output — how descriptions are trimmed, whether the @ prefix is retained on tags, whether Rule nodes survive, whether docstrings and data tables can coexist on one step, whether invalid language tags throw or silently fall back to en. Any tooling we build encodes those semantics, so the mode is an architectural decision to be made once, documented, and locked in a config value.
Technical risk. Moderate, and mostly concentrated in three places. First, the LEGACY default is a trap: teams assume "Gherkin parser" means cucumber-compatible parsing, and it doesn't by default. Second, deprecation debt is active: extending the core Lexer/Parser/Node classes is deprecated in favor of ParserInterface, FilterInterface/SimpleFilter are deprecated, and the Keywords API is slated for replacement by DialectProviderInterface. Building on the convenient-looking extension points today buys a forced refactor at the next major. Third, ecosystem signal is thin outside Behat: the dependents count reported here is effectively zero, meaning minimal independent production validation — this is a library that is trusted mostly because Behat ships it, not because a broad market has stress-tested it in isolation. The maintainer list is three named individuals, volunteer-run, with an explicit sponsorship appeal in the README. That is a normal, healthy OSS posture, but it is a bus-factor and support-SLA consideration, not a vendor relationship.
Key questions to resolve before committing.
Rule support? If yes, we are pinned to GHERKIN_32+ and cannot stay on LEGACY.Stack fit. Use it as a capability library behind an adapter, never sprinkled through controllers or commands. Concretely: bind a single GherkinParser contract in a service provider; construct Parser with an explicit ParserInterface-based composition (lexer + dialect provider + chosen compatibility mode); expose only domain-shaped methods — parseFile(): FeatureSpec, lintFile(): array<Violation>, listScenarios(TagFilter): Collection. Everything downstream (reports, ticket sync, docs) then depends on our FeatureSpec, not on Behat\Gherkin\Node\*. That adapter is maybe 100–200 lines and is the entire insurance policy against the deprecations listed above.
Migration path. Two entry scenarios. Scenario A — we already run Behat. Nothing to adopt; the package is already in composer.lock transitively. The work is governance: pin the constraint, add it to the dependency review board as a named library rather than an invisible transitive, and read its changelog on every Behat upgrade. Scenario B — we have a homegrown parser. Typical shape is regex scraping of .feature files for tags and scenario titles. Do not do a big-bang swap. Stand up the new adapter in parallel, run both against the full corpus of real feature files in CI, and diff the extracted tag/scenario/step sets. The parity diff is the migration evidence, and it also surfaces exactly where legacy Gherkin quirks bite (tag @ handling is a classic).
Compatibility. For new work, target GHERKIN_42 and document the rationale; it is declared complete and stable as of 4.18. For existing suites that would churn on tighter parsing (trimmed descriptions, stricter language tags, step-argument validation now throwing ParserException instead of NodeException), stay on LEGACY deliberately and put a dated note on the decision. Either way: do not extend Lexer, Parser, or the node classes; consume ParserInterface and compose filters rather than subclassing them.
Sequencing.
.feature files: volume, dialects used, Rule usage, tag conventions, any files with dual step arguments.LEGACY and GHERKIN_42 wired behind a config flag.Keywords → DialectProviderInterface) as tracked debt, not a someday.Maintenance. Favorable on hygiene: recent releases (4.18.0 is current, 2026-09-25 release date), dependabot managing action versions, PHPStan at level 10, a cucumber/gherkin parity test suite kept in sync with upstream translations and testdata, and an explicit backwards-compatibility CI job. That is a well-run project. The counterweight: it is volunteer-maintained by three people with sponsorship-based funding and no commercial support tier. For internal tooling and CI gates this is acceptable. For anything on a customer-facing contractual path, assume we are our own first line of support and budget accordingly.
Support. GitHub issues and PRs only. No SLA, no vendor to escalate to. The right posture is to treat it as vendored-by-decision: our adapter is the support boundary, so an upstream stall degrades to "we cannot upgrade" rather than "our pipeline is down." The small API surface makes a fork or local patch genuinely viable if the project ever goes quiet — that optionality is worth naming in the risk register.
Scaling. Parsing is CPU-bound and in-process; cost scales with feature-file count and file size, not with application traffic. Realistic volumes (hundreds to low thousands of feature files) are a seconds-scale problem. Avoid re-parsing per request — parse at test bootstrap, at build time, or in a queue job if we are bulk-processing for reporting. Use the built-in file/memory caching for repeated access, but pin the cache version and keep it out of shared multi-tenant cache namespaces; note that cache-directory race conditions were addressed in 4.15, so anything older is a known hazard. This is not a component that will show up on a capacity plan.
Failure modes to design for.
ParserException on structurally invalid files — files ending in tags, Background after the first Scenario, a step carrying both a docstring and a data table (pre-GHERKIN_42). These must be caught at the adapter and surfaced as actionable lint output, not stack traces.LEGACY, a bogus #language: tag falls back to en with only the resolved language reported. Tooling that trusts getLanguage() will mis-attribute content. GHERKIN_32+ throws instead — a good reason to prefer it.Rule nodes stripped for callers not updated for them, with rule backgrounds inlined into each scenario. Traceability tooling will silently lose a structural layer; verify explicitly.Ramp-up. Low technically, higher semantically. A mid-level Laravel developer is productive with the parser API in under a day — the README example is essentially the whole surface. The genuine learning curve is in the node tree's shape, the legacy-vs-GHERKIN_42 behavioral differences, and the filter composition rules. Budget half a day of reading release notes for whoever owns the compatibility-mode decision, and write a short internal note capturing it — otherwise every future engineer will re-litigate "why are tags missing their @?" from scratch. Overall: a small, well-behaved dependency with an outsized ability to cause quiet, hard-to-diagnose semantic drift if the compatibility mode is left implicit.
How can I help you explore Laravel packages today?