phine/exception
phine/exception is an improved exception handler for Laravel that replaces the default error pages with clearer, more useful output, including richer stack traces and context to help you diagnose and fix failures faster during development.
Prerequisites. The library is PHP 5.3-era: namespaces, no scalar type hints, no \Throwable. It targets a pre-PHP-7 world, so treat it as legacy-compatible code, not modern code.
Install. The repo is archived, so treat packagist availability as unreliable — pin explicitly and commit your composer.lock, or vendor it:
composer require phine/exception:^1.0
# if it 404s, add a VCS repository entry pointing at the GitHub repo and require a dev-* version
If you vendor it by hand, register the autoloader manually — the original layout is PSR-0 (src/Phine/Exception/*.php), not PSR-4.
Where to look first.
README.md — expect it to be thin; do not trust it to be complete.src/Phine/Exception/Exception.php — the base class. Read the constructor signature carefully; that's the whole API surface you'll use 95% of the time.src/Phine/Exception/ContextException.php (if present) — the part that makes the package worth a look at all: an exception that carries structured data alongside the message.tests/ — with 11 stars and an archived README, the test suite is the documentation.First use case. You have scattered throw new \RuntimeException('User ' . $id . ' not found') calls and your logs are unstructured strings. Replace the worst offender with a context-carrying exception so a logger can pull machine-readable fields:
use Phine\Exception\Exception;
throw new Exception(
'User ":id" could not be loaded from ":resource".',
array(':id' => $id, ':resource' => $resource)
);
Then at your boundary:
catch (Exception $e) {
$logger->error($e->getMessage(), $e->getContext() + array('exception' => $e));
}
That's the entire value proposition: a message template plus a context bag, in one object.
Template + context at the throw site. The intended flow is: interpolate user-facing/log-facing values into a readable template, keep the raw values in an array. This decouples "what the message says" from "what the code needs to know" — the message stays human-readable, the context stays machine-queryable.
Wrap and rethrow per layer. Add context as the exception crosses architectural boundaries instead of reconstructing the message:
try {
$this->db->insert($row);
} catch (\PDOException $e) {
throw new Exception('Failed to persist ":entity".', array(':entity' => 'user'), 0, $e);
}
getPrevious() is the only debugging aid you get for free, so always thread $e through — losing the previous exception in an archived, unmaintained library is how you spend an afternoon on a stack trace that points at the wrong line.
Domain subclasses. The cheapest integration is one subclass per failure category. Keep the constructor compatible with the base class so callers don't need to know which one they're throwing:
class UserNotFoundException extends Exception {}
Catch by category at the top (catch (UserNotFoundException $e)) and let everything else fall through to your global handler for a 500.
Logging adapter. Write one function that converts any Phine\Exception\Exception into a PSR-3 context array. Every log call then goes through it, so you can't forget fields and you get consistent log structure for free.
HTTP boundary. Map exception class → status code in a single table (validation → 422, not-found → 404, everything else → 500) and dump getContext() into a details object. Never echo getMessage() verbatim to a client if the template interpolates SQL, file paths, or IDs.
Assertions in tests. Assert on $e->getContext() rather than expectExceptionMessage(). Message strings are the part most likely to get reworded; the context keys are the contract.
catch (\Exception $e) will not catch \Error/TypeError on PHP 7+, so your global handler needs a separate \Throwable branch. Test on your actual runtime before committing.($message, array $context, $code, $previous) or ($message, $code, $previous, $context). Passing an exception as the third argument when the library expects an int $code produces a silent 0 in PHP 5 and a TypeError in PHP 7+. Read the signature, don't guess.strtr() with :key placeholders, your array keys need the colon (':id', not 'id'). If it uses vsprintf(), they must not. Get this wrong and you get literal :id in production logs with no warning — missing keys are typically left untouched rather than throwing.\Exception and Phine\Exception\Exception in scope. Always alias: use Phine\Exception\Exception as PhineException;.zend.exception_ignore_args=0 (PHP 7.4+) so traces show constructor arguments — you'll need them to reconstruct context when a log line is truncated. And var_export($e->getContext(), true) at the catch site beats a breakpoint for async/CLI paths.__toString() if you need a different serialization, and supplying your own formatter if you want a different placeholder syntax. If you need more than that, the package is under-serving you.\Throwable. Adopt this package only if it's already in a legacy tree you're maintaining; otherwise copy the pattern, not the dependency.How can I help you explore Laravel packages today?