Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Doctrine/Common/State/PersistProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
$reflectionProperty->setValue($newData, $newValue);
}
}
// We create a new entity through PUT
} else {
foreach (array_reverse($links) as $link) {
if ($link->getExpandedValue() || !$link->getFromClass()) {
Expand All @@ -92,6 +93,45 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
$reflectionProperty->setValue($newData, $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null));
}
}

$classMetadata = $manager->getClassMetadata($class);
foreach ($reflectionProperties as $propertyName => $reflectionProperty) {
if ($classMetadata->isIdentifier($propertyName)) {
continue;
}

$value = $reflectionProperty->getValue($newData);

if (!\is_object($value)) {
continue;
}

if (
!($relManager = $this->managerRegistry->getManagerForClass($class = $this->getObjectClass($value)))
|| $relManager->contains($value)
) {
continue;
}

if (\PHP_VERSION_ID > 80400) {
$r = new \ReflectionClass($value);
if ($r->isUninitializedLazyObject($value)) {
$r->initializeLazyObject($value);
}
}

$metadata = $manager->getClassMetadata($class);
$identifiers = $metadata->getIdentifierValues($value);

// Do not get reference for partial objects or objects with null identifiers
if (!$identifiers || \count($identifiers) !== \count(array_filter($identifiers, static fn ($v) => null !== $v))) {
continue;
}

\assert(method_exists($relManager, 'getReference'));

$reflectionProperty->setValue($newData, $relManager->getReference($class, $identifiers));
}
}

$data = $newData;
Expand Down
25 changes: 0 additions & 25 deletions src/State/ObjectMapper/ClearObjectMapInterface.php

This file was deleted.

23 changes: 2 additions & 21 deletions src/State/ObjectMapper/ObjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,18 @@
use Symfony\Component\ObjectMapper\ObjectMapperAwareInterface;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

final class ObjectMapper implements ObjectMapperInterface, ClearObjectMapInterface, ObjectMapperAwareInterface
final class ObjectMapper implements ObjectMapperInterface
{
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 (!\is_object($target) && isset($this->objectMap[$source])) {
$target = $this->objectMap[$source];
}
$mapped = $this->decorated->map($source, $target);
$this->objectMap[$mapped] = $source;

return $mapped;
}

public function clearObjectMap(): void
{
foreach ($this->objectMap as $k) {
$this->objectMap->detach($k);
}
return $this->decorated->map($source, $target);
}

public function withObjectMapper(ObjectMapperInterface $objectMapper): static
Expand Down
35 changes: 27 additions & 8 deletions src/State/Processor/ObjectMapperProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
namespace ApiPlatform\State\Processor;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ObjectMapper\ClearObjectMapInterface;
use ApiPlatform\State\ProcessorInterface;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

/**
Expand All @@ -30,27 +30,46 @@ final class ObjectMapperProcessor implements ProcessorInterface
public function __construct(
private readonly ?ObjectMapperInterface $objectMapper,
private readonly ProcessorInterface $decorated,
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null,
) {
// TODO: 4.3 add this deprecation
// if (!$objectMapperMetadata) {
// trigger_deprecation('api-platform/state', '4.3', 'Not injecting "%s" in "%s" will not be possible anymore in 5.0.', ObjectMapperMetadataFactoryInterface::class, __CLASS__);
// }
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$class = $operation->getInput()['class'] ?? $operation->getClass();

if (
!$this->objectMapper
|| !$operation->canWrite()
|| null === $data
|| !is_a($data, $operation->getClass(), true)
|| !(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)
|| !is_a($data, $class, true)
) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

$data = $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context), $operation->getClass());

if ($this->objectMapper instanceof ClearObjectMapInterface) {
$this->objectMapper->clearObjectMap();
if ($this->objectMapperMetadata) {
if (!$this->objectMapperMetadata->create($data)) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}
} elseif (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

return $data;
// return the Resource representation of the persisted entity
return $this->objectMapper->map(
// persist the entity
$this->decorated->process(
// maps the Resource to an Entity
$this->objectMapper->map($data),
$operation,
$uriVariables,
$context,
),
$operation->getClass()
);
}
}
29 changes: 26 additions & 3 deletions src/State/Provider/ObjectMapperProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use ApiPlatform\State\Pagination\PaginatorInterface;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

/**
Expand All @@ -36,7 +37,12 @@ final class ObjectMapperProvider implements ProviderInterface
public function __construct(
private readonly ?ObjectMapperInterface $objectMapper,
private readonly ProviderInterface $decorated,
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null,
) {
// TODO: 4.3 add this deprecation
// if (!$objectMapperMetadata) {
// trigger_deprecation('api-platform/state', '4.3', 'Not injecting "%s" in "%s" will not be possible anymore in 5.0.', ObjectMapperMetadataFactoryInterface::class, __CLASS__);
// }
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
Expand All @@ -57,9 +63,12 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$entityClass = $options->getDocumentClass();
}

$entityClass ??= $data::class;

if (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class) && !(new \ReflectionClass($entityClass))->getAttributes(Map::class)) {
// Look for Mapping metadata
if ($this->objectMapperMetadata) {
if (!$this->canBeMapped($operation->getClass()) && (!$entityClass || !$this->canBeMapped($entityClass))) {
return $data;
}
} elseif (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class) && !(new \ReflectionClass($entityClass))->getAttributes(Map::class)) {
return $data;
}

Expand All @@ -74,4 +83,18 @@ public function provide(Operation $operation, array $uriVariables = [], array $c

return $data;
}

private function canBeMapped(string $class): bool
{
try {
$r = new \ReflectionClass($class);
if (!$r->isInstantiable() || !$this->objectMapperMetadata->create($r->newInstanceWithoutConstructor(), null, ['_api_check_can_be_mapped' => true])) {
return false;
}
} catch (\ReflectionException $e) {
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
service('api_platform.resource_class_resolver'),
service('api_platform.property_accessor'),
service('api_platform.object_mapper')->nullOnInvalid(),
service('api_platform.object_mapper.metadata_factory')->nullOnInvalid(),
])
->tag('doctrine.event_listener', ['event' => 'preUpdate'])
->tag('doctrine.event_listener', ['event' => 'onFlush'])
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/Resources/config/state/object_mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@
->args([
service('api_platform.object_mapper')->nullOnInvalid(),
service('api_platform.state_provider.object_mapper.inner'),
service('api_platform.object_mapper.metadata_factory'),
]);

$services->set('api_platform.state_processor.object_mapper', 'ApiPlatform\State\Processor\ObjectMapperProcessor')
->decorate('api_platform.state_processor.locator', null, 0)
->args([
service('api_platform.object_mapper')->nullOnInvalid(),
service('api_platform.state_processor.object_mapper.inner'),
service('api_platform.object_mapper.metadata_factory'),
]);
};
55 changes: 39 additions & 16 deletions src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand All @@ -48,7 +49,8 @@ public function __construct(private readonly PurgerInterface $purger,
private readonly IriConverterInterface $iriConverter,
private readonly ResourceClassResolverInterface $resourceClassResolver,
?PropertyAccessorInterface $propertyAccessor = null,
private readonly ?ObjectMapperInterface $objectMapper = null)
private readonly ?ObjectMapperInterface $objectMapper = null,
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null)
{
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
}
Expand Down Expand Up @@ -153,13 +155,21 @@ private function gatherRelationTags(EntityManagerInterface $em, object $entity):
if (
\is_array($associationMapping)
&& \array_key_exists('targetEntity', $associationMapping)
&& !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])
&& (
!$this->objectMapper
|| !(new \ReflectionClass($associationMapping['targetEntity']))->getAttributes(Map::class)
)
&& ($targetEntity = $associationMapping['targetEntity'])
&& !$this->resourceClassResolver->isResourceClass($targetEntity)
) {
return;
if (!$this->objectMapper) {
return;
}

$targetRefl = new \ReflectionClass($targetEntity);
if ($this->objectMapperMetadata) {
if (!$this->objectMapperMetadata->create($targetRefl->newInstanceWithoutConstructor())) {
return;
}
} elseif (!$targetRefl->getAttributes(Map::class)) {
return;
}
}

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

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

if (!$mapAttributes) {
return [];
}
if (!$mappings) {
return [];
}

$resources = array_map(
fn ($mapping) => $this->objectMapper->map($entity, $mapping->target),
$mappings
);
} else {
$mapAttributes = (new \ReflectionClass($class))->getAttributes(Map::class);

// loop over all mappings to fetch all resources mapped to this entity
$resources = array_map(
fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
$mapAttributes
);
if (!$mapAttributes) {
return [];
}

// loop over all mappings to fetch all resources mapped to this entity
$resources = array_map(
fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
$mapAttributes
);
}
} else {
$resources[] = $entity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
#[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
Expand Up @@ -22,6 +22,7 @@
class MappedResourceWithRelationEntity
{
#[ORM\Id, ORM\Column]
#[Map(transform: 'to_string')]
private ?int $id = null;

#[ORM\ManyToOne(targetEntity: MappedResourceWithRelationRelatedEntity::class)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public function getId(): ?int
{
return $this->id;
}

public function setId(?int $id = null)
{
$this->id = $id;
}
}
Loading