skip to content

In Laravel 13, how do you log PHP and framework deprecation warnings, and why do none appear in a freshly installed app?

level: middleimportance: nice to knowfreq 20%

answer

  1. LOG_DEPRECATIONS_CHANNEL=null by default
  2. or define a channel named deprecations
  3. written at warning level
  4. LOG_DEPRECATIONS_TRACE adds a stack trace
  5. skipped in tests without LOG_DEPRECATIONS_WHILE_TESTING

basics

~20 s

Laravel sends E_DEPRECATED and E_USER_DEPRECATED notices to its deprecations channel, which the skeleton points at the null channel. Set LOG_DEPRECATIONS_CHANNEL to a real channel, or define a channel named deprecations, and they are logged as warnings.

solid answer

~40 s

Laravel's error handler treats PHP `E_DEPRECATED` and `E_USER_DEPRECATED` notices specially: instead of throwing an `ErrorException`, it logs them to a `deprecations` channel at the **warning** level, as '<message> in <file> on line <n>'. The skeleton's `config/logging.php` sets `'deprecations' => ['channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false)]`, and `.env.example` sets `LOG_DEPRECATIONS_CHANNEL=null`, so by default they go to the `null` channel and vanish. Point the variable at a channel such as `daily`, or define a channel literally named `deprecations`, which always wins. `LOG_DEPRECATIONS_TRACE=true` attaches an exception so the stack trace is logged. During tests deprecations are ignored unless `LOG_DEPRECATIONS_WHILE_TESTING` is set. The target channel's level must be `warning` or lower, or every entry is dropped.

code

php · 13 lines
php
<?php

// config/logging.php (excerpt): a dedicated file that always wins
return [
    'channels' => [
        'deprecations' => [
            'driver' => 'daily',
            'path' => storage_path('logs/deprecations.log'),
            'level' => 'warning',
            'max_files' => 7,
        ],
    ],
];

go deeper

for a junior

Know that deprecation warnings are discarded by default and LOG_DEPRECATIONS_CHANNEL turns them on.

for a middle

Explain the warning level, the deprecations channel taking priority, the trace option and the testing switch.

for a senior

Use deprecation logs to plan PHP and framework upgrades without flooding production logs or alert channels.

for a principal

Build deprecation tracking into the upgrade cadence so warnings are cleared before they become breaking changes.

## What counts as a deprecation A **deprecation** is a notice that a feature still works but will be removed or changed: PHP raises `E_DEPRECATED` for language features on their way out, and libraries, including Laravel and its dependencies, raise `E_USER_DEPRECATED` through `trigger_error()`. They are the early warning for the next PHP or framework upgrade. ## How Laravel handles them During bootstrap Laravel installs its own PHP error handler. For most PHP errors it throws an `ErrorException`, so a warning in your code becomes a real exception. Deprecations are the exception to that rule: 1. The handler sees `E_DEPRECATED` or `E_USER_DEPRECATED`. 2. It does not throw. It resolves the log manager and makes sure a `deprecations` channel exists. 3. It writes a **warning**-level entry to that channel: `<message> in <file> on line <n>`, or, with tracing on, the message plus an `ErrorException` so the formatter prints a stack trace. 4. Any failure while logging is swallowed, so a deprecation can never break a request. ## Why Laravel logs rather than throws Turning deprecations into exceptions would make every upgrade of PHP or of a dependency a potential outage: a library raising a new `E_USER_DEPRECATED` notice from inside its own code would crash requests that did nothing wrong. Logging keeps behaviour unchanged while still recording the signal. The trade-off is that the signal is easy to ignore, which is why deprecations need a deliberate destination and a periodic review rather than being left on the `null` channel forever. ## Why nothing appears by default The skeleton's `config/logging.php` contains: ```php 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false), ], ``` and `.env.example` sets `LOG_DEPRECATIONS_CHANNEL=null`. The `null` channel uses Monolog's `NullHandler`, which discards everything. Deprecations are processed and then thrown away, which keeps production logs quiet. ## Turning them on There are two ways, and one takes priority: - **A channel literally named `deprecations`** under `channels`. If it exists, Laravel always uses it, whatever `logging.deprecations` says. It is the tidy option when deprecations need their own file. - **`LOG_DEPRECATIONS_CHANNEL=daily`** (or any configured channel). Laravel copies that channel's config into a `deprecations` channel at runtime, so entries go wherever the named channel writes. Options worth knowing: | Setting | Effect | |---|---| | `LOG_DEPRECATIONS_TRACE=true` | Attaches an `ErrorException`, so entries carry a stack trace showing the caller | | `LOG_DEPRECATIONS_WHILE_TESTING` set | Logs deprecations during the test suite, which are otherwise skipped | | Target channel `level` | Must be `warning` or lower; a `critical`-only channel drops every deprecation | ## Using them before an upgrade A practical routine before moving to a new PHP or Laravel release: - send deprecations to their own file in staging, with tracing on, so each entry names the calling code; - run the test suite with `LOG_DEPRECATIONS_WHILE_TESTING` set, since tests exercise paths production rarely hits; - group the entries by message and fix your own code first; notices raised inside vendor packages usually mean upgrading that package; - turn tracing off again afterwards, because traces make each entry large. ## What an entry looks like With `LOG_DEPRECATIONS_CHANNEL=daily` and tracing off, a PHP 8.4+ deprecation raised when a class is compiled appears as a single warning line: ``` [2026-09-29 09:02:11] staging.WARNING: App\Services\FareQuote::price(): Implicitly marking parameter $promo as nullable is deprecated, the explicit nullable type must be used instead in /var/www/app/Services/FareQuote.php on line 42 ``` The message comes from PHP; Laravel appends the file and line. Many deprecations repeat each time their code path runs, so a busy app logs the same message many times: group by message when reviewing, rather than reading line by line. With `LOG_DEPRECATIONS_TRACE=true` the line keeps only the message, and the formatter prints the attached exception's stack trace underneath, which is what you need when the deprecated call sits inside a vendor package and you must find which of your calls reached it. ## Pitfalls - Pointing `LOG_DEPRECATIONS_CHANNEL` at an alert channel such as Slack at `critical`: warnings never pass its level, so nothing arrives, and at a lower level the chat floods. - Pointing it at the default stack, which mixes thousands of repeated notices into the main application log. - Expecting deprecations to fail tests; Laravel never turns them into exceptions.

  • Why do deprecations not show up while the test suite runs?
    Laravel skips deprecation logging when the app is running unit tests, unless the `LOG_DEPRECATIONS_WHILE_TESTING` environment variable is set. Set it in the test environment to collect them during a run.
  • Does Laravel turn a deprecation into an exception like other PHP warnings?
    No. Other PHP errors reported by `error_reporting()` become `ErrorException`s, but `E_DEPRECATED` and `E_USER_DEPRECATED` are only logged at warning level to the deprecations channel, and any failure while logging them is ignored.

saying these in an interview costs you the question

  • Laravel turns deprecation notices into ErrorException like other warnings
  • Deprecations are logged at the error level
  • LOG_DEPRECATIONS_CHANNEL overrides a channel named deprecations
  • Deprecations are logged during tests by default
  • Pointing deprecations at a critical-only Slack channel posts them all