Skip to content

Instantly share code, notes, and snippets.

@stefanfisk
Last active November 14, 2024 12:42
Show Gist options
  • Save stefanfisk/06651a51e69ba48322d59b456b5b3c23 to your computer and use it in GitHub Desktop.
Save stefanfisk/06651a51e69ba48322d59b456b5b3c23 to your computer and use it in GitHub Desktop.
Custom symfony/serialization object normalizer that maps null or missing keys to "" for string properties
<?php
declare(strict_types=1);
namespace App\Serializer\Normalizer;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
use function count;
use function is_array;
/**
* Decorates an object normalizer and denormalizes null or missing keys to empty
* strings for `string` properties.
*/
class NullToEmptyStringObjectNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
public function __construct(
private readonly NormalizerInterface&DenormalizerInterface $decorated,
private readonly PropertyInfoExtractorInterface $propertyInfoExtractor,
private readonly ?NameConverterInterface $nameConverter = null,
) {
}
public function setSerializer(SerializerInterface $serializer)
{
if ($this->decorated instanceof SerializerAwareInterface) {
$this->decorated->setSerializer($serializer);
}
}
/**
* @return array<string,?bool>
*/
public function getSupportedTypes(?string $format): array
{
return $this->decorated->getSupportedTypes($format);
}
public function supportsNormalization(mixed $data, ?string $format = null): bool
{
return $this->decorated->supportsNormalization($data, $format);
}
/**
* @param array<mixed> $context
* @phpstan-ignore missingType.iterableValue,missingType.generics
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): mixed
{
return $this->decorated->normalize($data, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null): bool
{
return $this->decorated->supportsDenormalization($data, $type, $format);
}
/**
* @param array<mixed> $context
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
if (! is_array($data)) {
return $this->decorated->denormalize($data, $type, $format, $context);
}
foreach ($this->propertyInfoExtractor->getProperties($type, $context) ?? [] as $propertyName) {
$key = $this->nameConverter?->normalize($propertyName) ?? $propertyName;
$value = $data[$key] ?? null;
if ($value !== null) {
continue;
}
if (! $this->isNonNullableStringProperty($type, $propertyName)) {
continue;
}
$data[$key ] = '';
}
return $this->decorated->denormalize($data, $type, $format, $context);
}
private function isNonNullableStringProperty(string $type, string $propertyName): bool
{
$propertyTypes = $this->propertyInfoExtractor->getTypes($type, $propertyName);
if ($propertyTypes === null || count($propertyTypes) !== 1) {
return false;
}
$propertyType = $propertyTypes[0];
if ($propertyType->getBuiltinType() !== 'string') {
return false;
}
if ($propertyType->isNullable()) {
return false;
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment