Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 113 additions & 45 deletions src/Phaseolies/Error/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,95 @@ public static function handle(): void
self::configureErrorReporting();
self::configureShutdownReporting();

set_exception_handler(function ($exception) {
self::logException($exception);
set_exception_handler(function (Throwable $exception) {
$loggerException = self::logException($exception);
$activeException = $loggerException ?? $exception;

self::triggerBeforeException($exception);
self::triggerBeforeException($activeException);
self::dispatch($activeException);
});
}

$handler = ErrorHandlerFactory::getSupportedHandler();
/**
* Dispatch the exception to the appropriate handler.
*
* @param Throwable $exception
* @return void
*/
protected static function dispatch(Throwable $exception): void
{
$handler = ErrorHandlerFactory::getSupportedHandler();

if ($handler) {
$handler->handle($exception);
} else {
self::handleFallback($exception);
}
});
if ($handler) {
$handler->handle($exception);
} else {
self::handleFallback($exception);
}
}

/**
* Log an exception using the application's logger.
*
* @param Throwable $exception
* @return void
* @return Throwable|null
*/
protected static function logException(Throwable $exception): void
protected static function logException(Throwable $exception): ?Throwable
{
$logMessage = "Error: " . $exception->getMessage();
$logMessage .= "\nFile: " . $exception->getFile();
$logMessage .= "\nLine: " . $exception->getLine();
$logMessage .= "\nTrace: " . $exception->getTraceAsString();

app(LoggerService::class)
->channel(env('LOG_CHANNEL', 'stack'))
->error($logMessage);
try {
$logMessage = sprintf(
"Error: %s\nFile: %s\nLine: %d\nTrace: %s",
$exception->getMessage(),
$exception->getFile(),
$exception->getLine(),
$exception->getTraceAsString()
);

app(LoggerService::class)
->channel(env('LOG_CHANNEL', 'stack'))
->error($logMessage);

return null;
} catch (Throwable $e) {
return new \RuntimeException(
sprintf(
'Logger unavailable: %s | Original error: %s in %s on line %d',
$e->getMessage(),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
),
0,
$e
);
}
}

/**
* Last resort fallback handler when no specific handler is available.
*
* @param Throwable $exception
* @return void
*/
protected static function handleFallback(Throwable $exception): void
{
abort($exception->getCode() ?: 500, "An error occurred. Please try again later.");
$code = $exception->getCode();
$httpCode = ($code >= 400 && $code < 600) ? $code : 500;

exit(1);
abort($httpCode, "An error occurred. Please try again later.");
}

/**
* Default fallback handler if no specific handler supports the context.
* Configure PHP error handler to convert runtime errors to exceptions.
*
* @param Throwable $exception
* @return void
*/
protected static function configureErrorReporting(): void
{
set_error_handler(function ($severity, $message, $file, $line) {
if (strpos($message, 'fsockopen():') === 0) {
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
return false;
}
if (str_starts_with($message, 'fsockopen():')) {
return false;
}

Expand All @@ -81,49 +120,78 @@ protected static function configureErrorReporting(): void
}

/**
* Configure handling of PHP runtime warnings and minor errors.
* Configure shutdown handler for fatal errors that bypass set_error_handler
*
* @return void
*/
protected static function configureShutdownReporting(): void
{
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
if (strpos($error['message'], 'fsockopen():') === 0) {
return;
}

if (ob_get_level() > 0) {
ob_end_clean();
}
if (!$error || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
return;
}

if (str_starts_with($error['message'], 'fsockopen():')) {
return;
}

throw new \ErrorException("A fatal error occurred: " . $error['message']);
if (ob_get_level() > 0) {
ob_end_clean();
}

$exception = new \ErrorException(
"A fatal error occurred: " . $error['message'],
0,
$error['type'],
$error['file'],
$error['line']
);

$loggerException = static::logException($exception);
$activeException = $loggerException ?? $exception;

static::triggerBeforeException($activeException);
static::dispatch($activeException);
});
}

/**
* Executes the hook before the main error handling flow begins
* Executes the user-defined hook before the main error handling flow
*
* @param Throwable $exception
* @return void
*/
protected static function triggerBeforeException(Throwable $exception): void
{
$beforeExceptionClass = \App\Http\Exceptions\BeforeExceptionHandler::class;
try {
$beforeExceptionClass = \App\Http\Exceptions\BeforeExceptionHandler::class;

if (!class_exists($beforeExceptionClass)) {
return;
}

if (class_exists($beforeExceptionClass)) {
$before = app($beforeExceptionClass);

if ($before->supports()) {
$response = app(Response::class);
$statusCode = $exception instanceof \Phaseolies\Http\Exceptions\HttpException
? $exception->getStatusCode()
: 500;
$response->setExceptionError($exception, $statusCode);
$before->handle($exception);
if (!$before->supports()) {
return;
}

$response = app(Response::class);
$statusCode = $exception instanceof \Phaseolies\Http\Exceptions\HttpException
? $exception->getStatusCode()
: 500;

$response->setExceptionError($exception, $statusCode);
$before->handle($exception);
} catch (Throwable $e) {
error_log(sprintf(
'[Doppar] BeforeExceptionHandler failed: %s in %s on line %d',
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
}
}
}
9 changes: 6 additions & 3 deletions src/Phaseolies/Http/Controllers/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,13 @@ protected function compileAndCache(string $actual, string $cache): void
// Make sure the file has proper permissions
@chmod($tempFile, 0644);

// Atomic rename (on Unix systems)
if (!rename($tempFile, $cache)) {
// Atomic rename (on Unix/Mac), fallback copy for Windows
if (!@rename($tempFile, $cache)) {
if (!@copy($tempFile, $cache)) {
@unlink($tempFile);
throw new RuntimeException("Failed to move compiled view to cache: {$cache}");
}
@unlink($tempFile);
throw new RuntimeException("Failed to move compiled view to cache: {$cache}");
}

// Verify the cache file was created successfully
Expand Down
19 changes: 15 additions & 4 deletions src/Phaseolies/Logger/Drivers/DailyLogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,27 @@ class DailyLogHandler extends AbstractHandler implements LogHandlerInterface
*/
public function configureHandler(Logger $logger, string $channel): void
{
$path = base_path('storage/logs');
$path = base_path('storage' . DIRECTORY_SEPARATOR . 'logs');

if (!is_dir($path)) {
mkdir($path, 0775, true);
if (!mkdir($path, 0775, true) && !is_dir($path)) {
throw new \RuntimeException('Unable to create log directory: ' . $path);
}
}

$logFile = $path . '/' . date('Y_m_d') . '_doppar.log';
if (!is_writable($path)) {
throw new \RuntimeException('Log directory is not writable: ' . $path);
}

$logFile = $path . DIRECTORY_SEPARATOR . date('Y_m_d') . '_doppar.log';

if (!is_file($logFile)) {
touch($logFile);
if (touch($logFile) === false) {
throw new \RuntimeException('Unable to create log file: ' . $logFile);
}

// Ignored on Windows but correct for Mac/Ubuntu
chmod($logFile, 0664);
}

$this->handleConfiguration($logger, $logFile);
Expand Down
18 changes: 14 additions & 4 deletions src/Phaseolies/Logger/Drivers/DefaultLogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@ class DefaultLogHandler extends AbstractHandler implements LogHandlerInterface
*/
public function configureHandler(Logger $logger, string $channel): void
{
$path = base_path('storage/logs');
$path = base_path('storage' . DIRECTORY_SEPARATOR . 'logs');

if (!is_dir($path)) {
mkdir($path, 0775, true);
if (!mkdir($path, 0775, true) && !is_dir($path)) {
throw new \RuntimeException('Unable to create log directory: ' . $path);
}
}

$logFile = $path . '/doppar.log';
if (!is_writable($path)) {
throw new \RuntimeException('Log directory is not writable: ' . $path);
}

$logFile = $path . DIRECTORY_SEPARATOR . 'doppar.log';

if (!is_file($logFile)) {
touch($logFile);
if (touch($logFile) === false) {
throw new \RuntimeException('Unable to create log file: ' . $logFile);
}

chmod($logFile, 0664);
}

$this->handleConfiguration($logger, $logFile);
Expand Down
18 changes: 14 additions & 4 deletions src/Phaseolies/Logger/Drivers/SingleLogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@ class SingleLogHandler extends AbstractHandler implements LogHandlerInterface
*/
public function configureHandler(Logger $logger, string $channel): void
{
$path = base_path('storage/logs');
$path = base_path('storage' . DIRECTORY_SEPARATOR . 'logs');

if (!is_dir($path)) {
mkdir($path, 0775, true);
if (!mkdir($path, 0775, true) && !is_dir($path)) {
throw new \RuntimeException('Unable to create log directory: ' . $path);
}
}

$logFile = $path . '/doppar.log';
if (!is_writable($path)) {
throw new \RuntimeException('Log directory is not writable: ' . $path);
}

$logFile = $path . DIRECTORY_SEPARATOR . 'doppar.log';

if (!is_file($logFile)) {
touch($logFile);
if (touch($logFile) === false) {
throw new \RuntimeException('Unable to create log file: ' . $logFile);
}

chmod($logFile, 0664);
}

$this->handleConfiguration($logger, $logFile);
Expand Down
Loading
Loading