Skip to content

Commit d06b1a0

Browse files
authored
fix(state): object-mapper reuse related entity (#7300)
1 parent 02a7649 commit d06b1a0

File tree

10 files changed

+323
-8
lines changed

10 files changed

+323
-8
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@
181181
"symfony/maker-bundle": "^1.24",
182182
"symfony/mercure-bundle": "*",
183183
"symfony/messenger": "^6.4 || ^7.0",
184-
"symfony/object-mapper": "^7.3",
184+
"symfony/object-mapper": "7.4.x-dev",
185185
"symfony/routing": "^6.4 || ^7.0",
186186
"symfony/security-bundle": "^6.4 || ^7.0",
187187
"symfony/security-core": "^6.4 || ^7.0",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\ObjectMapper;
15+
16+
/**
17+
* @internal
18+
*/
19+
interface ClearObjectMapInterface
20+
{
21+
/**
22+
* Clear object map to free memory.
23+
*/
24+
public function clearObjectMap(): void;
25+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\ObjectMapper;
15+
16+
use Symfony\Component\ObjectMapper\ObjectMapperAwareInterface;
17+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
18+
19+
final class ObjectMapper implements ObjectMapperInterface, ClearObjectMapInterface
20+
{
21+
private ?\SplObjectStorage $objectMap = null;
22+
23+
public function __construct(private ObjectMapperInterface $decorated)
24+
{
25+
if (null === $this->objectMap) {
26+
$this->objectMap = new \SplObjectStorage();
27+
}
28+
29+
if ($this->decorated instanceof ObjectMapperAwareInterface) {
30+
$this->decorated = $this->decorated->withObjectMapper($this);
31+
}
32+
}
33+
34+
public function map(object $source, object|string|null $target = null): object
35+
{
36+
if (!\is_object($target) && isset($this->objectMap[$source])) {
37+
$target = $this->objectMap[$source];
38+
}
39+
$mapped = $this->decorated->map($source, $target);
40+
$this->objectMap[$mapped] = $source;
41+
42+
return $mapped;
43+
}
44+
45+
public function clearObjectMap(): void
46+
{
47+
foreach ($this->objectMap as $k) {
48+
$this->objectMap->detach($k);
49+
}
50+
}
51+
}

src/State/Processor/ObjectMapperProcessor.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\State\Processor;
1515

1616
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\State\ObjectMapper\ClearObjectMapInterface;
1718
use ApiPlatform\State\ProcessorInterface;
1819
use Symfony\Component\ObjectMapper\Attribute\Map;
1920
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
@@ -42,6 +43,12 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
4243
return $this->decorated->process($data, $operation, $uriVariables, $context);
4344
}
4445

45-
return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context), $operation->getClass());
46+
$data = $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context), $operation->getClass());
47+
48+
if ($this->objectMapper instanceof ClearObjectMapInterface) {
49+
$this->objectMapper->clearObjectMap();
50+
}
51+
52+
return $data;
4653
}
4754
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,27 @@
44
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
55
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
66
<services>
7-
<service id="api_platform.state_provider.object_mapper" class="ApiPlatform\State\Provider\ObjectMapperProvider" decorates="api_platform.state_provider.read">
8-
<argument type="service" id="object_mapper" on-invalid="null" />
7+
<service id="api_platform.object_mapper.metadata_factory" class="Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory">
8+
</service>
9+
10+
<service id="api_platform.object_mapper" class="Symfony\Component\ObjectMapper\ObjectMapper">
11+
<argument type="service" id="api_platform.object_mapper.metadata_factory" />
12+
<argument type="service" id="property_accessor" on-invalid="null" />
13+
<argument type="tagged_locator" tag="object_mapper.transform_callable"/>
14+
<argument type="tagged_locator" tag="object_mapper.condition_callable"/>
15+
</service>
16+
17+
<service id="api_platform.object_mapper.relation" class="ApiPlatform\State\ObjectMapper\ObjectMapper" decorates="api_platform.object_mapper" decoration-priority="-255">
18+
<argument type="service" id="api_platform.object_mapper.relation.inner" />
19+
</service>
20+
21+
<service id="api_platform.state_provider.object_mapper" class="ApiPlatform\State\Provider\ObjectMapperProvider" decorates="api_platform.state_provider.locator">
22+
<argument type="service" id="api_platform.object_mapper" on-invalid="null" />
923
<argument type="service" id="api_platform.state_provider.object_mapper.inner" />
1024
</service>
1125

1226
<service id="api_platform.state_processor.object_mapper" class="ApiPlatform\State\Processor\ObjectMapperProcessor" decorates="api_platform.state_processor.locator">
13-
<argument type="service" id="object_mapper" on-invalid="null" />
27+
<argument type="service" id="api_platform.object_mapper" on-invalid="null" />
1428
<argument type="service" id="api_platform.state_processor.object_mapper.inner" />
1529
</service>
1630
</services>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Orm\State\Options;
17+
use ApiPlatform\JsonLd\ContextBuilder;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\Get;
20+
use ApiPlatform\Metadata\Put;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedResourceWithRelationEntity;
22+
use Symfony\Component\ObjectMapper\Attribute\Map;
23+
24+
#[ApiResource(
25+
stateOptions: new Options(entityClass: MappedResourceWithRelationEntity::class),
26+
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
27+
extraProperties: [
28+
'standard_put' => true,
29+
],
30+
operations: [
31+
new Get(),
32+
new Put(allowCreate: true),
33+
]
34+
)]
35+
#[Map(target: MappedResourceWithRelationEntity::class)]
36+
class MappedResourceWithRelation
37+
{
38+
public ?string $id = null;
39+
#[Map(if: false)]
40+
public ?string $relationName = null;
41+
#[Map(target: 'related')]
42+
public ?MappedResourceWithRelationRelated $relation = null;
43+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Orm\State\Options;
17+
use ApiPlatform\JsonLd\ContextBuilder;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\NotExposed;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedResourceWithRelationRelatedEntity;
21+
use Symfony\Component\ObjectMapper\Attribute\Map;
22+
23+
#[ApiResource(
24+
operations: [
25+
new NotExposed(
26+
stateOptions: new Options(entityClass: MappedResourceWithRelationRelatedEntity::class),
27+
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
28+
),
29+
],
30+
graphQlOperations: []
31+
)]
32+
#[Map(target: MappedResourceWithRelationRelatedEntity::class)]
33+
class MappedResourceWithRelationRelated
34+
{
35+
#[Map(if: false)]
36+
public string $id;
37+
38+
public string $name;
39+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResourceWithRelation;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
20+
#[ORM\Entity]
21+
#[Map(target: MappedResourceWithRelation::class)]
22+
class MappedResourceWithRelationEntity
23+
{
24+
#[ORM\Id, ORM\Column]
25+
private ?int $id = null;
26+
27+
#[ORM\ManyToOne(targetEntity: MappedResourceWithRelationRelatedEntity::class)]
28+
#[Map(target: 'relation')]
29+
#[Map(target: 'relationName', transform: [self::class, 'transformRelation'])]
30+
private ?MappedResourceWithRelationRelatedEntity $related = null;
31+
32+
public static function transformRelation($value, $source)
33+
{
34+
return $source->getRelated()->name;
35+
}
36+
37+
public function getId(): ?int
38+
{
39+
return $this->id;
40+
}
41+
42+
public function setId(?int $id = null)
43+
{
44+
$this->id = $id;
45+
46+
return $this;
47+
}
48+
49+
public function getRelated(): ?MappedResourceWithRelationRelatedEntity
50+
{
51+
return $this->related;
52+
}
53+
54+
public function setRelated(?MappedResourceWithRelationRelatedEntity $related): self
55+
{
56+
$this->related = $related;
57+
58+
return $this;
59+
}
60+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResourceWithRelationRelated;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
20+
#[ORM\Entity]
21+
#[Map(target: MappedResourceWithRelationRelated::class)]
22+
class MappedResourceWithRelationRelatedEntity
23+
{
24+
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
25+
private ?int $id = null;
26+
27+
#[ORM\Column]
28+
public string $name;
29+
30+
public function getId(): ?int
31+
{
32+
return $this->id;
33+
}
34+
}

0 commit comments

Comments
 (0)