I want to provide a custom request body as DTO for an operation for an entity.
So, let's say I have the Book entity, and I want to have a custom operation named POST /books/{id}/sell which will take a request body of:
{
"amount": 123,
"to": "Customer 1"
}
And, then I'd have a service which would accept the Book entity and the DTO data and do whatever custom operation I needed to do.
I haven't found a clean way to implement this, so I'm curious of what you guys think would be a clean solution.
I though of maybe just treating the /books/{id}/sell under the SellBookRequest DTO resource instead of the Book resource, but then I can't easily get API platform to fetch the parent book resource...
For now, i'm just opting to use an endpoint like /books/sell
and updating the DTO to look like:
{
"amount": 123,
"to": "Customer 1",
"book": "/books/1"
}
Recently i had same need and this solution works perfectly even without DTOs: https://api-platform.com/docs/core/operations#recommended-method
Client makes POST /api/teapots/42/return.jsonapi and my operation looks like this:
<?php
namespace App\Controller;
use App\Entity\Teapot;
use App\Exception\TeapotAlreadyReturnedException;
use App\Service\TeapotReturnChecker;
use Symfony\Bridge\Doctrine\RegistryInterface;
final class ReturnTeapotAction
{
private $em;
private $checker;
public function __construct(RegistryInterface $doctrine, TeapotReturnChecker $checker)
{
$this->em = $doctrine->getEntityManager();
$this->checker = $checker;
}
public function __invoke(Teapot $teapot): Teapot
{
if ($this->checker->isReturned($teapot)) throw new TeapotAlreadyReturnedException();
// ..... other domain logic with database .....
return $teapot;
}
}
And my entity:
<?php
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\ReturnTeapotAction;
/**
* @ApiResource(itemOperations={
* "get",
* "return"={
* "method"="POST",
* "path"="/teapots/{id}/return.{_format}",
* "controller"=ReturnTeapotAction::class,
* "swagger_context"={"summary"="Return teapot back to owner"},
* }
* })
*/
class Teapot { /* ..... regular entity stuff ..... */ }
@TrogWarZ Yes, that works fine if you don't need a custom request body.
The issue comes into play when you want to define an item operation, but then also have a custom request body.
@ragboyjr did you finally found a solution? I'm blocked at the same point, either accept a DTO or use an operation with an Entity, but not both options at the same time.
@SultanICQ y, checkout api-platform 2.4's input classes with messenger, it seems to work very well.
Woaw! that's speed! :D
I think you mean this right?
https://api-platform.com/docs/core/messenger/#using-messenger-with-an-input-object
yes exactly :)
You're right! Works like a charm! Thank you. The docs had a little mistake with the messenger value but I sent a pull request with a fix.
Great job :), thanks!
@SultanICQ Can you please post your final solution? I have the same issue and as far as I tried, documentation still doesn't cover that case.
@Jabolek you can use custom operation with disabled deserialization and extract data from request content like when handling file upload https://api-platform.com/docs/core/file-upload/
itemOperations={
"operation"={
"method"="POST",
"path"="/path",
"controller"=CustomOperation::class,
"deserialize"=false,
}
},
public function __invoke(Request $request)
{
// $request->getContent()
// ...
}
@ragboyjr @SultanICQ @Jabolek If possible can you please post your solution. I didn't get it managed to use custom operation with DTO. Documentation doesn't helps...
Can someone update the documentation to help people further?
@SultanICQ y, checkout api-platform 2.4's input classes with messenger, it seems to work very well.
@ragboyjr But it will return to handler only Dto object (body of request), how do you get your book id from url in handler?
@Eimantas123 I could manage to get the resource id in the handler using a custom DataTransformer that sets the ID in the input DTO.
The custom behavior is designed to happen only if the the resource specifies messenger="identified_input" and the input DTO implements a specific IdentifiedInputInterface.
This has been working very well for us and we use it all the time, but still I'm curious for alternative solutions since this one seems a bit kludgy.
Here we go:
interface IdentifiedInputInterface
{
public function setId($id);
}
Custom DataTransformer:
/**
* Enrich input with the id of the incoming resource.
*
* Copy/pasted from ApiPlatform\Core\Bridge\Symfony\Messenger\DataTransformer with minor changes
*
* This gives the ability to send the Input along with the resource id to a message handler.
*
* Usage: use messenger="identified_input" annotation on the resource/operation.
*/
final class IdentifiedDataTransformer implements DataTransformerInterface
{
private $resourceMetadataFactory;
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory)
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
}
/**
* {@inheritdoc}
*/
public function transform($object, string $to, array $context = [])
{
if ($object instanceof IdentifiedInputInterface && $context['object_to_populate']) {
$object->setId($context['object_to_populate']->getId());
}
return $object;
}
/**
* {@inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
if (
\is_object($data) // data is not normalized yet, it should be an array
||
null === ($context['input']['class'] ?? null)
) {
return false;
}
$metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? $to);
if (isset($context['graphql_operation_name'])) {
return 'identified_input' === $metadata->getGraphqlAttribute($context['graphql_operation_name'], 'messenger', null, true);
}
if (!isset($context['operation_type'])) {
return 'identified_input' === $metadata->getAttribute('messenger');
}
return 'identified_input' === $metadata->getTypedOperationAttribute(
$context['operation_type'],
$context[$context['operation_type'].'_operation_name'] ?? '',
'messenger',
null,
true
);
}
}
Use with this trait for concision in DTOs:
trait IdentifiedInputTrait
{
protected $id;
public function setId($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
}
Example DTO:
class MyCustomDTO implements IdentifiedInputInterface
{
use IdentifiedInputTrait;
/* ... */
}
Example handler:
class MyCustomDTOHandler implements MessageHandlerInterface
{
public function __invoke(MyCustomDTO $request)
{
$id = $request->getId();
/* ... */
}
}
Exemple annotation:
* "putMyCustomOp" = {
* "method" = "PUT",
* "path" = "/myresource/{id}/mycustomop",
* "messenger" = "identified_input",
* "input" = MyCustomDTO::class,
* },
Closing ApiPlatform should have enough extensions point to make all these things work. Let us know if you hit a bug or think we can improve the DX somewhere. @ragboyjr if you still have no solutions for this please let me know.
@soyuka Sounds good, I tend to agree with you, thanks for all the work.
Most helpful comment
Can someone update the documentation to help people further?