core
core copied to clipboard
Error during PUT operation when using DTO and stateOptions
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
- Configure an ApiResource over DTO with stateOptions settings based on a Doctrine Entity.
- 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.
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;
}
}
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);?
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.
Thank you @soyuka! It works