Core: Custom Operation with Custom Request DTO

Created on 21 Aug 2018  路  17Comments  路  Source: api-platform/core

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...

Hacktoberfest question

Most helpful comment

Can someone update the documentation to help people further?

All 17 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

soyuka picture soyuka  路  3Comments

DenisVorobyov picture DenisVorobyov  路  3Comments

stipic picture stipic  路  3Comments

theshaunwalker picture theshaunwalker  路  3Comments

vViktorPL picture vViktorPL  路  3Comments