core icon indicating copy to clipboard operation
core copied to clipboard

Error during PUT operation when using DTO and stateOptions

Open popadko opened this issue 1 year ago • 1 comments

Hello! Hope I find you in a good mood!

API Platform version(s) affected: 3.3.7

Description
I followed the next guides to create multiple ApiResources over one entity using DTO and stateOptions. https://www.youtube.com/watch?v=IVJjADhU7WM https://symfonycasts.com/screencast/api-platform-extending

Everything works like a charm except for the PUT operation. I get the 500 error because Doctrine can not retrieve the entity identifier. Please see the details below.

How to reproduce

  1. Configure an ApiResource over DTO with stateOptions settings based on a Doctrine Entity.
  2. Perform the PUT request on the ApiResource

ER: PUT works as expected. AR:

{
  "title": "An error occurred",
  "detail": "Given object is not an instance of the class this property was declared in",
  "status": 500,
  "type": "/errors/500",
  "trace": [
    {
      "file": "/home/www/vendor/doctrine/persistence/src/Persistence/Reflection/TypedNoDefaultReflectionPropertyBase.php",
      "line": 33,
      "function": "isInitialized",
      "class": "ReflectionProperty",
      "type": "->"
    },
    {
      "file": "/home/www/vendor/doctrine/orm/src/Mapping/ClassMetadata.php",
      "line": 616,
      "function": "getValue",
      "class": "Doctrine\\Persistence\\Reflection\\TypedNoDefaultReflectionProperty",
      "type": "->"
    },
    {
      "file": "/home/www/vendor/api-platform/core/src/Doctrine/Common/State/PersistProcessor.php",
      "line": 61,
      "function": "getIdentifierValues",
      "class": "Doctrine\\ORM\\Mapping\\ClassMetadata",
      "type": "->"
    },
    {
      "file": "/home/www/src/ApiPlatform/State/Processor/Doctrine/EntityClassDtoStateProcessor.php",
      "line": 32,
      "function": "process",
      "class": "ApiPlatform\\Doctrine\\Common\\State\\PersistProcessor",
      "type": "->"
    },

Possible Solution
I've debugged it a little and it seems Doctrine tries to fetch an identifier from $context['previous_data'] which is a DTO and not an entity, so it fails. Screenshot 2024-07-12 at 15 35 32

My understanding is that the case with stateOptions configuration should be handled inside the \ApiPlatform\Doctrine\Common\State\PersistProcessor.

Please assist with a correct solution here, or point me to my mistake. Thank you!

Additional Context

My ApiResource


#[ApiFilter(SearchFilter::class, properties: [
    'id' => 'exact',
    'type' => 'exact',
    'title' => 'partial',
    'description' => 'partial',
    'keywords' => 'partial'
])]
#[ApiFilter(JsonPropertySearchFilter::class, properties: ['metadata.zodiac'])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt' => 'DESC', 'id' => 'DESC'])]
#[ApiFilter(RangeFilter::class, properties: ['id'])]
#[ApiResource(
    shortName: 'Article Admin',
    operations: [
        new Get('/articles/{id}'),
        new GetCollection('/articles'),
        new Put(uriTemplate: '/articles/{id}', processor: EntityClassDtoStateProcessor::class),
        new Patch(uriTemplate: '/articles/{id}', processor: EntityClassDtoStateProcessor::class),
        new Post(uriTemplate: '/articles', processor: EntityClassDtoStateProcessor::class),
        new Delete('/articles/{id}')
    ],
    routePrefix: '/admin',
    paginationViaCursor: [
        ['field' => 'id', 'direction' => 'DESC']
    ],
    processor: EntityClassDtoStateProcessor::class,
    provider: EntityToDtoApiStateProvider::class,
    stateOptions: new Options(entityClass: Article::class)
)]
class AdminArticleApi
{
    #[ApiProperty(readable: true, writable: false, identifier: true)]
    private ?string $id;
// ...

My Processor

namespace App\ApiPlatform\State\Processor\Doctrine;

use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Doctrine\Common\State\RemoveProcessor;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfonycasts\MicroMapper\MicroMapperInterface;

class EntityClassDtoStateProcessor implements ProcessorInterface
{
    public function __construct(
        #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor,
        #[Autowire(service: RemoveProcessor::class)] private ProcessorInterface $removeProcessor,
        private MicroMapperInterface $microMapper
    ) {
    }

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
    {
        $stateOptions = $operation->getStateOptions();
        $entityClass = $stateOptions->getEntityClass();

        $entity = $this->microMapper->map($data, $entityClass);
        if ($operation instanceof DeleteOperationInterface) {
            $this->removeProcessor->process($entity, $operation, $uriVariables, $context);
            return null;
        }
        $this->persistProcessor->process($entity, $operation, $uriVariables, $context);
        $data->setId($entity->getId());
        return $data;
    }
}

popadko avatar Jul 12 '24 13:07 popadko

The mapper should transform your dto into an entity, you may need to set $context['previous_data'] = $entity = $this->microMapper->map($context['previous_data'], $entityClass);?

soyuka avatar Jul 15 '24 12:07 soyuka

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Sep 13 '24 23:09 stale[bot]

Thank you @soyuka! It works

popadko avatar Sep 16 '24 13:09 popadko