dakujem/strata

分层异常处理机制的合约。

1.1 2024-07-17 09:12 UTC

This package is auto-updated.

Last update: 2024-09-17 09:40:44 UTC


README

Test Suite Coverage Status

分层异常处理机制的合约和实现。

💿 composer require dakujem/strata

📒 变更日志

TL;DR

原生PHP异常允许携带字符串消息和整型代码。

👉 这还不够。 👈

拥抱具有上下文信息的异常。

具有上下文信息的异常

  • 向客户端传达信息
  • 为开发者携带元数据

HTTP客户端错误示例

use Dakujem\Strata\Http\UnprocessableContent;

if(!isEmail($input['email'])){
    // HTTP 422
    throw (new UnprocessableContent('Invalid e-mail address.'))
        // Convey messages to clients with localization support and metadata:
        ->convey(
            message: __('Please enter a valid e-mail address.'),
            source: 'input.email',
        )
        // Add details for developers to be reported or logged:
        ->pin([
            'name' => $input['name'],
            'email' => $input['email'],
        ], key: 'input');
}

内部逻辑故障

use Dakujem\Strata\LogicException;

throw (new LogicException('Invalid value of Something.'))
    // Convey messages and meta-data to clients, humans and apps alike:
    ->convey(
        message: __('We are sorry, we encountered an issue with Something and are unable to fulfil your request.'),
        source: 'something.or.other',
        description: __('We are already fixing the issue, please try again later.'),
        meta: ['param' => 42]
    )
    // Add details for developers to be reported or logged:
    ->explain('Fellow developers, this issue is caused by an invalid value: ' . $someValue)
    ->pin(['value' => $someValue, 'severity' => 'serious'], 'additional context')
    ->pin(['other' => $value], 'other context')
    ->pin('anything')
    ->tag('something')
    ->tag(tag: 'serious', key: 'severity')
;

元数据可以被服务器端错误处理器用来报告详细数据。

面向客户端的数据可以被客户端应用程序使用,并显示给最终用户或进行程序处理。
这对于API+客户端应用程序架构(JS小部件、PWA、移动应用程序等)特别有用。

使用错误处理器处理上下文

通常,您的应用程序将有一个全局错误处理机制,无论是启动时的try-catch块、错误处理中间件还是原生错误处理器。

类似于以下内容

try {
    process_request(Request::fromGlobals());
} catch (Throwable $e) {
    handle_exception($e);
}

例如,在Laravel中,通常是由 App\Exceptions\Handler 类来处理异常
在Slim中, 中间件用于错误处理
Symfony 也会捕获所有异常和错误,并且可以自定义处理逻辑。

所有这些都需要开发者想出特定的异常,这些异常携带特定的上下文,如Guzzle的 RequestException 携带HTTP请求和响应。
这很正常。无论如何,请为特定目的创建特定的异常。
Strata通过实现单个接口(SupportsContextStrata)和使用单个特性(ContextStrata)来帮助启用上下文支持。

也有时候,一个人不需要特定的异常,但仍然希望将上下文数据传递给全局错误处理器。
Strata为常见的HTTP响应提供异常。

为了报告或记录上下文,或者为了向前端消费者提供信息,strata提供接口和实现。

示例

Laravel中异常处理器的示例(JSON API)

namespace App\Exceptions;

use Dakujem\Strata\Contracts\IndicatesAuthenticationFault;
use Dakujem\Strata\Contracts\IndicatesAuthorizationFault;
use Dakujem\Strata\Contracts\IndicatesClientFault;
use Dakujem\Strata\Contracts\IndicatesConflict;
use Dakujem\Strata\Contracts\IndicatesInvalidInput;
use Dakujem\Strata\Support\ErrorContainer;
use Dakujem\Strata\Support\SuggestsHttpStatus;
use Dakujem\Strata\Support\SupportsPublicContext;

class Handler extends ExceptionHandler
{
    protected function prepareJsonResponse($request, Throwable $e)
    {
        $errors = $e instanceof SupportsPublicContext ? $e->publicContext() : null;
        $errors ??= [];

        if ($errors === []) {
            $message = $detail = null;
            // Public error message for clients:
            if ($e instanceof SuggestsErrorMessage) {
                $message = $e->suggestErrorMessage();
            }
            // Laravel/Symfony HTTP exception
            if ($e instanceof HttpExceptionInterface) {
                $message = $e->getMessage();
            }
            // Slim HTTP exception
            if ($e instanceof HttpException) {
                $message = $e->getTitle();
                $detail = $e->getDescription();
            }
            $errors[] = new ErrorContainer(
                message: $message,
                detail: $detail,
            );
        }

        // Status code
        $code = 500;
        if ($e instanceof SuggestsHttpStatus) {
            $code = $e->suggestStatusCode();
        } elseif ($e instanceof IndicatesInvalidInput) {
            $code = 422; // 422 Unprocessable Content
        } elseif ($e instanceof IndicatesConflict) {
            $code = 409; // 409 Conflict
        } elseif ($e instanceof IndicatesAuthorizationFault) {
            $code = 403; // 403 Forbidden
        } elseif ($e instanceof IndicatesAuthenticationFault) {
            $code = 401; // 401 Unauthorized
        } elseif ($e instanceof IndicatesClientFault) {
            $code = 400; // 400 Bad Request
        } elseif ($e instanceof HttpExceptionInterface) {
            // Laravel/Symfony HTTP exceptions
            $code = $e->getStatusCode();
        } elseif ($e instanceof ValidationException) {
            $code = $e->status; // 422 Unprocessable Content (default)
        } elseif ($e instanceof HttpException) {
            // Slim HTTP exception
            $code = $e->getCode();
        }

        return response()
            ->json(
                data: [
                    'errors' => $errors,
                ],
            )
            ->withStatus(
                $code,
            );
    }
}

处理内部上下文以改进 Sentry 报告的示例

use Dakujem\Strata\Support\SupportsInternalContext;
use Dakujem\Strata\Support\SupportsInternalExplanation;
use Dakujem\Strata\Support\SupportsTagging;
use Sentry\Event;
use Sentry\EventHint;
use Sentry\State\HubInterface;
use Sentry\State\Scope;

function reportException(Throwable $e)
{
    $hub = Container::get(HubInterface::class);
    $hub->configureScope(function (Scope $scope) use ($e): void {
    
        // Internal context comprises all the pinned data (see `pin` method usage above)
        if ($e instanceof SupportsInternalContext) {
            foreach ($e->context() as $key => $value) {
                if (is_string($value) || is_numeric($value) || $value instanceof Stringable) {
                    $value = [
                        'value' => (string)$value,
                    ];
                }
                if (is_object($value)) {
                    $value = (array)$value;
                }
                if (is_array($value)) {
                    $scope->setContext(
                        is_numeric($key) ? 'context-' . $key : $key,
                        $value,
                    );
                }
            }
        }

        if ($e instanceof SupportsTagging) {
            foreach ($e->tags() as $key => $value) {
                // When tags have numeric keys, use tag:true format, otherwise use key:tag format.
                $scope->setTag(
                    is_numeric($key) ? $value : $key,
                    is_numeric($key) ? 'true' : $value,
                );
            }
        }

        if ($e instanceof SupportsInternalExplanation) {
            $scope->setContext(
                '_dev_',
                [
                    'explanation' => $e->explanation(),
                ],
            );
        }
    });

    $event = Event::createEvent();
    $event->setMessage($e->getMessage());
    $hint = new EventHint();
    $hint->exception = $e;

    $hub->captureEvent($event, $hint);
}

API设计的著名合约

自动错误处理的合约,特别是对于HTTP API非常有用

  • IndicatesClientFault 4xx
  • IndicatesServerFault 5xx
if($exception instanceof IndicatesClientFault){
    return convert_client_exception_to_4xx_response($exception);
}
if($exception instanceof IndicatesServerFault){
    report_server_fault($exception);
    return apologize_for_server_issue_with_5xx_status($exception);
}

常见的HTTP 4xx异常

此软件包提供常见4xx HTTP状态响应的异常。

  • 400 BadRequest
  • 404 NotFound
  • 403 Forbidden
  • 401 Unauthorized
  • 409 Conflict
  • 422 UnprocessableContent

有关更多信息,请参阅 HTTP状态参考

自己构建

strata合约和特性允许并鼓励开发者轻松地为特定用例创建自己的异常。

class MySpecificException extends WhateverBaseException implements SupportsContextStrata
{
    use ContextStrata;

    public function __construct($message = null, $code = 0, Throwable $previous = null)
    {
        parent::__construct(
            $message ?? 'This is the default message for my specific exception.',
            $code ?? 0,
            $previous,
        );
    }
}

如果只需要选择机制,请使用下表选择特定接口和特性

为了提供具有HTTP能力的可抛出对象,实现以下简单接口

  • SuggestsErrorMessage 向错误处理器建议错误信息
  • SuggestsHttpStatus 向错误处理器建议HTTP状态码

兼容性和支持

此包需要 PHP >=8.0。没有其他外部依赖。

存在PHP 7.4的向后移植版本: dakujem/strata74