dakujem/strata74

dakujem/strata (分层异常处理机制合约) 的 PHP 7.4 版本回端口

2.0 2023-11-05 08:46 UTC

This package is auto-updated.

Last update: 2024-09-08 18:06:26 UTC


README

✋ 请注意

这是原始包 dakujem/strata 的回端口,仅适用于 PHP 7.4

基于 PHP 8 版本的 v1.0 版本。

在更新到 PHP 8 后,只需将要求更改为 dakujem/strata
很可能,您不需要做任何事情。
如果您的类直接实现了 strata 接口,您只需要更新实现的方法(pinreplaceContextpassexplaintagconvey)的类型提示。

PHP 8 版本改进了键处理(支持整数键,与 PHP 数组键处理对齐)和完整的测试覆盖率。

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状态参考

构建自己的

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

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状态码