Skip to content

Commit 0da5bef

Browse files
committed
Allow to directly use IRIs for attachmennts without the need to rely on _type
1 parent 8787423 commit 0da5bef

2 files changed

Lines changed: 63 additions & 6 deletions

File tree

src/Serializer/APIPlatform/SkippableItemNormalizer.php

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@
2323

2424
namespace App\Serializer\APIPlatform;
2525

26+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
27+
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
28+
use ApiPlatform\Metadata\IriConverterInterface;
2629
use ApiPlatform\Serializer\ItemNormalizer;
2730
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
28-
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
31+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
32+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
2933
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
3034
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
3135
use Symfony\Component\Serializer\SerializerAwareInterface;
@@ -35,20 +39,49 @@
3539
* This class decorates API Platform's ItemNormalizer to allow skipping the normalization process by setting the
3640
* DISABLE_ITEM_NORMALIZER context key to true. This is useful for all kind of serialization operations, where the API
3741
* Platform subsystem should not be used.
42+
*
43+
* It also works around a bug in API Platform's AbstractItemNormalizer where IRI strings for abstract resource classes
44+
* with a discriminator map fail deserialization when objectToPopulate is null (the discriminator is checked before
45+
* the IRI string check). See: https://github.com/Part-DB/Part-DB-server/issues/1370
3846
*/
3947
#[AsDecorator("api_platform.serializer.normalizer.item")]
4048
class SkippableItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
4149
{
4250

4351
public const DISABLE_ITEM_NORMALIZER = 'DISABLE_ITEM_NORMALIZER';
4452

45-
public function __construct(private readonly ItemNormalizer $inner)
46-
{
47-
53+
public function __construct(
54+
private readonly ItemNormalizer $inner,
55+
private readonly IriConverterInterface $iriConverter,
56+
) {
4857
}
4958

5059
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
5160
{
61+
// API Platform's AbstractItemNormalizer has a bug: when objectToPopulate is null and data is an IRI
62+
// string, it tries to resolve the discriminator class from [$iri_string] before reaching the IRI
63+
// check (line 271). For abstract resource classes with a discriminator map (e.g. Attachment), this
64+
// fails because the array has no _type key. Fix by resolving IRI strings directly.
65+
// See: https://github.com/Part-DB/Part-DB-server/issues/1370
66+
if (is_string($data)) {
67+
try {
68+
return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
69+
} catch (ItemNotFoundException $e) {
70+
if (false === ($context['denormalize_throw_on_relation_not_found'] ?? true)) {
71+
return null;
72+
}
73+
if (!isset($context['not_normalizable_value_exceptions'])) {
74+
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
75+
}
76+
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [$type], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
77+
} catch (InvalidArgumentException $e) {
78+
if (!isset($context['not_normalizable_value_exceptions'])) {
79+
throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
80+
}
81+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Invalid IRI "%s".', $data), $data, [$type], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
82+
}
83+
}
84+
5285
return $this->inner->denormalize($data, $type, $format, $context);
5386
}
5487

@@ -87,4 +120,4 @@ public function getSupportedTypes(?string $format): array
87120
'object' => false
88121
];
89122
}
90-
}
123+
}

tests/API/Endpoints/PartEndpointTest.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,28 @@ public function testDeleteItem(): void
6969
{
7070
$this->_testDeleteItem(1);
7171
}
72-
}
72+
73+
public function testAttachmentPatchWithIRI(): void
74+
{
75+
$client = static::createAuthenticatedClient();
76+
77+
// Create a new attachment with a picture URL for Part 1
78+
$response = $client->request('POST', '/api/attachments', ['json' => [
79+
'name' => 'Test Picture',
80+
'url' => 'http://example.com/test.jpg',
81+
'_type' => 'Part',
82+
'element' => '/api/parts/1',
83+
'attachment_type' => '/api/attachment_types/1',
84+
]]);
85+
self::assertResponseIsSuccessful();
86+
$attachmentIri = $response->toArray()['@id'];
87+
88+
// Now PATCH Part 1 to set master_picture_attachment
89+
$client->request('PATCH', '/api/parts/1', [
90+
'json' => ['master_picture_attachment' => $attachmentIri],
91+
'headers' => ['Content-Type' => 'application/merge-patch+json'],
92+
]);
93+
self::assertResponseIsSuccessful();
94+
self::assertJsonContains(['master_picture_attachment' => ['@id' => $attachmentIri]]);
95+
}
96+
}

0 commit comments

Comments
 (0)