automapper icon indicating copy to clipboard operation
automapper copied to clipboard

IRI's not returned for a relation

Open fpetrovic opened this issue 7 months ago • 0 comments

I have custom provider in api-platform that uses automapper. My expectation would be that automapper handles IRI's where needed, but it does not do that. It works well if needs to return an object, but totally fails with IRI's.

In example below, I expected media to be returned as array of iri strings, when I call records collection, and it fails to do that.

PHP: 8.3.20 Symfony: 7.2.5 jolicode/automapper 9.4.1 api-platform: 4.1.7

#[ApiResource(
    shortName: 'Media',
    operations: [new Get()],
    normalizationContext: ['openapi_definition_name' => 'item-Read', 'groups' => ['media:item:read']],
    denormalizationContext: ['openapi_definition_name' => 'item-Write', 'groups' => ['media:item:write']],
    provider: EntityToDtoStateProvider::class,
    processor: EntityClassDtoStateProcessor::class,
    stateOptions: new Options(
        entityClass: RecordMedia::class,
    ),
)]
class MediaApi implements ApiInterface
{
    #[Groups(['media:list', 'media:item:read'])]
    private string $id;

    // rest of props

    public function getId(): string
    {
        return $this->id;
    }

    public function setId(string $id): void
    {
        $this->id = $id;
    }
   ///  rest of methods
}

#[ApiResource(
    shortName: 'Record',
    operations: [
        new Get(
            security: 'is_granted("CAN_VIEW_RECORD", object)',
        ),
        new GetCollection(
            normalizationContext: [
                'openapi_definition_name' => 'list',
                'groups' => RecordApi::RECORD_LIST_NORMALIZATION_GROUP,
            ],
            security: 'is_granted("CAN_VIEW_RECORD", object)',
        ),
    ],
    normalizationContext: [
        'openapi_definition_name' => 'item-Read',
        'groups' => RecordApi::RECORD_ITEM_NORMALIZATION_GROUP,
    ],
    denormalizationContext: ['openapi_definition_name' => 'item-Write', 'groups' => ['record:item:write']],
    provider: RecordProvider::class,
    processor: EntityClassDtoStateProcessor::class,
    stateOptions: new Options(
        entityClass: Record::class,
    ),
)]
class RecordApi implements ApiInterface
{
    public const array RECORD_LIST_NORMALIZATION_GROUP = [
        'record:list',
    ];

    public const array RECORD_ITEM_NORMALIZATION_GROUP = [
        'record:item:read',
        'media:item:read',
    ];

    #[Groups(['record:item:read', 'record:list'])]
    private string $id;

    // automapper check singularity of property plural: Media - Medium for adder and remover methods
    // that is the reason why we have addMedium/removeMedium methods
    // ideally, it should use some reflection class check, but it is what it is
    // adder and remover are essential for automapper to work for Collection to array
    /**
     * @var MediaApi[]$media
     */
    #[Groups(['record:item:read', 'record:list'])]
    #[MapFrom(property: 'recordMedia')]
    private array $media;

    public function __construct()
    {
        $this->media = [];
    }

    public function getId(): string
    {
        return $this->id;
    }
// rest of methods


readonly class RecordProvider implements ProviderInterface
{
    public function __construct(
       // consructor arguments
    ) {
    }

    /**
     * @param MapperContextArray $context
     *
     * @throws \Exception
     */
    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        /** @var class-string<object> $resourceClass */
        $resourceClass = $operation->getClass();

        if ($operation instanceof CollectionOperationInterface) {
            // some logic

            $recordsResponse = $this->recordsRepository->findFromExternalApiRequest($indexRecordsRequest);

            $dtos = [];
            foreach ($recordsResponse->getResults() as $entity) {
                $dtos[] = $this->mapEntityToDto($entity, $resourceClass, $context);
            }

            return new TraversablePaginator(
                new \ArrayIterator($dtos),
                $currentPage,
                $itemsPerPage,
                 $recordsResponse->getTotalResultCount(),
            );
        }

        $entity = $this->itemProvider->provide($operation, $uriVariables, $context);

        if (!$entity) {
            return null;
        }

        return $this->mapEntityToDto($entity, $resourceClass, $context);
    }

    /**
     * @param class-string<object> $resourceClass
     * @param MapperContextArray   $context
     *
     * @throws \Exception
     */
    private function mapEntityToDto(object $entity, string $resourceClass, array $context): object
    {
        $mapped = $this->autoMapper->map($entity, $resourceClass, $context);
        assert(is_object($mapped));

        return $mapped;
    }
}```

fpetrovic avatar Jun 05 '25 09:06 fpetrovic