symfony/deprecation-contracts
This package provides a single global function, trigger_deprecation(), that triggers silenced deprecation notices so a custom PHP error handler can catch and log them in dev and prod environments. It takes the package name, version, and a printf-style message.
trigger_deprecation() turns "we want to remove X" from an opinion into a measurable signal. Every call emits Since <package> <version>: <message>, which means a PM can ship a notice first, watch the log for weeks, and only greenlight the actual removal once usage data says it's safe. This is the difference between a breaking-change decision backed by telemetry and one backed by a guess.symfony/console, symfony/http-foundation, and friends. There is no plausible internal build that beats zero maintenance cost. The build decision is therefore not about the contract — it is about the surrounding pipeline (log channel, CI gate, dashboards), which is genuinely our work and genuinely worth scoping.Adopt when most of the following hold:
LOG_DEPRECATIONS_CHANNEL configured (it defaults to null), a log sink you actually read, and ideally failOnDeprecation in CI. The package is inert without this; the pipeline is the project.Since vendor/pkg 2.4.0:) and can be filtered, counted, and alerted on.'next', 'TBD', or 'upcoming' destroy the semantic value of the prefix and should be treated as a review-blocking defect.Look elsewhere or don't bother when:
Log::warning() or an inline // TODO: remove in v5 comment carries the same information with no new dependency.@trigger_error suppression plus a null log channel) is the package's defining gotcha.@deprecated docblocks with IDE strikethrough, or CI rules — not a runtime notice that production may never surface.trigger_deprecation()" trick reliably causes a fatal redeclare in apps where Composer already loaded the dependency's files autoload. Set the channel to null instead — and never let anyone add a global stub to a test bootstrap or shared utility package, because it silently no-ops notices project-wide with no compiler protection.To executives — 30 seconds: "This is a nearly free mechanism for retiring old code safely. Instead of deciding to remove something and hoping nobody depended on it, we ship a version that logs a warning, watch the data for a few weeks, and then remove it with evidence. The package itself is a ten-line open-source function we already have; the investment is a small amount of logging setup so the warnings are actually visible. Total cost is days, not a quarter, and it converts recurring 'is anyone still using this?' meetings into one dashboard."
To engineering — the honest framing:
composer show symfony/deprecation-contracts first — you almost certainly already have it. Add it explicitly only if your own code will call it, so a transitive change can't remove a function you depend on.deprecations log channel, LOG_DEPRECATIONS_CHANNEL set (default is null, which drops everything silently), deprecation tracing off in production, and failOnDeprecation in CI so new noise can't merge.App\Support\Deprecations::since() so a version bump is one edit; (2) call it on the first line of the legacy path, so it fires even if the method subsequently throws; (3) guard hot paths and per-row accessors with once() — the cost is log volume, not CPU.ArgumentCountError or ValueError and crashes the request, so test that the notice fires.What success looks like, stated as a metric: for each deprecated surface, a first-occurrence alert (still in use → keep it) and a last-occurrence alert (no longer in use → delete it, on schedule, with a documented rationale). That is the deliverable. The package is just the messenger.
How can I help you explore Laravel packages today?