set_error_handler($callback, $levels) routes non-fatal errors to your function with the level, message, file and line. Return true for handled, or false to let PHP's own handler run too. Handlers stack, and restore_error_handler() pops back to the previous one.
<?php
$seen = [];
set_error_handler(function (int $no, string $msg, string $file, int $line) use (&$seen): bool {
$seen[] = "[level $no, line $line] $msg";
return true; // handled: PHP prints nothing
}, E_WARNING | E_USER_NOTICE); // the levels this handler receives
$rows = file('prices.csv');
trigger_error('price list is empty', E_USER_NOTICE);
echo implode("\n", $seen), "\n";Output
[level 2, line 7] file(prices.csv): Failed to open stream: No such file or directory [level 1024, line 8] price list is empty
E_ERROR, E_PARSE and the other engine fatals never reach it (Uncaught Exceptions). The handler runs even when @ hides the error, so it must check the mask itself (Warnings to Exceptions). A fifth $errcontext parameter, removed in PHP 8.0, now makes an old handler fail with an ArgumentCountError.