如何为JSON api使用本地Symfony序列器和验证器

237 阅读1分钟

如果你的API只针对JSON请求,那么你可以使用Symfony本地的serializer包,而不是更强大的JMS Serilizer包。使用方法见下面的例子。

安装

$ composer require symfony/serializer
$ composer require symfony/property-info
$ composer require symfony/validator

配置

# services.yaml

services:
    ...

    Symfony\Component\Serializer\Normalizer\PropertyNormalizer:
        arguments:
            $nameConverter: '@serializer.name_converter.camel_case_to_snake_case'
            $propertyTypeExtractor: '@property_info.php_doc_extractor'
        tags: [serializer.normalizer]

请求工具(RequestUtil)

namespace App\Util;

use Exception;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class RequestUtil
{
    private $serializer;
    private $validator;
    private $violator;

    public function __construct(
        SerializerInterface $serializer,
        ValidatorInterface $validator,
        ViolationUtil $violator
    ) {
        $this->serializer = $serializer;
        $this->validator = $validator;
        $this->violator = $violator;
    }

    public function validate(string $data, string $model): object
    {
        if (!$data) {
            throw new BadRequestHttpException('Empty body.');
        }

        try {
            $object = $this->serializer->deserialize($data, $model, 'json');
        } catch (Exception $e) {
            throw new BadRequestHttpException('Invalid body.');
        }

        $errors = $this->validator->validate($object);

        if ($errors->count()) {
            throw new BadRequestHttpException(json_encode($this->violator->build($errors)));
        }

        return $object;
    }
}

ViolationUtil

declare(strict_types=1);

namespace Inanzzz\RequestResponseHandler\Util;

use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;

class ViolationUtil implements ViolationUtilInterface
{
    private $textUtil;

    public function __construct(TextUtilInterface $textUtil)
    {
        $this->textUtil = $textUtil;
    }

    public function build(ConstraintViolationListInterface $violations): array
    {
        $errors = [];

        /** @var ConstraintViolation $violation */
        foreach ($violations as $violation) {
            $errors[
                $this->textUtil->makeSnakeCase($violation->getPropertyPath())
            ] = $violation->getMessage();
        }

        return $this->buildMessages($errors);
    }

    private function buildMessages(array $errors): array
    {
        $result = [];

        foreach ($errors as $path => $message) {
            $temp = &$result;

            foreach (explode('.', $path) as $key) {
                preg_match('/(.*)(\[.*?\])/', $key, $matches);
                if ($matches) {
                    $index = str_replace(['[', ']'], '', $matches[2]);
                    $temp = &$temp[$matches[1]][$index];
                } else {
                    $temp = &$temp[$key];
                }
            }

            $temp = $message;
        }

        return $result;
    }
}

文本利用(TextUtil

declare(strict_types=1);

namespace Inanzzz\RequestResponseHandler\Util;

class TextUtil implements TextUtilInterface
{
    public function makeSnakeCase(string $text): string
    {
        if (!trim($text)) {
            return $text;
        }

        return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $text));
    }
}

响应利用

namespace App\Util;

use Symfony\Component\Serializer\SerializerInterface;

class ResponseUtil implements ResponseUtilInterface
{
    private $serializer;

    public function __construct(
        SerializerInterface $serializer
    ) {
        $this->serializer = $serializer;
    }

    public function serialize(object $model): string
    {
        return $this->serializer->serialize($model, 'json');
    }
}

模型

用户

namespace App\Model\User\Update;

use Symfony\Component\Validator\Constraints as Assert;

class User
{
    /**
     * @var string
     *
     * @Assert\Type("string")
     * @Assert\NotBlank
     */
    public $fullName;

    /**
     * @var iterable
     *
     * @Assert\Type("iterable")
     */
    public $favouriteTeams;

    /**
     * @var iterable
     *
     * @Assert\Type("iterable")
     */
    public $randomData;

    /**
     * @var Address
     *
     * @Assert\NotBlank
     * @Assert\Type("App\Model\User\Update\Address")
     * @Assert\Valid
     */
    public $address;
}

地址

namespace App\Model\User\Update;

use Symfony\Component\Validator\Constraints as Assert;

class Address
{
    /**
     * @var int
     *
     * @Assert\Type("int")
     * @Assert\NotBlank
     */
    public $houseNumber;

    /**
     * @var string
     *
     * @Assert\Type("string")
     * @Assert\NotBlank
     */
    public $line1;

    /**
     * @var string
     *
     * @Assert\Type("string")
     */
    public $line2;

    /**
     * @var iterable
     *
     * @Assert\Type("iterable")
     * @Assert\Count(max=2)
     */
    public $flats;

    /**
     * @var Occupier[]
     *
     * @Assert\NotBlank
     * @Assert\Type("iterable")
     * @Assert\Valid
     */
    public $occupiers;
}

占用者

namespace App\Model\User\Update;

use Symfony\Component\Validator\Constraints as Assert;

class Occupier
{
    /**
     * @var string
     *
     * @Assert\Type("string")
     * @Assert\NotBlank
     */
    public $fullName;

    /**
     * @var string
     *
     * @Assert\Type("string")
     * @Assert\NotBlank
     */
    public $sex;
}

使用情况

# Deserialize request as User model and validate it
$userModel = $this->requestUtil->validate($request->getContent(), User::class);

# Serilize User object as JSON string
$userJsonString = $this->responseUtil->serialize($userModel);

# Return response
return new Response($result, 200, ['Content-Type' => 'application/json']);

请求

{
  "full_name": "John Travolta",
  "favourite_teams": [
    "Fenerbahce",
    "Arsenal"
  ],
  "random_data": {
    "hair_style": "Curly",
    "eye_colour": "Brown",
    "allergies": [
      "Nuts",
      "Shellfish"
    ]
  },
  "address": {
    "house_number": 184,
    "line1": "House 101",
    "line2": "",
    "flats": [
      "A",
      "B"
    ],
    "occupiers": [
      {
        "full_name": "Robert DeNiro",
        "sex": "Male"
      },
      {
        "full_name": "Sharon Stone",
        "sex": "Female"
      }
    ]
  }
}