Skip to content
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

Unable to map an array of dto #218

Open
Nilofar opened this issue Jan 23, 2025 · 2 comments
Open

Unable to map an array of dto #218

Nilofar opened this issue Jan 23, 2025 · 2 comments

Comments

@Nilofar
Copy link

Nilofar commented Jan 23, 2025

Hi,

I'm actually using your mapper in a symfony app with api platform. I'm trying to use mapper in a custom EntityToDtoProvider to automatically hydrate dtos from entities.

It's almost working, but it seems like array of objects are not transformed into dto.

Dto is something like that :

class CompanyDto
{
    #[Groups([Company::READ])]
    public ?int $id = null;

    #[Groups([Company::READ])]
    public ?string $title = null;

    #[Groups(Company::READ)]
    /**@var JobDto[] $jobs */
    public array $jobs;
}

Entity is something like that

class Company extends GenericEntityUser
{
    public const READ = "companies:read";

    #[ORM\Column(length: 255)]
    #[Groups([Company::READ])]
    #[Gedmo\Versioned]
    private ?string $title = null;

    #[ORM\OneToMany(mappedBy: 'company', targetEntity: Job::class, orphanRemoval: true)]
    #[Groups([Company::READ])]
    private Collection $jobs;

Custom provider is like that

<?php
namespace App\Provider;
use ApiPlatform\Doctrine\Orm\Paginator;
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use AutoMapper\AutoMapper;
use AutoMapper\AutoMapperInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

readonly class EntityToDtoStateProvider implements ProviderInterface
{
    public function __construct(
        #[Autowire(service: CollectionProvider::class)]
        private ProviderInterface $collectionProvider,
        #[Autowire(service: ItemProvider::class)]
        private ProviderInterface $itemProvider,
        private AutoMapperInterface $autoMapper
    )
    {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        $resourceClass = $operation->getClass();
        if ($operation instanceof CollectionOperationInterface) {
            $entities = $this->collectionProvider->provide($operation, $uriVariables, $context);
            assert($entities instanceof Paginator);

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

            return new TraversablePaginator(
                new \ArrayIterator($dtos),
                $entities->getCurrentPage(),
                $entities->getItemsPerPage(),
                $entities->getTotalItems()
            );
        }

        $entity = $this->itemProvider->provide($operation, $uriVariables, $context);
        if (!$entity) {
            return null;
        }

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

    private function mapEntityToDto(object $entity, string $resourceClass, array $context = []): object
    {
        return $this->autoMapper->map($entity, $resourceClass, [
            'groups' => $context['groups'] ?? [],
        ]);
    }
}

But when i make a call through the api, i got a CompanyDto but the propety jobs in dto is expected to have an array of JobDto but i got an array of Job Entity.

What i'm missing ? Do I need to add something to get this behavior ?

Thanks a lot

@Nilofar
Copy link
Author

Nilofar commented Jan 24, 2025

Hi,

Finally, after analyzing the ArrayTransformer, I found a solution consisting in adding a method add and a method remove to the dto.

Probably because of this if/else in AbstractArrayTransformer that check if type of writeMutator is TYPE_ADDER_AND_REMOVER (5), if i dont set add and remove method, i got a type TYPE_PROPERTY (2)

        if ($propertyMapping->target->writeMutator && $propertyMapping->target->writeMutator->type === WriteMutator::TYPE_ADDER_AND_REMOVER) {
            /**
             * Use add and remove methods.
             *
             * $target->add($output);
             */
            $mappedValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('mappedValue'));
            $itemStatements[] = new Stmt\Expression(new Expr\Assign($mappedValueVar, $output));
            $itemStatements[] = new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $mappedValueVar), [
                'stmts' => [
                    new Stmt\Expression($propertyMapping->target->writeMutator->getExpression($target, $mappedValueVar, $assignByRef)),
                ],
            ]);

//            dd($input, $target, $uniqueVariableScope, $propertyMapping, $source);
        } else {
            /*
             * Assign the value to the array.
             *
             * $values[] = $output;
             * or
             * $values[$key] = $output;
             */
//            dd($output);
//            dd(new Stmt\Expression($this->getAssignExpr($valuesVar, $output, $loopKeyVar, $assignByRef)));
            $itemStatements[] = new Stmt\Expression($this->getAssignExpr($valuesVar, $output, $loopKeyVar, $assignByRef));
        }

So adding a method addJob(JobDto $dto) and removeJob(JobDto $dto) transforms the array of Entity in array of dtos.

    public function addJob(JobDto $dto): self
    {
        $this->jobs[] = $dto;
        return $this;
    }

    public function removeJob(CollaboratorDto $dto): self
    {
        foreach ($this->jobs as $index => $job) {
            if ($job === $dto) {
                unset($this->jobs[$index]);
            }
        }
        return $this;
    }

Is this normal behavior? I find it odd to have to create these methods for an array especially in a dto object

@fpetrovic
Copy link

I also had a problem with mapping source Collection property to target array property. Not sure what is the best approach for this. Docs are not covering this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants