So, I think I found the cause of my problem.
Everything is in the ApiPlatform\JsonLd\Serializer\ItemNormalizer.
Indeed, since v2.7 (or 3.0) the @id parameter is generated thanks to the iriConverter with the context operation passed to the method.
The operation is a Patch operation and the iriConverter take the input iri as @id.
To fix this, I made a custom normalizer which decorates the JsonLd one. Like this :
<?php
namespace App\Normalizer;
use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class CustomNormalizer implements NormalizerInterface
{
private NormalizerInterface $decorated;
public function __construct(private IriConverterInterface $iriConverter, private NormalizerInterface $decorated)
{
}
public function normalize($object, ?string $format = null, array $context = []): null|array|\ArrayObject|bool|float|int|string
{
$normalizedData = $this->decorated->normalize($object, $format, $context);
if (self::isCustomOperation($normalizedData, $context)) {
$normalizedData = $this->regenerateIdWithIriFromGetOperation($object, $context, $normalizedData);
}
return $normalizedData;
}
public function supportsNormalization($data, ?string $format = null): bool
{
return $this->decorated->supportsNormalization($data, $format);
}
private function regenerateIdWithIriFromGetOperation($object, array $context, array $normalizedData): array
{
// We force the converter to use the GET operation instead of the called operation to generate the iri
$iri = $this->iriConverter->getIriFromResource($object, context: $context);
$normalizedData['@id'] = $iri;
return $normalizedData;
}
private static function isCustomOperation($normalizedData, array $context): bool
{
if (!is_array($normalizedData) || !array_key_exists('@id', $normalizedData)) {
return false;
}
if (!($context['operation'] ?? null) instanceof Operation) {
return false;
}
$extraProps = $context['operation']->getExtraProperties();
return $extraProps['custom_operation'] ?? false;
}
}
Then, I added the custom normalizer in the YAML configuration (config/services.yaml) :
App\Normalizer\CustomNormalizer:
decorates: "api_platform.jsonld.normalizer.item"
And for the given operations, I added an extra property to only activate the custom normalizer on specific operations:
#[Patch(
uriTemplate: '/o_ts/{id}/reactivated',
uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])],
controller: OTReactivated::class,
openapiContext: ['summary' => 'Work Order reactivated'],
securityPostDenormalize: 'is_granted("OT", object)',
securityPostDenormalizeMessage: "Vous n'avez pas l'accès à cette ressource.",
name: 'api_o_ts_reactivated',
extraProperties: ['custom_operation' => true],
)]
With that, the @id returned is the correct one!