-
-
Notifications
You must be signed in to change notification settings - Fork 926
fix(state): object-mapper reuse related entity #7300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\State\ObjectMapper; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
interface ClearObjectMapInterface | ||
{ | ||
/** | ||
* Clear object map to free memory. | ||
*/ | ||
public function clearObjectMap(): void; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\State\ObjectMapper; | ||
|
||
use Symfony\Component\ObjectMapper\ObjectMapperAwareInterface; | ||
use Symfony\Component\ObjectMapper\ObjectMapperInterface; | ||
|
||
final class ObjectMapper implements ObjectMapperInterface, ClearObjectMapInterface | ||
{ | ||
private ?\SplObjectStorage $objectMap = null; | ||
|
||
public function __construct(private ObjectMapperInterface $decorated) | ||
{ | ||
if (null === $this->objectMap) { | ||
$this->objectMap = new \SplObjectStorage(); | ||
} | ||
|
||
if ($this->decorated instanceof ObjectMapperAwareInterface) { | ||
$this->decorated = $this->decorated->withObjectMapper($this); | ||
} | ||
} | ||
|
||
public function map(object $source, object|string|null $target = null): object | ||
{ | ||
if (isset($this->objectMap[$source])) { | ||
$target = $this->objectMap[$source]; | ||
} | ||
$mapped = $this->decorated->map($source, $target); | ||
$this->objectMap[$mapped] = $source; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't you also add This probably should be indexed by the object class too, otherwise multiple mappings will not work - you'll get errors similar to what was fixed by #7311 |
||
|
||
return $mapped; | ||
} | ||
|
||
public function clearObjectMap(): void | ||
{ | ||
foreach ($this->objectMap as $k) { | ||
$this->objectMap->detach($k); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; | ||
|
||
use ApiPlatform\Doctrine\Orm\State\Options; | ||
use ApiPlatform\JsonLd\ContextBuilder; | ||
use ApiPlatform\Metadata\ApiResource; | ||
use ApiPlatform\Metadata\Get; | ||
use ApiPlatform\Metadata\Put; | ||
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedResourceWithRelationEntity; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
|
||
#[ApiResource( | ||
stateOptions: new Options(entityClass: MappedResourceWithRelationEntity::class), | ||
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false], | ||
extraProperties: [ | ||
'standard_put' => true, | ||
], | ||
operations: [ | ||
new Get(), | ||
new Put(allowCreate: true), | ||
] | ||
)] | ||
#[Map(target: MappedResourceWithRelationEntity::class)] | ||
class MappedResourceWithRelation | ||
{ | ||
public ?string $id = null; | ||
#[Map(if: false)] | ||
public ?string $relationName = null; | ||
#[Map(target: 'related')] | ||
public ?MappedResourceWithRelationRelated $relation = null; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; | ||
|
||
use ApiPlatform\Doctrine\Orm\State\Options; | ||
use ApiPlatform\JsonLd\ContextBuilder; | ||
use ApiPlatform\Metadata\NotExposed; | ||
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedResourceWithRelationRelatedEntity; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
|
||
#[NotExposed( | ||
stateOptions: new Options(entityClass: MappedResourceWithRelationRelatedEntity::class), | ||
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false], | ||
)] | ||
#[Map(target: MappedResourceWithRelationRelatedEntity::class)] | ||
class MappedResourceWithRelationRelated | ||
{ | ||
#[Map(if: false)] | ||
public string $id; | ||
|
||
public string $name; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; | ||
|
||
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResourceWithRelation; | ||
use Doctrine\ORM\Mapping as ORM; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
|
||
#[ORM\Entity] | ||
#[Map(target: MappedResourceWithRelation::class)] | ||
class MappedResourceWithRelationEntity | ||
{ | ||
#[ORM\Id, ORM\Column] | ||
private ?int $id = null; | ||
|
||
#[ORM\ManyToOne(targetEntity: MappedResourceWithRelationRelatedEntity::class)] | ||
#[Map(target: 'relation')] | ||
#[Map(target: 'relationName', transform: [self::class, 'transformRelation'])] | ||
private ?MappedResourceWithRelationRelatedEntity $related = null; | ||
|
||
public static function transformRelation($value, $source) | ||
{ | ||
return $source->getRelated()->name; | ||
} | ||
|
||
public function getId(): ?int | ||
{ | ||
return $this->id; | ||
} | ||
|
||
public function setId(?int $id = null) | ||
{ | ||
$this->id = $id; | ||
|
||
return $this; | ||
} | ||
|
||
public function getRelated(): ?MappedResourceWithRelationRelatedEntity | ||
{ | ||
return $this->related; | ||
} | ||
|
||
public function setRelated(?MappedResourceWithRelationRelatedEntity $related): self | ||
{ | ||
$this->related = $related; | ||
|
||
return $this; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; | ||
|
||
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResourceWithRelationRelated; | ||
use Doctrine\ORM\Mapping as ORM; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
|
||
#[ORM\Entity] | ||
#[Map(target: MappedResourceWithRelationRelated::class)] | ||
class MappedResourceWithRelationRelatedEntity | ||
{ | ||
#[ORM\Id, ORM\Column, ORM\GeneratedValue] | ||
private ?int $id = null; | ||
|
||
#[ORM\Column] | ||
public string $name; | ||
|
||
public function getId(): ?int | ||
{ | ||
return $this->id; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we want to check if a target has not been given explicitly by the user before overriding it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my tests the target is a string and I do need to map but I'll test this further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, if the target is an object it shouldn't be overridden.