m1x0n/opis-json-schema-error-presenter

opis库的JSON schema错误展示器

v0.5.3 2023-03-14 11:08 UTC

This package is auto-updated.

Last update: 2024-09-14 14:09:13 UTC


README

警告

⚠️此库可能由于新主要版本(2.0.0)的opis/json-schema已支持错误格式化而变得过时。

为opis/json-schema库生成的JSON schema验证错误提供可定制的错误展示器:JSON schema实现。

换句话说,它是对Opis\JsonSchema\ValidationError集合以人类可读方式表示的直接尝试。

要求

  • php >= 7.1
  • opis/json-schema

安装

composer require m1x0n/opis-json-schema-error-presenter

使用示例

use Opis\JsonSchema\Schema;
use Opis\JsonSchema\ValidationResult;
use Opis\JsonSchema\Validator;
use OpisErrorPresenter\Contracts\PresentedValidationError;
use OpisErrorPresenter\Implementation\MessageFormatterFactory;
use OpisErrorPresenter\Implementation\PresentedValidationErrorFactory;
use OpisErrorPresenter\Implementation\ValidationErrorPresenter;

require __DIR__ . '/../vendor/autoload.php';

$jsonSchema ='{
  "$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#",
  "$id": "http://example.com/product.schema.json",
  "title": "Product",
  "description": "A product from Acme\'s catalog",
  "type": "object",
  "properties": {
    "productId": {
      "type": "integer",
      "minimum": 1
    },
    "productName": {
      "type": "string",
      "minLength": 3
    },
    "price": {
      "type": "object",
      "properties": {
        "amount": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1000
        },
        "currency": {
          "type": "string",
          "enum": ["USD", "EUR", "BTC"]
        }
      },
      "required": ["amount", "currency"]
    }
  },
  "required": [ "productId", "productName", "price" ]
}';

$data = '{
  "productId": "123",
  "productName": "XX",
  "price": {
    "amount": 200,
    "currency": "GBP"
  }
}';

$data = json_decode($data);
$jsonSchema = Schema::fromJsonString($jsonSchema);
$validator = new Validator();

// Get all errors. Yeah -1 here.
/** @var ValidationResult $result */
$result = $validator->schemaValidation($data, $jsonSchema, -1);

// Default strategy is AllErrors
$presenter = new ValidationErrorPresenter(
    new PresentedValidationErrorFactory(
        new MessageFormatterFactory()
    )
);

$presented = $presenter->present(...$result->getErrors());

// Inspected presenter error
print_r(array_map(static function (PresentedValidationError $error) {
    return $error->toArray();
}, $presented));

// Json-serializable
echo json_encode($presented);

输出结果示例

Array
(
    [0] => Array
        (
            [keyword] => type
            [pointer] => productId
            [message] => The attribute expected to be of type 'integer' but 'string' given.
        )

    [1] => Array
        (
            [keyword] => minLength
            [pointer] => productName
            [message] => The attribute length should be at least 3 characters.
        )

    [2] => Array
        (
            [keyword] => enum
            [pointer] => price/currency
            [message] => The attribute must be one of the following values: 'USD', 'EUR', 'BTC'.
        )

)

展示策略

  • AllErrors - 显示所有可用的展示错误
  • FirstError - 选择展示错误中的第一个
  • BestMatchError - 评估最佳匹配错误

为了指定策略,只需将选定的策略传递给PresentedValidationErrorFactory,例如

$presenter = new ValidationErrorPresenter(
    new PresentedValidationErrorFactory(
        new MessageFormatterFactory()
    ),
    new BestMatchError()
);

自定义翻译

有自定义翻译的可能性。目前只有DefaultTranslator公开了一些通用消息,例如

属性长度应至少为3个字符

为了替换、扩展或提出新的翻译,必须实现MessageTranslator接口。例如

<?php
declare(strict_types=1);

namespace Acme;

use OpisErrorPresenter\Contracts\Keyword;
use OpisErrorPresenter\Implementation\MessageFormatterFactory;
use OpisErrorPresenter\Implementation\PresentedValidationErrorFactory;
use OpisErrorPresenter\Implementation\Translators\DefaultTranslator;
use OpisErrorPresenter\Implementation\ValidationErrorPresenter;

class InternationalTranslator extends DefaultTranslator
{
    protected $messages = [];

    private const DEFAULT_MESSAGE = 'The attribute is invalid';

    public function __construct()
    {
        parent::__construct();
        $this->loadMessages();
    }

    public function translate(string $key, array $replacements = [], $locale = null): string
    {
        if ($locale && array_key_exists($locale, $this->messages)) {
            $message = $this->messages[$locale][$key] ?? self::DEFAULT_MESSAGE;
            return strtr($message, $replacements);
        }

        // Fallback on default locale
        return parent::translate($key, $replacements, $locale);
    }

    private function loadMessages(): void
    {
        /*
            Locales structure example:
            [
               'locale_1' => [
                  'keyword' => 'translation_1'
                  ...
               ],
               'locale_2' => [
                  'keyword' => 'translation_2'
                  ...
               ],
               ...
            ]
        */
        $this->messages = [
            'de_DE' => [
                // The rest of other keywords ...
                Keyword::MIN_LENGTH => 'Die Attributlänge sollte mindestens betragen: min: Zeichen.'
                // ...
            ],
            'ru_RU' => [
                // ...
                Keyword::ENUM => 'Длина атрибута должна быть минимум :min: символов.'
                // ....
            ]
        ];
    }
}

// Then configure presenter factory
$presenter = new ValidationErrorPresenter(
    new PresentedValidationErrorFactory(
        new MessageFormatterFactory(),
        new InternationalTranslator()
    )
);

区域设置解析

为了获得更好的不同本地化体验,可以使用自定义翻译器与自动区域设置解析一起使用。

目前实现了以下区域设置解析器

  • NullLocaleResolver作为通用消息的回退。
  • HttpLocaleResolver尝试根据HTTP_ACCEPT_LANGUAGE头检测区域设置,并需要安装ext-intl

也可以通过实现LocaleResolver接口来实现自定义区域设置解析器。

为了设置HttpLocaleResolver或自定义的解析器

$presenter = new ValidationErrorPresenter(
    new PresentedValidationErrorFactory(
        new MessageFormatterFactory(),
        new InternationalTranslator(),
        new HttpLocaleResolver()
    )
);

区域设置支持

目前有使用通过ArrayLocaleLoader定义的区域设置的可能性。见lang/en.php

为了加载更多语言,可以按照以下方式配置ValidationErrorPresenter

$presenter = new ValidationErrorPresenter(
    new PresentedValidationErrorFactory(
        new MessageFormatterFactory(),
        new DefaultTranslator(
            (new ArrayLocaleLoader())->addPath('de', '/path_to_lang/lang/de.php')
        )
    )
);

但是,可以通过实现LocaleLoader接口从任何地方加载区域设置字符串。

此外,可以使用单个区域设置配置展示器。为此,可以使用FixedLocaleResolver。例如

$presenter = new ValidationErrorPresenter(
    new PresentedValidationErrorFactory(
        new MessageFormatterFactory(),
        new DefaultTranslator(
            (new ArrayLocaleLoader())->addPath('cs', '../lang')
        ),
        new FixedLocaleResolver('cs')
    )
);

可以通过使用DI-container稍微简化所有配置。