Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/Elasticsearch/Serializer/DocumentDenormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Elasticsearch\Serializer;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Document denormalizer for Elasticsearch.
*
* @experimental
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class DocumentDenormalizer implements DenormalizerInterface, SerializerAwareInterface
{
public const FORMAT = 'elasticsearch';

private readonly ObjectNormalizer $decoratedDenormalizer;

public function __construct(
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
private readonly ?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null,
?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null,
?callable $objectClassResolver = null,
array $defaultContext = [],
) {
$this->decoratedDenormalizer = new ObjectNormalizer($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
}

/**
* {@inheritdoc}
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return self::FORMAT === $format && $this->decoratedDenormalizer->supportsDenormalization($data, $type, $format, $context);
}

/**
* {@inheritdoc}
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
if (\is_string($data['_id'] ?? null) && \is_array($data['_source'] ?? null)) {
$data = $this->populateIdentifier($data, $type)['_source'];
}

return $this->decoratedDenormalizer->denormalize($data, $type, $format, $context);
}

/**
* Populates the resource identifier with the document identifier if not present in the original JSON document.
*/
private function populateIdentifier(array $data, string $class): array
{
$identifier = 'id';
$resourceMetadata = $this->resourceMetadataCollectionFactory->create($class);

$operation = $resourceMetadata->getOperation();
if ($operation instanceof HttpOperation) {
$uriVariable = $operation->getUriVariables()[0] ?? null;

if ($uriVariable) {
$identifier = $uriVariable->getIdentifiers()[0] ?? 'id';
}
}

$identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier, $class, self::FORMAT);

if (!isset($data['_source'][$identifier])) {
$data['_source'][$identifier] = $data['_id'];
}

return $data;
}

/**
* {@inheritdoc}
*/
public function setSerializer(SerializerInterface $serializer): void
{
$this->decoratedDenormalizer->setSerializer($serializer);
}

/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array
{
return self::FORMAT === $format ? ['object' => true] : [];
}
}
67 changes: 14 additions & 53 deletions src/Elasticsearch/Serializer/DocumentNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,32 @@

namespace ApiPlatform\Elasticsearch\Serializer;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Document denormalizer for Elasticsearch.
* Document normalizer for Elasticsearch.
*
* @experimental
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class DocumentNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
final class DocumentNormalizer implements NormalizerInterface, SerializerAwareInterface
{
public const FORMAT = 'elasticsearch';

private readonly ObjectNormalizer $decoratedNormalizer;

public function __construct(
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
private readonly ?NameConverterInterface $nameConverter = null,

Check failure on line 41 in src/Elasticsearch/Serializer/DocumentNormalizer.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.5)

Property ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer::$nameConverter is never read, only written.
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null,
?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null,
Expand All @@ -56,66 +51,32 @@
/**
* {@inheritdoc}
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return self::FORMAT === $format && $this->decoratedNormalizer->supportsDenormalization($data, $type, $format, $context);
}

/**
* {@inheritdoc}
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
if (\is_string($data['_id'] ?? null) && \is_array($data['_source'] ?? null)) {
$data = $this->populateIdentifier($data, $type)['_source'];
// Ensure that a resource is being normalized
if (!\is_object($data)) {
return false;
}

return $this->decoratedNormalizer->denormalize($data, $type, $format, $context);
}

/**
* {@inheritdoc}
*/
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
// prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format
// Only normalize for the elasticsearch format
return self::FORMAT === $format;
}

/**
* {@inheritdoc}
*
* @throws LogicException
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
throw new LogicException(\sprintf('%s is a write-only format.', self::FORMAT));
}

/**
* Populates the resource identifier with the document identifier if not present in the original JSON document.
*/
private function populateIdentifier(array $data, string $class): array
{
$identifier = 'id';
$resourceMetadata = $this->resourceMetadataCollectionFactory->create($class);

$operation = $resourceMetadata->getOperation();
if ($operation instanceof HttpOperation) {
$uriVariable = $operation->getUriVariables()[0] ?? null;

if ($uriVariable) {
$identifier = $uriVariable->getIdentifiers()[0] ?? 'id';
}
}

$identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier, $class, self::FORMAT);
$normalizedData = $this->decoratedNormalizer->normalize($data, $format, $context);

if (!isset($data['_source'][$identifier])) {
$data['_source'][$identifier] = $data['_id'];
// Add _id and _source if not already present
// This is a basic implementation and might need to be more sophisticated based on specific needs.
// It assumes 'id' is the primary identifier for the resource.
if (\is_array($normalizedData) && !isset($normalizedData['_id']) && isset($normalizedData['id'])) {
$normalizedData = ['_id' => (string) $normalizedData['id'], '_source' => $normalizedData];
}

return $data;
return $normalizedData;
}

/**
Expand Down
100 changes: 100 additions & 0 deletions src/JsonApi/Serializer/ItemDenormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\JsonApi\Serializer;

use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use ApiPlatform\Serializer\TagCollectorInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

/**
* Converts JSON:API data to objects (denormalization only).
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class ItemDenormalizer extends AbstractItemNormalizer
{
use ItemNormalizerTrait;

public function __construct(
PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
PropertyMetadataFactoryInterface $propertyMetadataFactory,
IriConverterInterface $iriConverter,
ResourceClassResolverInterface $resourceClassResolver,
?PropertyAccessorInterface $propertyAccessor = null,
?NameConverterInterface $nameConverter = null,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
array $defaultContext = [],
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
?ResourceAccessCheckerInterface $resourceAccessChecker = null,
protected ?TagCollectorInterface $tagCollector = null,
) {
parent::__construct(
$propertyNameCollectionFactory,
$propertyMetadataFactory,
$iriConverter,
$resourceClassResolver,
$propertyAccessor,
$nameConverter,
$classMetadataFactory,
$defaultContext,
$resourceMetadataCollectionFactory,
$resourceAccessChecker,
$tagCollector
);
}

/**
* {@inheritdoc}
*
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
// Avoid issues with proxies if we populated the object
if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) {
if (true !== ($context['api_allow_update'] ?? true)) {
throw new NotNormalizableValueException('Update is not allowed for this operation.');
}

$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri(
$data['data']['id'],
$context + ['fetch_data' => false]
);
}

// Merge attributes and relationships, into format expected by the parent normalizer
$dataToDenormalize = array_merge(
$data['data']['attributes'] ?? [],
$data['data']['relationships'] ?? []
);

return parent::denormalize(
$dataToDenormalize,
$type,
$format,
$context
);
}
}
13 changes: 3 additions & 10 deletions src/JsonApi/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\TypeHelper;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use ApiPlatform\Serializer\CacheKeyTrait;
use ApiPlatform\Serializer\ContextTrait;
use ApiPlatform\Serializer\TagCollectorInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand All @@ -48,16 +45,12 @@
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*
* @todo Denormalization methods should be deprecated in 5.x, use ItemDenormalizer instead
*/
final class ItemNormalizer extends AbstractItemNormalizer
{
use CacheKeyTrait;
use ClassInfoTrait;
use ContextTrait;

public const FORMAT = 'jsonapi';

private array $componentsCache = [];
use ItemNormalizerTrait;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null)
{
Expand Down
Loading
Loading