Streamline PHP Exception Handling with a Unified Elegant Approach

This article explores why excessive try‑catch blocks clutter PHP code, compares a messy controller implementation with a clean version, explains PHP exception fundamentals, and demonstrates how to centralize error handling in Webman using a custom TinywanHandler with practical configuration and examples.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Streamline PHP Exception Handling with a Unified Elegant Approach

Background

In everyday software development, handling exceptions consumes a large portion of a developer's time—often more than half of the total effort. Repeated try { … } catch { … } finally { … } blocks make the codebase look like a patchwork of ad‑hoc fixes, causing redundancy, reduced readability, and maintenance headaches.

Messy Code Example

declare(strict_types=1);
namespace app\controller;
use app\common\service\ArticleService;
use support\Request;
use support\Response;
use Tinywan\ExceptionHandler\Exception\BadRequestHttpException;
use Tinywan\ExceptionHandler\Exception\ForbiddenHttpException;
use Webman\Exception\Exception\BusinessException;

class ArticleController {
    public function index(Request $request): Response {
        try {
            $res = ArticleService::getArticleList();
        } catch (ForbiddenHttpException $e) {
            // do something
        } catch (BadRequestHttpException $e) {
            // do something
        } catch (BusinessException $e) {
            // do something
        }
        return json($res);
    }
    // add() and detail() contain similar repetitive try‑catch blocks
}

Elegant Code Example

declare(strict_types=1);
namespace app\controller;
use app\common\service\ArticleService;
use support\Request;
use support\Response;

class ArticleController {
    public function index(Request $request): Response {
        return json(ArticleService::getArticleList());
    }
    public function add(Request $request): Response {
        return json(ArticleService::getArticleList());
    }
    public function detail(Request $request): Response {
        return json(ArticleService::getArticleList());
    }
}

Even though the controller layer can be simplified, deeper layers such as services often re‑introduce numerous try‑catch blocks, turning the code into a “forest of patches”. A unified error‑handling strategy is essential to keep business logic clean while still catching and processing exceptions properly.

Exception Basics

An exception represents an unexpected situation that deviates from the normal execution flow. In PHP, exceptions are thrown with throw and caught with catch. If no matching catch exists, the exception bubbles up the call stack until a handler is found; otherwise a fatal error terminates the script.

PHP’s exception model is similar to many other languages: code that may fail is wrapped in a try block, and each try must have at least one catch or finally clause.

Unified Exception Handling in Webman

Modern PHP frameworks (Webman, Laravel, ThinkPHP, Yii) provide a central exception handler so that developers do not need to repeat error‑handling code in every controller. In Webman, the class Webman\Exception\ExceptionHandler is the default handler.

Webman Default Handler (simplified)

class ExceptionHandler implements ExceptionHandlerInterface {
    protected $logger = null;
    protected $debug = false;
    public $dontReport = [];
    public function __construct($logger, $debug) {
        $this->logger = $logger;
        $this->debug = $debug;
    }
    public function report(Throwable $exception) {
        if ($this->shouldntReport($exception)) return;
        $logs = '';
        if ($request = \request()) {
            $logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');
        }
        $this->logger->error($logs . PHP_EOL . $exception);
    }
    public function render(Request $request, Throwable $exception): Response {
        if (method_exists($exception, 'render') && ($response = $exception->render($request))) {
            return $response;
        }
        $code = $exception->getCode();
        if ($request->expectsJson()) {
            $json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error'];
            $this->debug && $json['traces'] = (string)$exception;
            return new Response(200, ['Content-Type' => 'application/json'], json_encode($json, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
        }
        $error = $this->debug ? nl2br((string)$exception) : 'Server internal error';
        return new Response(500, [], $error);
    }
    // ... other helper methods omitted for brevity
}

Developers can extend this class to implement custom logic, such as adding request metadata, formatting response bodies, or triggering notifications.

Custom Elegant Handler – TinywanHandler

The following class extends Webman’s ExceptionHandler and adds features like configurable status codes, error messages, response data enrichment, debug information, and event/trace integration.

declare(strict_types=1);
namespace Tinywan\ExceptionHandler;
use FastRoute\BadRouteException;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use Throwable;
use Tinywan\ExceptionHandler\Event\DingTalkRobotEvent;
use Tinywan\ExceptionHandler\Exception\BaseException;
use Tinywan\ExceptionHandler\Exception\ServerErrorHttpException;
use Tinywan\Jwt\Exception\JwtRefreshTokenExpiredException;
use Tinywan\Jwt\Exception\JwtTokenException;
use Tinywan\Jwt\Exception\JwtTokenExpiredException;
use Tinywan\Validate\Exception\ValidateException;
use Webman\Exception\ExceptionHandler;
use Webman\Http\Request;
use Webman\Http\Response;

class TinywanHandler extends ExceptionHandler {
    public $dontReport = [];
    public int $statusCode = 200;
    public array $header = [];
    public int $errorCode = 0;
    public string $errorMessage = 'no error';
    protected array $responseData = [];
    protected array $config = [];
    public string $error = 'no error';

    public function report(Throwable $exception) {
        $this->dontReport = config('plugin.tinywan.exception-handler.app.exception_handler.dont_report', []);
        parent::report($exception);
    }

    public function render(Request $request, Throwable $exception): Response {
        $this->config = array_merge($this->config, config('plugin.tinywan.exception-handler.app.exception_handler', []) ?? []);
        $this->addRequestInfoToResponse($request);
        $this->solveAllException($exception);
        $this->addDebugInfoToResponse($exception);
        $this->triggerNotifyEvent($exception);
        $this->triggerTraceEvent($exception);
        return $this->buildResponse();
    }

    protected function addRequestInfoToResponse(Request $request): void {
        $this->responseData = array_merge($this->responseData, [
            'domain' => $request->host(),
            'method' => $request->method(),
            'request_url' => $request->method() . ' ' . $request->uri(),
            'timestamp' => date('Y-m-d H:i:s'),
            'client_ip' => $request->getRealIp(),
            'request_param' => $request->all(),
        ]);
    }

    protected function solveAllException(Throwable $e) {
        if ($e instanceof BaseException) {
            $this->statusCode = $e->statusCode;
            $this->header = $e->header;
            $this->errorCode = $e->errorCode;
            $this->errorMessage = $e->errorMessage;
            $this->error = $e->error;
            if (isset($e->data)) $this->responseData = array_merge($this->responseData, $e->data);
            if (!($e instanceof ServerErrorHttpException)) return;
        }
        $this->solveExtraException($e);
    }

    protected function solveExtraException(Throwable $e): void {
        $status = $this->config['status'];
        $this->errorMessage = $e->getMessage();
        if ($e instanceof BadRouteException) {
            $this->statusCode = $status['route'] ?? 404;
        } elseif ($e instanceof \TypeError) {
            $this->statusCode = $status['type_error'] ?? 400;
            $this->errorMessage = $status['type_error_is_response'] ?? false ? $e->getMessage() : 'Network connection seems unstable. Please check your network!';
            $this->error = $e->getMessage();
        } elseif ($e instanceof ValidateException) {
            $this->statusCode = $status['validate'];
        } elseif ($e instanceof JwtTokenException) {
            $this->statusCode = $status['jwt_token'];
        } elseif ($e instanceof JwtTokenExpiredException) {
            $this->statusCode = $status['jwt_token_expired'];
        } elseif ($e instanceof JwtRefreshTokenExpiredException) {
            $this->statusCode = $status['jwt_refresh_token_expired'];
        } elseif ($e instanceof \InvalidArgumentException) {
            $this->statusCode = $status['invalid_argument'] ?? 415;
            $this->errorMessage = 'Invalid argument configuration: ' . $e->getMessage();
        } elseif ($e instanceof DbException) {
            $this->statusCode = 500;
            $this->errorMessage = 'Db: ' . $e->getMessage();
            $this->error = $e->getMessage();
        } elseif ($e instanceof ServerErrorHttpException) {
            $this->errorMessage = $e->errorMessage;
            $this->statusCode = 500;
        } else {
            $this->statusCode = $status['server_error'] ?? 500;
            $this->errorMessage = $status['server_error_is_response'] ?? false ? $e->getMessage() : 'Internal Server Error';
            $this->error = $e->getMessage();
            Logger::error($this->errorMessage, array_merge($this->responseData, [
                'error' => $this->error,
                'file' => $e->getFile(),
                'line' => $e->getLine(),
            ]));
        }
    }

    protected function addDebugInfoToResponse(Throwable $e): void {
        if (config('app.debug', false)) {
            $this->responseData['error_message'] = $this->errorMessage;
            $this->responseData['error_trace'] = explode("
", $e->getTraceAsString());
            $this->responseData['file'] = $e->getFile();
            $this->responseData['line'] = $e->getLine();
        }
    }

    protected function triggerNotifyEvent(Throwable $e): void {
        if (!$this->shouldntReport($e) && ($this->config['event_trigger']['enable'] ?? false)) {
            $data = $this->responseData;
            $data['message'] = $this->errorMessage;
            $data['error'] = $this->error;
            $data['file'] = $e->getFile();
            $data['line'] = $e->getLine();
            DingTalkRobotEvent::dingTalkRobot($data, $this->config);
        }
    }

    protected function triggerTraceEvent(Throwable $e): void {
        if (isset(request()->tracer) && isset(request()->rootSpan)) {
            $samplingFlags = request()->rootSpan->getContext();
            $this->header['Trace-Id'] = $samplingFlags->getTraceId();
            $span = request()->tracer->newChild($samplingFlags);
            $span->setName('exception');
            $span->start();
            $span->tag('error.code', (string)$this->errorCode);
            $span->annotate(json_encode([
                'event' => 'error',
                'message' => $this->errorMessage,
                'stack' => 'Exception:' . $e->getFile() . '|' . $e->getLine(),
            ]));
            $span->finish();
        }
    }

    protected function buildResponse(): Response {
        $bodyKey = array_keys($this->config['body']);
        $bodyValue = array_values($this->config['body']);
        $responseBody = [
            $bodyKey[0] ?? 'code' => $this->setCode($bodyValue, $this->errorCode),
            $bodyKey[1] ?? 'msg' => $this->errorMessage,
            $bodyKey[2] ?? 'data' => $this->responseData,
        ];
        $header = array_merge(['Content-Type' => 'application/json;charset=utf-8'], $this->header);
        return new Response($this->statusCode, $header, json_encode($responseBody));
    }

    private function setCode($bodyValue, $errorCode) {
        return $errorCode > 0 ? $errorCode : ($bodyValue[0] ?? 0);
    }
}

Configuration

To replace Webman’s default handler, add the custom class to config/exception.php:

return [
    // Register custom exception handler
    '' => support\exception\TinywanHandler::class,
];

In a multi‑application setup, each app can specify its own handler in its own configuration file.

Practical Example 1 – Throwing a Business Exception

class ArticleController {
    public function index(Request $request): Response {
        $res = ArticleService::getArticleList();
        if (empty($res)) {
            throw new BadRequestHttpException('Article list is empty');
        }
        return json($res);
    }
}

Resulting HTTP response (JSON):

{
    "code": 0,
    "msg": "Article list is empty",
    "data": {}
}

Practical Example 2 – Division by Zero

public function index(Request $request): Response {
    $page = 100 / 0; // triggers a TypeError
    return json(['page' => $page]);
}

Resulting HTTP response (JSON):

{
    "code": 0,
    "msg": "Division by zero",
    "data": {}
}

For more detailed use‑cases, refer to the official plugin repository: https://www.workerman.net/plugin/16

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentPHPError handlingExceptionWebmanCustom Handler
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.