camelot/thrower
Utility wrappers that replace PHP’s inconsistent error/warning/notice behavior with exception-based throwing. Simplifies common tasks by ensuring functions fail predictably via exceptions (excluding deprecation warnings).
Installation:
composer require camelot/thrower
No additional configuration is needed—autoloading handles the rest.
First Use Case:
Replace a function call that emits PHP errors with its Thrower counterpart. For example, wrap file_get_contents to handle errors as exceptions:
use Camelot\Throwable\Thrower;
try {
$content = Thrower::file_get_contents('path/to/file.txt');
} catch (\RuntimeException $e) {
// Handle the exception (e.g., log, return HTTP response)
return response()->json(['error' => $e->getMessage()], 500);
}
Where to Look First:
routes/, app/Http/Controllers/, and app/Providers/ where errors might slip through.Function Wrapping:
Replace core PHP functions with Thrower equivalents in:
file_get_contents(), file_put_contents(), include_once(), require_once().json_decode(), unserialize().Example:
// Before (may emit errors)
$data = json_decode($jsonString);
// After (throws exception on error)
$data = Thrower::json_decode($jsonString);
Error Handling in Controllers:
Use Thrower wrappers in controllers to ensure consistent exception-based responses:
public function uploadFile(Request $request) {
try {
$content = Thrower::file_put_contents(
storage_path('app/uploads/' . $request->file('file')->getClientOriginalName()),
$request->file('file')->get()
);
return response()->json(['success' => true]);
} catch (\RuntimeException $e) {
return response()->json(['error' => 'Upload failed'], 500);
}
}
Global Error Standardization:
Enable Thrower globally in bootstrap/app.php to wrap all errors by default:
Thrower::enable(); // Converts all PHP errors to exceptions
Pair this with Laravel’s App\Exceptions\Handler to render consistent responses.
Custom Wrappers:
Extend Thrower for domain-specific needs. For example, wrap a third-party library:
class LegacyLibraryWrapper {
public static function safeCall($method, ...$args) {
return Thrower::wrapError(function() use ($method, $args) {
return call_user_func_array($method, $args);
});
}
}
CLI and Artisan Commands:
Use Thrower in CLI scripts to ensure exceptions are caught and logged:
use Camelot\Throwable\Thrower;
try {
$result = Thrower::exec('ls /nonexistent');
} catch (\RuntimeException $e) {
Log::error('Command failed: ' . $e->getMessage());
exit(1);
}
Laravel Exception Handler:
Ensure App\Exceptions\Handler catches Throwable\ErrorException to log or render errors consistently:
public function render($request, Throwable $exception) {
if ($exception instanceof \Throwable\ErrorException) {
// Custom logic for wrapped errors
}
return parent::render($request, $exception);
}
Testing:
Mock Thrower wrappers in PHPUnit to test error scenarios:
public function testFileReadFails() {
$this->expectException(\RuntimeException::class);
Thrower::file_get_contents('nonexistent.txt');
}
Performance: Benchmark wrapped functions in high-traffic endpoints (e.g., APIs) to ensure minimal overhead.
Double Error Handling:
App\Exceptions\Handler) and Thrower both process errors, exceptions may be thrown twice.Thrower to skip certain errors:
Thrower::ignore(E_DEPRECATED); // Skip deprecation warnings
Lost Error Context:
Throwable::wrapError() with a callback to preserve context:
$result = Thrower::wrapError(function() {
return file_get_contents('file.txt');
}, function($error) {
return new \RuntimeException("Failed to read file.txt: {$error->getMessage()}", 0, $error);
});
PHP Version Incompatibility:
Throwable). Test thoroughly on PHP 8.x to avoid issues with deprecated functions.Static Class Limitations:
Thrower is a static class, making it hard to mock in tests. Consider dependency injection for critical paths:
// Instead of:
$content = Thrower::file_get_contents('file.txt');
// Use a service:
$fileService = new FileService();
$content = $fileService->read('file.txt');
Missing Wrappers:
Throwable::wrapError():
$result = Thrower::wrapError(function() {
return some_unsupported_function();
});
Enable Verbose Logging:
Configure Thrower to log wrapped errors for debugging:
Thrower::setLogger(function($error) {
Log::debug('Wrapped error: ' . $error->getMessage(), ['file' => $error->getFile(), 'line' => $error->getLine()]);
});
Check Error Ignore List: If errors aren’t being caught, verify the ignore list:
Thrower::ignore(E_NOTICE); // Ensure this isn’t suppressing critical errors
Test Edge Cases:
E_USER_ERROR, E_USER_WARNING, and E_USER_NOTICE to ensure consistent behavior.Global Enable/Disable:
Use Thrower::enable()/Thrower::disable() to toggle error wrapping globally. Disable in production if using custom error handlers:
if (app()->environment('production')) {
Thrower::disable();
}
Custom Exception Classes: Override the default exception class for wrapped errors:
Thrower::setExceptionClass(\App\Exceptions\CustomErrorException::class);
Error Code Mapping: Map PHP error codes to custom exceptions:
Thrower::mapError(E_USER_WARNING, \App\Exceptions\WarningException::class);
Add Custom Wrappers:
Extend Thrower by adding new static methods. For example, wrap fopen():
public static function fopen($path, $mode) {
return self::wrapError(function() use ($path, $mode) {
return fopen($path, $mode);
});
}
Integrate with Laravel Events: Listen for wrapped exceptions in Laravel’s event system:
Event::listen(\Throwable::class, function($exception) {
if ($exception instanceof \Throwable\ErrorException) {
// Custom logic for wrapped errors
}
});
Custom Error Transformers:
Use Throwable::wrapError() with a transformer callback to enrich exceptions:
$result = Thrower::wrapError(function() {
return some_risky_operation();
}, function($error) {
return new \RuntimeException(
"Operation failed: {$error->getMessage()}",
$error->getCode(),
$error
);
});
Environment-Specific Behavior: Conditionally enable/disable wrappers based on the environment:
if (app()->environment(['local', 'staging'])) {
Thrower::enable();
}
How can I help you explore Laravel packages today?