caciobanu/behat-deprecation-extension
A Behat extension that captures PHP deprecation notices raised during your scenarios and surfaces them in the test output, making it easy to track down and fix deprecated code before upgrading dependencies or PHP versions. Keeps your test suite clean and your upgrades predictable.
What you're adding. A Behat extension that hooks Behat's lifecycle, registers a PHP error handler for E_DEPRECATED / E_USER_DEPRECATED, aggregates notices emitted while scenarios execute, and prints them next to your scenario results. On a Laravel codebase this is most useful during framework/PHP upgrade treadmills: your Behat suite becomes a deprecation detector for the code paths your feature files actually exercise.
Pin the version you actually reviewed. v2.1.1 is a maintenance-only release: a Symfony 2.8 compatibility fix plus a Travis CI matrix update for "supported PHP versions." No new config, no new handler behaviour, no breaking changes. But it tells you something important about the codebase — compatibility work aimed at Symfony 2.8 dates the extension's lineage to the Symfony 2.x/3.x era, so its error-handler and formatter integration predate almost everything modern in the Symfony/Laravel stack.
composer require --dev caciobanu/behat-deprecation-extension:^2.1.1
Then confirm what actually landed and what the entry class is:
ls vendor/caciobanu/behat-deprecation-extension/src
composer show caciobanu/behat-deprecation-extension # check the requires: behat/*, symfony/*, php
Enable it in behat.yml (verify the exact FQCN against the source you just listed — extension namespaces are not guaranteed across a 2.x line):
default:
extensions:
Caciobanu\Behat\DeprecationExtension\Extension: ~
suites:
default:
contexts: [FeatureContext]
First sanity check before writing any scenarios. Behat can tell you whether the extension loaded and what config it accepts:
vendor/bin/behat --config=behat.yml -dl # list suites/definitions; extension config errors surface here
vendor/bin/behat --config=behat.yml -vvv # verbose, shows extension bootstrapping
If the extension registers a config node, an intentionally wrong key (e.g. failOnDeprecation: maybe) will produce a schema error listing the valid keys — the fastest way to discover the real option names on an undocumented build.
Your first use case. Pick one scenario that touches a known-stale API — a facade call to something deprecated in your Laravel version, Carbon::now()->subDays() style date math on an old Carbon, or a scenario that resolves a container binding whose class now has an implicit-nullable signature under PHP 8.4. Run just that feature, confirm the deprecation shows up in the output, then widen to the full suite.
Where to look first in the repo. src/Extension.php (config tree + listener wiring) and whatever ErrorHandler/DeprecationCollector class it installs. Those two files tell you: which error levels are captured, whether user deprecations are included, and how the report is emitted (formatter vs. an AfterSuite print). Everything else in this assessment is downstream of those two answers. v2.1.1 does not change any of them.
Run-with-warning locally, gate in CI. Don't turn failures on during week one. Two Behat profiles give you an adoption path:
default:
extensions:
Caciobanu\Behat\DeprecationExtension\Extension: ~ # report only
ci:
extensions:
Caciobanu\Behat\DeprecationExtension\Extension:
failOnDeprecation: true # name per your Configuration class
vendor/bin/behat # developer feedback loop
vendor/bin/behat -p ci --strict # CI gate on new deprecations
If the extension only reports and doesn't fail, behat --strict plus a grep over the output is a workable substitute:
vendor/bin/behat -p ci --no-colors | tee behat.log
! grep -qi 'deprecat' behat.log
Baseline before you ratchet. Run the whole suite once, pipe to a file, dedupe by message, and commit that list as a documented baseline in docs/deprecations.md or as an exclusion regex. This is the single highest-value step — a first run on a mature Laravel app routinely emits dozens of distinct notices, almost all from vendor code you cannot fix. Separate them:
vendor/bin/behat --no-colors | grep -i deprecat | sort | uniq -c | sort -rn
Then exclude vendor/-originated noise and focus on app/ and tests/.
Pair with a Laravel-aware Behat bootstrap. For this extension to see anything meaningful, your Behat suite has to actually boot Laravel and hit real application code. Use laracasts/behat-laravel-extension (or a manual FeatureContext that boots bootstrap/app.php and calls $kernel->handle()), so that Eloquent, facades, container resolution and service providers all execute inside the window where the error handler is installed. A Behat suite that only exercises isolated value objects will produce a clean, useless report.
Tag-scoped runs for upgrade readiness. Tag scenarios covering code you're about to touch, then run them under the next PHP/Laravel version in a throwaway branch:
vendor/bin/behat --tags=@billing
This is the practical version of "which parts of my app break if I bump Laravel" — a per-domain deprecation sweep, cheaper than a full static-analysis setup.
Treat the count, not just the message, as the metric. New deprecations are the signal; a fixed deprecation should reduce a count to zero. Store the deduped output as a CI artefact per build and diff it against main. A rising count on an untouched area means a dependency bump, not your code — usually the more urgent finding.
Complementary tools, not substitutes. Behat only covers executed paths. Pair this with Rector's deprecation rule sets and PHPStan's deprecation extension for the static side, and composer outdated --direct for the "the API is gone, not just deprecated" side. Use this extension for runtime-only deprecations (magic methods, dynamic properties, framework internals invoked by reflection).
Custom hook as a fallback reporter. If the built-in output is too coarse, install your own handler in a @BeforeSuite hook before the extension's, or read the collected notices in @AfterScenario and emit them through your own formatter. Keep it additive — don't replace the extension's handler, or you'll silently lose the aggregation.
error_reporting must include E_DEPRECATED. Behat will not override your php.ini for you. Check and force it:
php -i | grep error_reporting
vendor/bin/behat -d error_reporting=E_ALL # Behat 3.x passes -d through to PHP
In phpunit.xml-style environments people routinely set error_reporting to E_ALL & ~E_DEPRECATED; if the same php.ini serves both, deprecations vanish silently and the extension looks broken.
Laravel's own error handler competes for the slot. Illuminate\Foundation\Bootstrap\HandleExceptions::bootstrap() calls set_error_handler() and routes errors through the log. Whichever handler is registered last wins. If you boot Laravel inside FeatureContext after the extension initialises, Laravel may take over and you'll see nothing. v2.1.1 does not address this — the release only touches a Symfony 2.8 compat path and CI config. Verify with a deliberate probe:
@trigger_error('probe-deprecation', E_USER_DEPRECATED); // in a step, then check output
If the probe doesn't appear, registration order is your problem — boot the kernel in @BeforeSuite rather than lazily inside the first step, or re-register the extension's handler after bootstrap.
@-suppressed deprecations cannot be captured. Under PHP 8, the @ operator sets error_reporting() to a minimal mask for the duration of the call; if the extension's handler honours error_reporting() (most do, as a guard), suppressed notices are dropped. Laravel and Symfony suppress aggressively in places. This means "clean report" ≠ "no deprecated calls" — it means "no unsuppressed ones."
User deprecations are a separate level. Symfony's trigger_deprecation() emits E_USER_DEPRECATED, not E_DEPRECATED. If the extension only registers E_DEPRECATED, every Symfony/Laravel-internal deprecation is invisible. Verify against src/ before you trust a zero count.
Deprecations before the suite starts are invisible. Composer autoload, PHP version notices, and anything triggered during Behat's own bootstrap happen before the extension's handler is armed. Run a separate smoke check (php -d error_reporting=E_ALL artisan about, composer dump-autoload -o) to cover that window.
PHP 8.4's implicit-nullable deprecation will flood the report. The v2.1.1 changelog contains nothing about PHP 8, and a maintainer whose last compatibility work was Symfony 2.8 has no awareness of it. Expect thousands of Implicitly marking parameter $x as nullable is deprecated lines from vendor code on 8.4. Scope the report to app/ before drawing any conclusion, or you'll dismiss the whole tool as noise.
Compatibility is still the primary risk — and v2.1.1 doesn't reduce it. A Symfony 2.8 fix is a backward-compatibility patch, not a forward one: it says the maintainer is keeping old Symfony installs working, not chasing Symfony 6/7 or Behat 4. The Travis CI update is likewise about the old supported-PHP matrix, which means there is no evidence PHP 8.x was ever exercised in CI. Treat any 8.x run as untested. Check the constraint before adopting:
composer why caciobanu/behat-deprecation-extension
composer require --dev --with-all-dependencies caciobanu/behat-deprecation-extension:^2.1.1
If it collides with your Behat version (likely on Behat 4), the fallback is a ~30-line FeatureContext that installs its own set_error_handler in @BeforeSuite and prints a deduped list in @AfterSuite. For most teams that's a smaller cost than fighting a stale dependency — and it's fully under your control for PHP 8.4 handling.
Don't read the patch bump as a maintenance signal. A 2.1.1 with "fix Symfony 2.8 compat" is housekeeping on an old branch. Don't infer that a 2.2/3.0 with modern Symfony support is coming; plan the migration/DIY fallback on the assumption that v2.1.1 is the last thing you get.
--dry-run produces no deprecations. Obvious in hindsight, frequently misdiagnosed. Same for scenarios that are all @BeforeScenario failures — nothing executes, so nothing is captured.
Deduplicate by message and origin. The same deprecation firing in a loop generates one line per invocation. Collapse with counts (sort | uniq -c) before reporting to humans, or the signal is unreadable. Where the extension supports an exclude/filter regex, anchor it on the file path rather than the message text — messages get rephrased between patch releases, paths don't.
Don't let it fail the build on day one. A hard failure on an existing codebase produces one of two outcomes: the extension is removed from behat.yml, or the whole suite is skipped. Warn first, baseline second, gate third — and gate only on app/-originated notices, which you can typically express as an allow/deny list once you've seen the real output.
Watch for duplicate handler chaining. If you also have symfony/phpunit-bridge or a deprecation-contracts listener active in the same process, you may get each message printed twice or attributed to the wrong scenario. Disable the competing collector in the Behat profile while measuring.
How can I help you explore Laravel packages today?