Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

If your API is for only JSON requests then you can use native Symfony serializer package instead of more powerful JMS Serilizer package. See example below for usage.


Installation


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

Configuration


# 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));
}
}

ResponseUtil


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');
}
}

Models


User


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;
}

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;
}

Occupier


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;
}

Usage


# 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']);

Request


{
"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"
}
]
}
}