Skip to content

[ObjectMapper] Mapping array to Collection and the other way around #61266

@Methraen

Description

@Methraen

Description

When you have a DTO with an array you want to map on an entity with a doctrine Collection (or the other way around), right now it's not handled, I found a workaround but it would be super nice to add this feature...

Workaround array to collection:

    /** @var Collection<int, Contact> */
    #[Map(target: 'contacts', transform: ArrayTransformer::class)]
    #[ORM\OneToMany(mappedBy: 'utilisateur', targetEntity: Contact::class, cascade: ['persist', 'remove'])]
    private Collection $contacts;

+

<?php

declare(strict_types=1);

namespace App\Common\ObjectMapper;

use Doctrine\Common\Collections\Collection;
use LogicException;
use ReflectionClass;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\ObjectMapper\TransformCallableInterface;

/**
 * @template T of object
 *
 * @template-implements TransformCallableInterface<T, T[]>
 *
 * @phpstan-ignore-next-line
 */
class ArrayTransformer implements TransformCallableInterface
{
    public function __construct(
        private readonly ObjectMapperInterface $objectMapper,
    ) {
    }

    /**
     * @param Collection<T>|mixed $value
     *
     * @return T[]|mixed
     */
    public function __invoke(mixed $value, object $source, ?object $target): mixed
    {
        if (!$value instanceof Collection || $value->isEmpty()) {
            return $value;
        }

        $firstItem = $value->first();
        if (!is_object($firstItem)) {
            throw new LogicException("La collection ne contient pas d'objets transformables.");
        }

        $targetClass = $this->getTargetClass($firstItem);
        if (null === $targetClass) {
            throw new LogicException("La collection ne contient pas d'objets transformables.");
        }

        /** @var T[] $items */
        $items = $value->toArray();

        return array_map(
            fn ($item): object => $this->objectMapper->map($item, $targetClass),
            $items
        );
    }

    /**
     * @return class-string<T>|null
     */
    private function getTargetClass(object $object): ?string
    {
        $reflectionClass = new ReflectionClass($object);
        $mapAttribute    = $reflectionClass->getAttributes(Map::class)[0] ?? null;

        if (null === $mapAttribute) {
            return null;
        }

        $arguments = $mapAttribute->getArguments();

        /** @var class-string<T>|null $targetClass */
        $targetClass = $arguments['target'] ?? null;

        return $targetClass;
    }
}

Workaround collection to array:

        #[Map(target: 'contacts', transform: CollectionTransformer::class)]
        public readonly ?array $contacts = [],

+

<?php

declare(strict_types=1);

namespace App\Common\ObjectMapper;

use Doctrine\Common\Collections\ArrayCollection;
use LogicException;
use ReflectionClass;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\ObjectMapper\TransformCallableInterface;

/**
 * @template T of object
 *
 * @template-implements TransformCallableInterface<T, ArrayCollection<int, T>>
 */
class CollectionTransformer implements TransformCallableInterface
{
    public function __construct(
        private readonly ObjectMapperInterface $objectMapper,
    ) {
    }

    /**
     * @param T[]|mixed $value
     *
     * @return ArrayCollection<int, T>|mixed
     */
    public function __invoke(mixed $value, object $source, ?object $target): mixed
    {
        if (!is_array($value) || [] === $value) {
            return $value;
        }

        $firstItem = $value[0];
        if (!is_object($firstItem)) {
            throw new LogicException("Le tableau ne contient pas d'objets transformables.");
        }

        $targetClass = $this->getTargetClass($firstItem);
        if (null === $targetClass) {
            throw new LogicException("Le tableau ne contient pas d'objets transformables.");
        }

        /** @var T[] $items */
        $items = $value;

        return new ArrayCollection(
            array_map(
                fn (object $item): object => $this->objectMapper->map($item, $targetClass),
                $items
            )
        );
    }

    /**
     * @return class-string<T>|null
     */
    private function getTargetClass(object $object): ?string
    {
        $reflectionClass = new ReflectionClass($object);
        $mapAttribute    = $reflectionClass->getAttributes(Map::class)[0] ?? null;

        if (null === $mapAttribute) {
            return null;
        }

        $arguments = $mapAttribute->getArguments();

        /** @var class-string<T>|null $targetClass */
        $targetClass = $arguments['target'] ?? null;

        return $targetClass;
    }
}

Example

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions