Skip to content

Commit d282cd4

Browse files
committed
mhh
1 parent 4dc9541 commit d282cd4

File tree

10 files changed

+58
-9
lines changed

10 files changed

+58
-9
lines changed

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use ApiPlatform\Metadata\Util\ClassInfoTrait;
2929
use ApiPlatform\Serializer\AbstractItemNormalizer;
3030
use ApiPlatform\Serializer\ContextTrait;
31+
use ApiPlatform\Serializer\OperationResourceResolverInterface;
3132
use ApiPlatform\Serializer\TagCollectorInterface;
3233
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
3334
use Symfony\Component\Serializer\Exception\LogicException;
@@ -71,9 +72,9 @@ final class ItemNormalizer extends AbstractItemNormalizer
7172
'@vocab',
7273
];
7374

74-
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null, private ?OperationMetadataFactoryInterface $operationMetadataFactory = null)
75+
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null, private ?OperationMetadataFactoryInterface $operationMetadataFactory = null, ?OperationResourceResolverInterface $operationResourceResolver = null)
7576
{
76-
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector);
77+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector, $operationResourceResolver);
7778
}
7879

7980
/**
@@ -109,11 +110,12 @@ public function normalize(mixed $data, ?string $format = null, array $context =
109110
// from the output class (not from the item_uri_template operation).
110111
$itemUriTemplate = $context['item_uri_template'];
111112
unset($context['item_uri_template']);
113+
$originalData = $data;
112114
$data = parent::normalize($data, $format, $context);
113115
if (\is_array($data)) {
114116
try {
115117
$context['item_uri_template'] = $itemUriTemplate;
116-
$data['@id'] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, null, $context);
118+
$data['@id'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, null, $context);
117119
} catch (\Exception) {
118120
}
119121
}
@@ -139,7 +141,14 @@ public function normalize(mixed $data, ?string $format = null, array $context =
139141
}
140142

141143
if (isset($context['item_uri_template']) && $this->operationMetadataFactory) {
142-
$context['output']['operation'] = $this->operationMetadataFactory->create($context['item_uri_template']);
144+
$itemOp = $this->operationMetadataFactory->create($context['item_uri_template']);
145+
// Use resource-level shortName for @type, not operation-specific shortName
146+
try {
147+
$itemResourceShortName = $this->resourceMetadataCollectionFactory->create($itemOp->getClass())[0]->getShortName();
148+
$context['output']['operation'] = $itemOp->withShortName($itemResourceShortName);
149+
} catch (\Exception) {
150+
$context['output']['operation'] = $itemOp;
151+
}
143152
} elseif ($this->resourceClassResolver->isResourceClass($resourceClass)) {
144153
$context['output']['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
145154
}
@@ -178,7 +187,13 @@ public function normalize(mixed $data, ?string $format = null, array $context =
178187
if (!isset($metadata['@type']) && $operation) {
179188
$types = $operation instanceof HttpOperation ? $operation->getTypes() : null;
180189
if (null === $types) {
181-
$types = [$operation->getShortName()];
190+
// Use resource-level shortName to avoid operation-specific overrides
191+
$typeClass = $isResourceClass ? $resourceClass : ($operation->getClass() ?? $resourceClass);
192+
try {
193+
$types = [$this->resourceMetadataCollectionFactory->create($typeClass)[0]->getShortName()];
194+
} catch (\Exception) {
195+
$types = [$operation->getShortName()];
196+
}
182197
}
183198
$metadata['@type'] = 1 === \count($types) ? $types[0] : $types;
184199
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,9 @@ public function register(): void
976976
$defaultContext,
977977
$app->make(ResourceAccessCheckerInterface::class),
978978
// $app->make(TagCollectorInterface::class)
979+
null,
980+
null,
981+
$app->make(OperationResourceResolverInterface::class),
979982
);
980983
});
981984

src/Serializer/AbstractItemNormalizer.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
7676
protected array $localFactoryOptionsCache = [];
7777
protected ?ResourceAccessCheckerInterface $resourceAccessChecker;
7878

79-
public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected IriConverterInterface $iriConverter, protected ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null)
79+
public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected IriConverterInterface $iriConverter, protected ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null, protected ?OperationResourceResolverInterface $operationResourceResolver = null)
8080
{
8181
if (!isset($defaultContext['circular_reference_handler'])) {
8282
$defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
@@ -97,7 +97,21 @@ public function supportsNormalization(mixed $data, ?string $format = null, array
9797
return false;
9898
}
9999

100-
$class = $context['force_resource_class'] ?? $this->getObjectClass($data);
100+
$class = $this->getObjectClass($data);
101+
102+
// Only honor force_resource_class if the resolver confirms this object
103+
// maps to the operation's resource class (prevents context leakage to
104+
// unrelated objects like DateTimeImmutable)
105+
if (isset($context['force_resource_class']) && $context['force_resource_class'] !== $class) {
106+
$operation = $context['operation'] ?? $context['root_operation'] ?? null;
107+
if ($operation && $this->operationResourceResolver) {
108+
$resolvedClass = $this->operationResourceResolver->resolve($data, $operation);
109+
if ($resolvedClass !== $class) {
110+
$class = $context['force_resource_class'];
111+
}
112+
}
113+
}
114+
101115
if (($context['output']['class'] ?? null) === $class) {
102116
return true;
103117
}
@@ -123,6 +137,8 @@ public function getSupportedTypes(?string $format): array
123137
public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
124138
{
125139
$resourceClass = $context['force_resource_class'] ?? $this->getObjectClass($data);
140+
// Prevent force_resource_class from leaking to child property normalizations
141+
unset($context['force_resource_class']);
126142
if ($outputClass = $this->getOutputClass($context)) {
127143
if (!$this->serializer instanceof NormalizerInterface) {
128144
throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');

src/Serializer/ItemNormalizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ class ItemNormalizer extends AbstractItemNormalizer
4141
{
4242
private readonly LoggerInterface $logger;
4343

44-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, ?LoggerInterface $logger = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, array $defaultContext = [], protected ?TagCollectorInterface $tagCollector = null)
44+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, ?LoggerInterface $logger = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, array $defaultContext = [], protected ?TagCollectorInterface $tagCollector = null, ?OperationResourceResolverInterface $operationResourceResolver = null)
4545
{
46-
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataFactory, $resourceAccessChecker, $tagCollector);
46+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataFactory, $resourceAccessChecker, $tagCollector, $operationResourceResolver);
4747

4848
$this->logger = $logger ?: new NullLogger();
4949
}

src/Symfony/Bundle/Resources/config/api.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
use ApiPlatform\Serializer\ItemNormalizer;
3333
use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory;
3434
use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader;
35+
use ApiPlatform\Serializer\OperationResourceResolver;
36+
use ApiPlatform\Serializer\OperationResourceResolverInterface;
3537
use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider;
3638
use ApiPlatform\Serializer\SerializerContextBuilder;
3739
use ApiPlatform\Serializer\SerializerFilterContextBuilder;
@@ -115,6 +117,9 @@
115117

116118
$services->alias(GroupFilter::class, 'api_platform.serializer.group_filter');
117119

120+
$services->set('api_platform.serializer.operation_resource_resolver', OperationResourceResolver::class);
121+
$services->alias(OperationResourceResolverInterface::class, 'api_platform.serializer.operation_resource_resolver');
122+
118123
$services->set('api_platform.serializer.normalizer.item', ItemNormalizer::class)
119124
->args([
120125
service('api_platform.metadata.property.name_collection_factory'),
@@ -129,6 +134,7 @@
129134
service('api_platform.security.resource_access_checker')->ignoreOnInvalid(),
130135
[],
131136
service('api_platform.http_cache.tag_collector')->ignoreOnInvalid(),
137+
service('api_platform.serializer.operation_resource_resolver'),
132138
])
133139
->tag('serializer.normalizer', ['priority' => -895]);
134140

src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use ApiPlatform\Doctrine\Odm\State\CollectionProvider;
3333
use ApiPlatform\Doctrine\Odm\State\ItemProvider;
3434
use ApiPlatform\Doctrine\Odm\State\LinksHandler;
35+
use ApiPlatform\Doctrine\Orm\Serializer\DoctrineOperationResourceResolver;
3536
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
3637

3738
return function (ContainerConfigurator $container) {
@@ -45,6 +46,8 @@
4546

4647
$services->set('api_platform.doctrine.metadata_factory', ClassMetadataFactory::class)->factory([service('doctrine_mongodb.odm.default_document_manager'), 'getMetadataFactory']);
4748

49+
$services->set('api_platform.serializer.operation_resource_resolver', DoctrineOperationResourceResolver::class);
50+
4851
$services->set('api_platform.doctrine_mongodb.odm.state.remove_processor', RemoveProcessor::class)
4952
->args([service('doctrine_mongodb')])
5053
->tag('api_platform.state_processor', ['priority' => -100, 'key' => 'api_platform.doctrine_mongodb.odm.state.remove_processor'])

src/Symfony/Bundle/Resources/config/doctrine_orm.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use ApiPlatform\Doctrine\Orm\Metadata\Property\DoctrineOrmPropertyMetadataFactory;
3333
use ApiPlatform\Doctrine\Orm\Metadata\Resource\DoctrineOrmLinkFactory;
3434
use ApiPlatform\Doctrine\Orm\Metadata\Resource\DoctrineOrmResourceCollectionMetadataFactory;
35+
use ApiPlatform\Doctrine\Orm\Serializer\DoctrineOperationResourceResolver;
3536
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
3637
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
3738
use ApiPlatform\Doctrine\Orm\State\LinksHandler;
@@ -42,6 +43,8 @@
4243

4344
$services->set('api_platform.doctrine.metadata_factory', ClassMetadataFactory::class)->factory([service('doctrine.orm.default_entity_manager'), 'getMetadataFactory']);
4445

46+
$services->set('api_platform.serializer.operation_resource_resolver', DoctrineOperationResourceResolver::class);
47+
4548
$services->set('api_platform.doctrine.orm.state.remove_processor', RemoveProcessor::class)
4649
->args([service('doctrine')])
4750
->tag('api_platform.state_processor', ['priority' => -100, 'key' => 'api_platform.doctrine.orm.state.remove_processor'])

src/Symfony/Bundle/Resources/config/hal.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
service('api_platform.metadata.resource.metadata_collection_factory')->ignoreOnInvalid(),
6565
service('api_platform.security.resource_access_checker')->ignoreOnInvalid(),
6666
service('api_platform.http_cache.tag_collector')->ignoreOnInvalid(),
67+
service('api_platform.serializer.operation_resource_resolver'),
6768
])
6869
->tag('serializer.normalizer', ['priority' => -890]);
6970

src/Symfony/Bundle/Resources/config/jsonapi.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
service('api_platform.metadata.resource.metadata_collection_factory'),
7373
service('api_platform.security.resource_access_checker')->ignoreOnInvalid(),
7474
service('api_platform.http_cache.tag_collector')->ignoreOnInvalid(),
75+
service('api_platform.serializer.operation_resource_resolver'),
7576
])
7677
->tag('serializer.normalizer', ['priority' => -890]);
7778

src/Symfony/Bundle/Resources/config/jsonld.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
service('api_platform.security.resource_access_checker')->ignoreOnInvalid(),
5151
service('api_platform.http_cache.tag_collector')->ignoreOnInvalid(),
5252
service('api_platform.metadata.operation.metadata_factory')->ignoreOnInvalid(),
53+
service('api_platform.serializer.operation_resource_resolver'),
5354
])
5455
->tag('serializer.normalizer', ['priority' => -890]);
5556

0 commit comments

Comments
 (0)