Replies: 3 comments
-
Maybe this will get you started (Symfony 6.1, PHP 8.1). This replaces UUID references with IRI (and vice versa) for format <?php
declare(strict_types=1);
namespace App\Serializer\Normalizer;
use ArrayObject;
use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
use Symfony\Component\PropertyInfo\Type;
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;
use function is_string;
use function strrpos;
use function substr;
#[AsDecorator(decorates: 'api_platform.serializer.normalizer.item', priority: 100)]
final class ItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
private readonly NormalizerInterface|DenormalizerInterface $normalizer;
public function __construct(
#[MapDecorated] NormalizerInterface|DenormalizerInterface $normalizer,
private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
private readonly IriConverterInterface $iriConverter,
) {
if (!$normalizer instanceof NormalizerInterface) {
throw new InvalidArgumentException('Passed denormalizer must also implement normalizer interface.');
}
if (!$normalizer instanceof DenormalizerInterface) {
throw new InvalidArgumentException('Passed normalizer must also implement denormalizer interface.');
}
$this->normalizer = $normalizer;
}
public function normalize(
mixed $object,
string $format = null,
array $context = []
): array|string|int|float|bool|ArrayObject|null {
$result = $this->normalizer->normalize($object, $format, $context);
// For json, we have to change the IRIs to plain ids.
if ('json' !== $format) {
return $result;
}
foreach ($result as $key => $value) {
// If set in resources, the value is an IRI.
if (is_string($value) && isset($context['resources'][$value])) {
$result[$key] = substr($value, strrpos($value, '/') + 1);
}
}
return $result;
}
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return $this->normalizer->supportsNormalization($data, $format, $context);
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
// Can only handle arrays.
if (!is_array($data)) {
return $this->normalizer->denormalize($data, $type, $format, $context);
}
if ($type == $context['operation']->getClass()) {
foreach ($data as $key => $value) {
$propertyMetadata = $this->propertyMetadataFactory->create($type, $key);
$propertyTypeObject = $propertyMetadata->getBuiltinTypes()[0];
$propertyType = $propertyTypeObject->getBuiltinType();
if ($propertyType === 'object') {
$data = $this->convertIdRelationsToIRIs($data, $key, $value, $propertyTypeObject);
}
}
}
return $this->normalizer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $this->normalizer->supportsDenormalization($data, $type, $format, $context);
}
public function setSerializer(SerializerInterface $serializer): void
{
if ($this->normalizer instanceof SerializerAwareInterface) {
$this->normalizer->setSerializer($serializer);
}
}
private function convertIdRelationsToIRIs(array $data, string $key, mixed $value, Type $type): array
{
if ($type->isCollection()) {
return $this->handleCollectionIdsConversion($data, $key, $value, $type);
}
return $this->handleItemIdConversion($data, $key, $value, $type);
}
/**
* Convert ["uuid1", "uuid2", "uuid3"]
* to ["/bar/foo/uuid1", "/bar/foo/uuid2", "/bar/foo/uuid3"]
*/
private function handleCollectionIdsConversion(array $data, string $key, mixed $value, Type $type): array
{
if (!is_array($value) || count($value) === 0) {
return $data;
}
$className = $type->getCollectionValueTypes()[0]->getClassName();
$iriList = [];
foreach ($value as $uuid) {
if (!Uuid::isValid($uuid)) {
throw new InvalidArgumentException('Parameter for relation ' . $className . ' must be UUID');
}
$iriList[] = $this->iriConverter->getIriFromResource(
resource: $className,
context: ['uri_variables' => ['id' => $uuid]]
);
}
$data[$key] = $iriList;
return $data;
}
/**
* Convert "uuid" to "/bar/foo/uuid"
*/
private function handleItemIdConversion(array $data, string $key, mixed $value, Type $type): array
{
$className = $type->getClassName();
if (!Uuid::isValid($value)) {
throw new InvalidArgumentException('Parameter for relation ' . $className . ' must be UUID');
}
$data[$key] = $this->iriConverter->getIriFromResource(
resource: $className,
context: ['uri_variables' => ['id' => $value]]
);
return $data;
}
} |
Beta Was this translation helpful? Give feedback.
-
Removing allow_plain_identifiers caused some major breaks in our application, and now we have to revisit all API requests to fix them which is very hard This is very specific |
Beta Was this translation helpful? Give feedback.
-
How about this?
In Entity use this:
|
Beta Was this translation helpful? Give feedback.
-
Is there a more detailed explanation on how to "replicate" allow_plain_identifiers: true when writing data to the API?
I understand that in the docs there is one subsection in 'core/serialization/#plain-identifiers' but this seems does not answer completely, because what happens when I have more than one relation in a Doctrine entity? Does that means I have to create a lot of
if
block for each relation both in::denormalize
and::supportsDenormalization
?Beta Was this translation helpful? Give feedback.
All reactions