Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/Symfony/Bundle/ApiPlatformBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\FilterPass;
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass;
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass;
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ItemNormalizerDecorationBcPass;
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\JsonStreamerTransformerPass;
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\McpRegistryPass;
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
Expand Down Expand Up @@ -66,6 +67,8 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new MutatorPass());
$container->addCompilerPass(new McpRegistryPass());
$container->addCompilerPass(new PropertyInfoTagPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -100);
// Runs after AutowireAsDecoratorPass (100) and before FrameworkBundle's SerializerPass (0), which reads the serializer.normalizer tag.
$container->addCompilerPass(new ItemNormalizerDecorationBcPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
// Must run after Symfony's TransformerPass so we can rely on the value_object_transformer tag being processed.
$container->addCompilerPass(new JsonStreamerTransformerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?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\Symfony\Bundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

/**
* Detaches an item denormalizer from the serializer chain when a user decorator of the matching normalizer implements {@see DenormalizerInterface}.
*
* @internal
*
* @todo remove in 6.0
*/
final class ItemNormalizerDecorationBcPass implements CompilerPassInterface
{
private const PAIRS = [
'api_platform.serializer.normalizer.item' => 'api_platform.serializer.denormalizer.item',
'api_platform.jsonld.normalizer.item' => 'api_platform.jsonld.denormalizer.item',
'api_platform.jsonapi.normalizer.item' => 'api_platform.jsonapi.denormalizer.item',
'api_platform.graphql.normalizer.item' => 'api_platform.graphql.denormalizer.item',
];

public function process(ContainerBuilder $container): void
{
foreach (self::PAIRS as $normalizerId => $denormalizerId) {
if (!$container->hasDefinition($normalizerId) || !$container->hasDefinition($denormalizerId)) {
continue;
}

$decorators = $this->findUserDecorators($container, $normalizerId);
if ([] === $decorators || [] !== $this->findUserDecorators($container, $denormalizerId, false)) {
continue;
}

$container->getDefinition($denormalizerId)->clearTag('serializer.normalizer');

foreach ($decorators as $id) {
trigger_deprecation('api-platform/core', '4.4', 'Service "%s" decorates "%s" and implements "%s": denormalization is routed through the decorated normalizer for backward compatibility. Decorate "%s" instead.', $id, $normalizerId, DenormalizerInterface::class, $denormalizerId);
}
}
}

/**
* @return list<string>
*/
private function findUserDecorators(ContainerBuilder $container, string $decoratedId, bool $onlyDenormalizers = true): array
{
$ids = [];
foreach ($container->getDefinitions() as $id => $definition) {
if (str_starts_with((string) $id, 'api_platform.') || ($definition->getDecoratedService()[0] ?? null) !== $decoratedId) {
continue;
}

if ($onlyDenormalizers && !$this->implementsDenormalizer($container, $definition->getClass())) {
continue;
}

$ids[] = $id;
}

return $ids;
}

private function implementsDenormalizer(ContainerBuilder $container, ?string $class): bool
{
if (null === $class) {
return false;
}

$class = $container->getParameterBag()->resolveValue($class);

return \is_string($class) && true === $container->getReflectionClass($class, false)?->implementsInterface(DenormalizerInterface::class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\Tests\Fixtures\TestBundle\Serializer\Decorator;

use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ValidateOnce;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

#[AsDecorator('api_platform.jsonld.normalizer.item')]
final class DenormalizingJsonLdItemNormalizerDecorator implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
public function __construct(#[AutowireDecorated] private readonly NormalizerInterface&DenormalizerInterface&SerializerAwareInterface $inner)
{
}

public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
return $this->inner->normalize($data, $format, $context);
}

public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $this->inner->supportsNormalization($data, $format, $context);
}

public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
$object = $this->inner->denormalize($data, $type, $format, $context);

if ($object instanceof ValidateOnce) {
$object->name = 'decorated';
}

return $object;
}

public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return $this->inner->supportsDenormalization($data, $type, $format, $context);
}

public function setSerializer(SerializerInterface $serializer): void
{
$this->inner->setSerializer($serializer);
}

public function getSupportedTypes(?string $format): array
{
return $this->inner->getSupportedTypes($format);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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\Tests\Fixtures\TestBundle\Serializer\Decorator;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class NormalizeOnlyItemNormalizerDecorator implements NormalizerInterface
{
public function __construct(private readonly NormalizerInterface $inner)
{
}

public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
return $this->inner->normalize($data, $format, $context);
}

public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $this->inner->supportsNormalization($data, $format, $context);
}

public function getSupportedTypes(?string $format): array
{
return $this->inner->getSupportedTypes($format);
}
}
80 changes: 80 additions & 0 deletions tests/Functional/DecoratedItemNormalizerDenormalizationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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\Tests\Functional;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ValidateOnce;
use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Decorator\DenormalizingJsonLdItemNormalizerDecorator;
use ApiPlatform\Tests\SetupClassResourcesTrait;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class DecoratedItemNormalizerAppKernel extends \AppKernel
{
public function getCacheDir(): string
{
return parent::getCacheDir().'/decorated_item_normalizer';
}

public function getLogDir(): string
{
return parent::getLogDir().'/decorated_item_normalizer';
}

protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void
{
parent::configureContainer($c, $loader);

$loader->load(static function (ContainerBuilder $container): void {
$container->register(DenormalizingJsonLdItemNormalizerDecorator::class)
->setAutowired(true)
->setAutoconfigured(true);
});
}
}

final class DecoratedItemNormalizerDenormalizationTest extends ApiTestCase
{
use SetupClassResourcesTrait;

protected static ?bool $alwaysBootKernel = false;

/**
* @return class-string[]
*/
public static function getResources(): array
{
return [ValidateOnce::class];
}

protected static function getKernelClass(): string
{
return DecoratedItemNormalizerAppKernel::class;
}

#[IgnoreDeprecations]
public function testDecoratorOfTheItemNormalizerStillDenormalizes(): void
{
$this->expectUserDeprecationMessage('Since api-platform/core 4.4: Calling "denormalize()" on "ApiPlatform\JsonLd\Serializer\ItemNormalizer" is deprecated, use "ApiPlatform\JsonLd\Serializer\ItemDenormalizer" instead.');

$response = self::createClient()->request('POST', '/validate_once', [
'headers' => ['Content-Type' => 'application/ld+json', 'Accept' => 'application/ld+json'],
'json' => ['name' => 'original'],
]);

$this->assertResponseStatusCodeSame(201);
$this->assertSame('decorated', $response->toArray()['name']);
}
}
Loading
Loading