定制 levacic/exception-with-context 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

levacic/exception-with-context

Composer 安装命令:

composer require levacic/exception-with-context

包简介

A generic interface for exception classes carrying their own context

README 文档

README

Packagist PHP Version Support Packagist Version Packagist Downloads Packagist License

This minimal package provides a single ExceptionWithContext interface which can be implemented in client-side code so your exception objects can carry their own context.

For more information on exactly how and why you would want to do this, read the "Usage" section.

Requirements

  • PHP >= 7.0

Installation

composer require levacic/exception-with-context

Usage

Why?

The "why" obviously depends on how you use exceptions in general, but this is specifically useful when logging exceptions, in order to have some additional information on why the exception was thrown. In general, if you're logging in PHP in 2020, you're more than likely using a logger compatible with PSR-3 - and these loggers allow you to log additional context data, along with the actual log message.

This package, along with some manual or automated wiring (as described further below) allows exceptions to carry their own context, so your logging logic can be cleaner while being more informative at the same time.

Example implementation

When using this package, you would usually implement an exception class something like this:

<?php

declare(strict_types=1);

namespace App\Exceptions;

use Levacic\Exceptions\ExceptionWithContext;
use RuntimeException;
use Throwable;

class UserNotActivated extends RuntimeException implements ExceptionWithContext
{
    /**
     * The ID of the non-activated user.
     */
    private int $userId;

    /**
     * @param int            $userId   The ID of the non-activated user.
     * @param Throwable|null $previous The previous exception.
     * @param int            $code     The internal exception code.
     */
    public function __construct(int $userId, ?Throwable $previous = null, int $code = 0)
    {
        parent::__construct('The user has not been activated yet.', $code, $previous);

        $this->userId = $userId;
    }

    /**
     * @inheritDoc
     */
    public function getContext(): array
    {
        return [
            'userId' => $this->userId,
        ];
    }
}

Note: My preferred convention is to not use the Exception suffix on exception classes, even though it's common to do so in the PHP community. The reason is that I don't personally find it provides any useful information - exception classes are often located in some kind of an Exceptions namespace, and when you're working with them in code, you're usually throwing or catching them - making it pretty obvious what they are.

That's it. The idea is simply to pass additional context info - in this contrived example, a user ID - to the exception, so that it can return it in the getContext() method implementation.

How do you pass the context to an exception? It doesn't matter!

This example implementation uses a constructor to achieve that, so you would throw it maybe something like this:

if (!$user->isActivated()) {
    throw new UserNotActivated($user->id);
}

You could also use a setter method, or even a public property (although you probably don't want these if you prefer immutability in your code).

And all of these approaches work even if you prefer using exception factories (either as separate classes, or static creator methods on the exception class itself).

The only important aspects are:

  • The exception object carries some additional context info
  • This additional context info is exposed through a getContext() method which returns an array of information

This just made it trivial to log this context along with the exception, no matter where/how you handle your logging.

How to log

Logging is a complex subject and depends a lot on your approach to logging in general (e.g. what stuff you want to log, when/why, where you are logging to, etc.), as well as your app's (or framework's) logging architecture - so this section will be a little generic.

So how do you log this information?

Assuming you have a PSR-3 logger instance, you can do something like this:

$logger->error($exception->getMessage(), $exception->getContext());

Of course, the logger needs to be configured with a handler and formatter that are able to process and output the context, wherever it is you're logging to. As far as I know, most common default setups already handle this, so most of the time, this should just work as-is.

So where do you do this? Wherever you want!

A default place where you would definitely want to do this is your app's top-level exception handler.

Another useful location where you can do this is any catch block within which you might already have a way to handle the situation, but still want to log that this exception happened, for debugging or general logging purposes.

E.g. if your app has some service class which is interacting with an external API, you might have something like this:

$response = makeRequestToExternalApi($someRequestData);

if ($response->statusCode !== 200) {
    throw new InvalidResponseFromExternalApi(
        $someRequestData,
        $response->statusCode,
        $response->body,
        $response->headers,
    );
}

return $response;

The InvalidResponseFromExternalApi exception's constructor would accept these arguments, store them, and then format them nicely in the getContext() method.

Some higher-level code, e.g. a controller which sits in the app's HTTP layer, might in turn do something like this:

try {
    $response = $apiService->getResponse();
} catch (InvalidResponseFromExternalApi $exception) {
    $logger->error($exception->getMessage(), $exception->getContext());

    return new \Symfony\Component\HttpFoundation\Response('External API is currently unavailable.', 503);
}

return $response;

(Or you can rethrow as a ServiceUnavailableHttpException or whatever makes sense in the context of your app's architecture).

With this, you would basically return a useful message to the client, while still logging internally that an error occurred - and this log message would include useful context information.

Laravel

If you're using a somewhat recent Laravel version, you can override Illuminate\Foundation\Exceptions\Handler::exceptionContext() in your App\Exceptions\Handler, which extends Laravel's class in a default Laravel setup:

protected function exceptionContext(Throwable $e)
{
    if ($e instanceof ExceptionWithContext) {
        return $e->getContext();
    }

    return parent::exceptionContext($e);
}

This is, in turn, called automatically when Laravel logs an error.

Thus, any exceptions with context that are uncaught within application code will get logged along with the context they're carrying.

However, this will only work when the caught exception is the one carrying context - but any chained exceptions with context would have their context ignored. A better solution can be achieved with a custom Monolog processor, in case, of course, you're using Monolog - which is highly likely.

Custom Monolog processor

You can attach a processor to Monolog which checks if an exception key in the log record's context is set, and if it is, whether it is a Throwable, and then do some custom logic in that case.

To achieve what we want, we would basically just traverse the exception chain (via $exception->getPrevious()), and for each of the exceptions in the chain, check whether it implements ExceptionWithContext and then extract that data.

In fact, I'm in the process of creating a package with a processor that does exactly this - which I'll link to here once it has been published.

License

This package is open-source software licensed under the [MIT license][LICENSE].

levacic/exception-with-context 适用场景与选型建议

levacic/exception-with-context 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 57.36k 次下载、GitHub Stars 达 1, 最近一次更新时间为 2020 年 12 月 30 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「logging」 「errors」 「exceptions」 「Context」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 levacic/exception-with-context 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 levacic/exception-with-context 我们能提供哪些服务?
定制开发 / 二次开发

基于 levacic/exception-with-context 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 57.36k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 15
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2020-12-30