Skip to content

Instantly share code, notes, and snippets.

@benjaminrau
Last active December 13, 2019 20:44
Show Gist options
  • Save benjaminrau/afdf2c53ac8f77346bbc6b66b2e10c2e to your computer and use it in GitHub Desktop.
Save benjaminrau/afdf2c53ac8f77346bbc6b66b2e10c2e to your computer and use it in GitHub Desktop.
Allow to have different serialization groups for root resource and embedded relations
<?php
namespace Emma\AppBundle\Serializer;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Copy from \ApiPlatform\Core\JsonLd\Serializer\ItemJsonNormalizer
*/
class ItemJsonNormalizer implements NormalizerInterface, DenormalizerInterface
{
use ClassInfoTrait;
private const ENTRY_POINT_OBJECT_GROUP_KEY = '_entry_point_object';
private const NOT_ENTRY_POINT_OBJECT_GROUP_KEY = 'not' . self::ENTRY_POINT_OBJECT_GROUP_KEY;
/**
* @var NormalizerInterface
*/
private $normalizer;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* CustomItemNormalizer constructor.
*
* @param $normalizer NormalizerInterface
* @param $eventDispatcher EventDispatcherInterface
*/
public function __construct(NormalizerInterface $normalizer, EventDispatcherInterface $eventDispatcher)
{
if (!$normalizer instanceof DenormalizerInterface) {
throw new \InvalidArgumentException('The normalizer must implement the DenormalizerInterface');
}
$this->normalizer = $normalizer;
$this->eventDispatcher = $eventDispatcher;
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
return $this->normalizer->denormalize($data, $class, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return $this->normalizer->supportsDenormalization($data, $type, $format);
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
$this->appendEntryPointObjectGroups($context);
$context['cache_key'] = md5(json_encode($context['groups']));
if (!empty($context['parent_property']) && isset($context['property_path'])) {
$context['property_path'][] = explode('::', $context['parent_property'])[1];
} elseif (!empty($context['parent_property'])) {
$context['property_path'] = [explode('::', $context['parent_property'])[1]];
}
$data = $this->normalizer->normalize($object, $format, $context);
return $data;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $this->normalizer->supportsNormalization($data, $format);
}
/**
* @param $context
*/
private function appendEntryPointObjectGroups(&$context)
{
if ($context['operation_type'] === 'subresource') {
return;
}
if (!isset($context['parent_property'])) {
foreach ($context['groups'] as $group) {
$context['groups'][] = $group . self::ENTRY_POINT_OBJECT_GROUP_KEY;
}
} else {
foreach ($context['groups'] as $key => $group) {
if (strpos($group, self::ENTRY_POINT_OBJECT_GROUP_KEY) !== false) {
unset($context['groups'][$key]);
}
if (strpos($group, self::NOT_ENTRY_POINT_OBJECT_GROUP_KEY) === false) {
$context['groups'][] = $group . self::NOT_ENTRY_POINT_OBJECT_GROUP_KEY;
}
}
}
}
}
<?php
declare(strict_types=1);
namespace Emma\AppBundle\Serializer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use ApiPlatform\Core\JsonLd\Serializer\JsonLdContextTrait;
use ApiPlatform\Core\Serializer\ContextTrait;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Copy from \ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer
*/
final class ItemNormalizer extends AbstractItemNormalizer
{
use ClassInfoTrait;
use ContextTrait;
use JsonLdContextTrait;
public const FORMAT = 'jsonld';
private $contextBuilder;
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = [])
{
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory);
$this->contextBuilder = $contextBuilder;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null): bool
{
return self::FORMAT === $format && parent::supportsNormalization($data, $format);
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = [])
{
if (null !== $this->getOutputClass($this->getObjectClass($object), $context)) {
return parent::normalize($object, $format, $context);
}
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null);
$context = $this->initContext($resourceClass, $context);
$iri = $this->iriConverter->getIriFromItem($object);
$context['iri'] = $iri;
$context['api_normalize'] = true;
$metadata = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
$context['resource_class'] = $resourceClass;
$data = parent::normalize($object, $format, $context);
if (!\is_array($data)) {
return $data;
}
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
$metadata['@id'] = $iri;
$metadata['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName();
return $metadata + $data;
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null): bool
{
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format);
}
/**
* {@inheritdoc}
*
* @throws NotNormalizableValueException
*/
public function denormalize($data, $class, $format = null, array $context = [])
{
// Avoid issues with proxies if we populated the object
if (isset($data['@id']) && !isset($context[self::OBJECT_TO_POPULATE])) {
if (true !== ($context['api_allow_update'] ?? true)) {
throw new NotNormalizableValueException('Update is not allowed for this operation.');
}
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri($data['@id'], $context + ['fetch_data' => true]);
}
return parent::denormalize($data, $class, $format, $context);
}
/**
* {@inheritdoc}
*
* @throws NoSuchPropertyException
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
{
$context['api_attribute'] = $attribute;
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
try {
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
} catch (NoSuchPropertyException $e) {
if (!$propertyMetadata->hasChildInherited()) {
throw $e;
}
$attributeValue = null;
}
$type = $propertyMetadata->getType();
if (
is_iterable($attributeValue) &&
$type &&
$type->isCollection() &&
($collectionValueType = $type->getCollectionValueType()) &&
($className = $collectionValueType->getClassName()) &&
$this->resourceClassResolver->isResourceClass($className)
) {
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
$childContext = $this->createChildContext($context, $attribute, $format);
$childContext['resource_class'] = $resourceClass;
unset($childContext['iri']);
// LOCC START
$context['parent_property'] = $this->getObjectClass($object) . '::'.$attribute;
// LOCC END
return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
}
if (
$type &&
($className = $type->getClassName()) &&
$this->resourceClassResolver->isResourceClass($className)
) {
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
$childContext = $this->createChildContext($context, $attribute, $format);
$childContext['resource_class'] = $resourceClass;
unset($childContext['iri']);
// LOCC START
$context['parent_property'] = $this->getObjectClass($object) . '::'.$attribute;
// LOCC END
return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
}
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
}
unset($context['resource_class']);
return $this->serializer->normalize($attributeValue, $format, $context);
}
/**
* @inheritDoc
*/
protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, ?string $format, array $context): array
{
return array_values(
parent::normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $context)
);
}
}
api.serializer.normalizer.item:
public: false
class: Emma\AppBundle\Serializer\ItemNormalizer
arguments: [ '@api.serializer.normalizer.item.json', '@event_dispatcher']
tags: [ { name: serializer.normalizer, priority: 9 } ]
api.serializer.normalizer.item.json:
public: false
class: Emma\AppBundle\Serializer\ItemJsonNormalizer
arguments:
- '@api_platform.metadata.resource.metadata_factory'
- '@api_platform.metadata.property.name_collection_factory'
- '@api_platform.metadata.property.metadata_factory'
- '@api_platform.iri_converter'
- '@api_platform.resource_class_resolver'
- '@api_platform.jsonld.context_builder'
- '@api_platform.property_accessor'
- ~
- '@serializer.mapping.class_metadata_factory'
tags: [ { name: serializer.normalizer, priority: 8 } ]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment