Skip to content

Commit e84d434

Browse files
committed
fix(state): create PUT has relation with lazy mapper
1 parent 70611b3 commit e84d434

File tree

11 files changed

+141
-40
lines changed

11 files changed

+141
-40
lines changed

src/Doctrine/Common/State/PersistProcessor.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
5050
// https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555
5151
if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true)) {
5252
\assert(method_exists($manager, 'getReference'));
53+
5354
$newData = $data;
5455
$identifiers = array_reverse($uriVariables);
5556
$links = $this->getLinks($class, $operation, $context);
@@ -78,6 +79,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
7879
$reflectionProperty->setValue($newData, $newValue);
7980
}
8081
}
82+
// We create a new entity through PUT
8183
} else {
8284
foreach (array_reverse($links) as $link) {
8385
if ($link->getExpandedValue() || !$link->getFromClass()) {
@@ -92,6 +94,45 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
9294
$reflectionProperty->setValue($newData, $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null));
9395
}
9496
}
97+
98+
if (\PHP_VERSION_ID > 80400) {
99+
$classMetadata = $manager->getClassMetadata($class);
100+
foreach ($reflectionProperties as $propertyName => $reflectionProperty) {
101+
if ($classMetadata->isIdentifier($propertyName)) {
102+
continue;
103+
}
104+
105+
$value = $reflectionProperty->getValue($newData);
106+
107+
if (!\is_object($value)) {
108+
continue;
109+
}
110+
111+
if (
112+
!($relManager = $this->managerRegistry->getManagerForClass($class = $this->getObjectClass($value)))
113+
|| $relManager->contains($value)
114+
) {
115+
continue;
116+
}
117+
118+
$r = new \ReflectionClass($value);
119+
if ($r->isUninitializedLazyObject($value)) {
120+
$r->initializeLazyObject($value);
121+
}
122+
123+
$metadata = $manager->getClassMetadata($class);
124+
$identifiers = $metadata->getIdentifierValues($value);
125+
126+
// Do not get reference for partial objects or objects with null identifiers
127+
if (!$identifiers || \count($identifiers) !== \count(array_filter($identifiers, static fn ($v) => null !== $v))) {
128+
continue;
129+
}
130+
131+
\assert(method_exists($relManager, 'getReference'));
132+
133+
$reflectionProperty->setValue($newData, $relManager->getReference($class, $identifiers));
134+
}
135+
}
95136
}
96137

97138
$data = $newData;

src/State/ObjectMapper/ObjectMapper.php

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313

1414
namespace ApiPlatform\State\ObjectMapper;
1515

16-
use ApiPlatform\Metadata\Exception\RuntimeException;
1716
use Symfony\Component\ObjectMapper\ObjectMapperAwareInterface;
1817
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
1918

20-
final class ObjectMapper implements ObjectMapperInterface, ClearObjectMapInterface, ObjectMapperAwareInterface
19+
final class ObjectMapper implements ObjectMapperInterface, ClearObjectMapInterface
2120
{
2221
private ?\SplObjectStorage $objectMap = null;
2322

@@ -34,6 +33,10 @@ public function __construct(private ObjectMapperInterface $decorated)
3433

3534
public function map(object $source, object|string|null $target = null): object
3635
{
36+
if (\PHP_VERSION_ID >= 80400) {
37+
return $this->decorated->map($source, $target);
38+
}
39+
3740
if (!\is_object($target) && isset($this->objectMap[$source])) {
3841
$target = $this->objectMap[$source];
3942
}
@@ -49,15 +52,4 @@ public function clearObjectMap(): void
4952
$this->objectMap->detach($k);
5053
}
5154
}
52-
53-
public function withObjectMapper(ObjectMapperInterface $objectMapper): static
54-
{
55-
if (!$this->decorated instanceof ObjectMapperAwareInterface) {
56-
throw new RuntimeException(\sprintf('Given object mapper "%s" does not implements %s.', get_debug_type($this->decorated), ObjectMapperAwareInterface::class));
57-
}
58-
59-
$this->decorated->withObjectMapper($objectMapper);
60-
61-
return $this;
62-
}
6355
}

src/State/Processor/ObjectMapperProcessor.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
namespace ApiPlatform\State\Processor;
1515

1616
use ApiPlatform\Metadata\Operation;
17-
use ApiPlatform\State\ObjectMapper\ClearObjectMapInterface;
1817
use ApiPlatform\State\ProcessorInterface;
1918
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
2020
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
2121

2222
/**
@@ -30,7 +30,12 @@ final class ObjectMapperProcessor implements ProcessorInterface
3030
public function __construct(
3131
private readonly ?ObjectMapperInterface $objectMapper,
3232
private readonly ProcessorInterface $decorated,
33+
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null,
3334
) {
35+
// TODO: 4.3 add this deprecation
36+
// if (!$objectMapperMetadata) {
37+
// trigger_deprecation('api-platform/state', '4.3', 'Not injecting "%s" in "%s" will not be possible anymore in 4.4.', ObjectMapperMetadataFactoryInterface::class, __CLASS__);
38+
// }
3439
}
3540

3641
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
@@ -40,17 +45,29 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
4045
|| !$operation->canWrite()
4146
|| null === $data
4247
|| !is_a($data, $operation->getClass(), true)
43-
|| !(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)
4448
) {
4549
return $this->decorated->process($data, $operation, $uriVariables, $context);
4650
}
4751

48-
$data = $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context), $operation->getClass());
49-
50-
if ($this->objectMapper instanceof ClearObjectMapInterface) {
51-
$this->objectMapper->clearObjectMap();
52+
if ($this->objectMapperMetadata) {
53+
if (!$this->objectMapperMetadata->create($data)) {
54+
return $this->decorated->process($data, $operation, $uriVariables, $context);
55+
}
56+
} elseif (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)) {
57+
return $this->decorated->process($data, $operation, $uriVariables, $context);
5258
}
5359

54-
return $data;
60+
// return the Resource representation of the persisted entity
61+
return $this->objectMapper->map(
62+
// persist the entity
63+
$this->decorated->process(
64+
// maps the Resource to an Entity
65+
$this->objectMapper->map($data),
66+
$operation,
67+
$uriVariables,
68+
$context,
69+
),
70+
$operation->getClass()
71+
);
5572
}
5673
}

src/State/Provider/ObjectMapperProvider.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\State\Pagination\PaginatorInterface;
2222
use ApiPlatform\State\ProviderInterface;
2323
use Symfony\Component\ObjectMapper\Attribute\Map;
24+
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
2425
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
2526

2627
/**
@@ -36,7 +37,12 @@ final class ObjectMapperProvider implements ProviderInterface
3637
public function __construct(
3738
private readonly ?ObjectMapperInterface $objectMapper,
3839
private readonly ProviderInterface $decorated,
40+
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null,
3941
) {
42+
// TODO: 4.3 add this deprecation
43+
// if (!$objectMapperMetadata) {
44+
// trigger_deprecation('api-platform/state', '4.3', 'Not injecting "%s" in "%s" will not be possible anymore in 4.4.', ObjectMapperMetadataFactoryInterface::class, __CLASS__);
45+
// }
4046
}
4147

4248
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
@@ -59,7 +65,22 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5965

6066
$entityClass ??= $data::class;
6167

62-
if (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class) && !(new \ReflectionClass($entityClass))->getAttributes(Map::class)) {
68+
if ($this->objectMapperMetadata) {
69+
if (!class_exists($operation->getClass()) || !class_exists($entityClass)) {
70+
return $data;
71+
}
72+
73+
$operationData = new \ReflectionClass($operation->getClass());
74+
$entityData = new \ReflectionClass($entityClass);
75+
76+
if ($entityData->isAbstract() || $operationData->isAbstract()) {
77+
return $data;
78+
}
79+
80+
if (!$this->objectMapperMetadata->create($operationData->newInstanceWithoutConstructor()) && !$this->objectMapperMetadata->create($entityData->newInstanceWithoutConstructor())) {
81+
return $data;
82+
}
83+
} elseif (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class) && !(new \ReflectionClass($entityClass))->getAttributes(Map::class)) {
6384
return $data;
6485
}
6586

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
service('api_platform.resource_class_resolver'),
2424
service('api_platform.property_accessor'),
2525
service('api_platform.object_mapper')->nullOnInvalid(),
26+
service('api_platform.object_mapper.metadata_factory')->nullOnInvalid(),
2627
])
2728
->tag('doctrine.event_listener', ['event' => 'preUpdate'])
2829
->tag('doctrine.event_listener', ['event' => 'onFlush'])

src/Symfony/Bundle/Resources/config/state/object_mapper.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@
3535
->args([
3636
service('api_platform.object_mapper')->nullOnInvalid(),
3737
service('api_platform.state_provider.object_mapper.inner'),
38+
service('api_platform.object_mapper.metadata_factory'),
3839
]);
3940

4041
$services->set('api_platform.state_processor.object_mapper', 'ApiPlatform\State\Processor\ObjectMapperProcessor')
4142
->decorate('api_platform.state_processor.locator', null, 0)
4243
->args([
4344
service('api_platform.object_mapper')->nullOnInvalid(),
4445
service('api_platform.state_processor.object_mapper.inner'),
46+
service('api_platform.object_mapper.metadata_factory'),
4547
]);
4648
};

src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Doctrine\ORM\Mapping\AssociationMapping;
2828
use Doctrine\ORM\PersistentCollection;
2929
use Symfony\Component\ObjectMapper\Attribute\Map;
30+
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
3031
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
3132
use Symfony\Component\PropertyAccess\PropertyAccess;
3233
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -48,7 +49,8 @@ public function __construct(private readonly PurgerInterface $purger,
4849
private readonly IriConverterInterface $iriConverter,
4950
private readonly ResourceClassResolverInterface $resourceClassResolver,
5051
?PropertyAccessorInterface $propertyAccessor = null,
51-
private readonly ?ObjectMapperInterface $objectMapper = null)
52+
private readonly ?ObjectMapperInterface $objectMapper = null,
53+
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null)
5254
{
5355
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
5456
}
@@ -153,13 +155,21 @@ private function gatherRelationTags(EntityManagerInterface $em, object $entity):
153155
if (
154156
\is_array($associationMapping)
155157
&& \array_key_exists('targetEntity', $associationMapping)
156-
&& !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])
157-
&& (
158-
!$this->objectMapper
159-
|| !(new \ReflectionClass($associationMapping['targetEntity']))->getAttributes(Map::class)
160-
)
158+
&& ($targetEntity = $associationMapping['targetEntity'])
159+
&& !$this->resourceClassResolver->isResourceClass($targetEntity)
161160
) {
162-
return;
161+
if (!$this->objectMapper) {
162+
return;
163+
}
164+
165+
$targetRefl = new \ReflectionClass($targetEntity);
166+
if ($this->objectMapperMetadata) {
167+
if (!$this->objectMapperMetadata->create($targetRefl->newInstanceWithoutConstructor())) {
168+
return;
169+
}
170+
} elseif (!$targetRefl->getAttributes(Map::class)) {
171+
return;
172+
}
163173
}
164174

165175
$this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
@@ -210,17 +220,30 @@ private function getResourcesForEntity(object $entity): array
210220
return [];
211221
}
212222

213-
$mapAttributes = (new \ReflectionClass($class))->getAttributes(Map::class);
223+
if ($this->objectMapperMetadata) {
224+
$mappings = $this->objectMapperMetadata->create($entity);
214225

215-
if (!$mapAttributes) {
216-
return [];
217-
}
226+
if (!$mappings) {
227+
return [];
228+
}
229+
230+
$resources = array_map(
231+
fn ($mapping) => $this->objectMapper->map($entity, $mapping->target),
232+
$mappings
233+
);
234+
} else {
235+
$mapAttributes = (new \ReflectionClass($class))->getAttributes(Map::class);
218236

219-
// loop over all mappings to fetch all resources mapped to this entity
220-
$resources = array_map(
221-
fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
222-
$mapAttributes
223-
);
237+
if (!$mapAttributes) {
238+
return [];
239+
}
240+
241+
// loop over all mappings to fetch all resources mapped to this entity
242+
$resources = array_map(
243+
fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
244+
$mapAttributes
245+
);
246+
}
224247
} else {
225248
$resources[] = $entity;
226249
}

tests/Fixtures/TestBundle/ApiResource/MappedResourceWithRelationRelated.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
#[Map(target: MappedResourceWithRelationRelatedEntity::class)]
3333
class MappedResourceWithRelationRelated
3434
{
35-
#[Map(if: false)]
3635
public string $id;
37-
3836
public string $name;
3937
}

tests/Fixtures/TestBundle/Entity/MappedResourceWithRelationEntity.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
class MappedResourceWithRelationEntity
2323
{
2424
#[ORM\Id, ORM\Column]
25+
#[Map(transform: 'to_string')]
2526
private ?int $id = null;
2627

2728
#[ORM\ManyToOne(targetEntity: MappedResourceWithRelationRelatedEntity::class)]

tests/Fixtures/TestBundle/Entity/MappedResourceWithRelationRelatedEntity.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public function getId(): ?int
3131
{
3232
return $this->id;
3333
}
34+
35+
public function setId(?int $id = null)
36+
{
37+
$this->id = $id;
38+
}
3439
}

0 commit comments

Comments
 (0)