Bruno Guignard
Bruno Guignard

Reputation: 303

Why testing API Platform PUT and PATCH requests does not work but API works with same operations with custom components with DTO

I use Doctrine objects in database that are mapped to DTOs by services with a getComponentDtoFromEntity($component) method to expose them on API Platform 3.2 on Symfony 7.1 and PHP 8.3. Each DTO has a route with needed HTTP verbs which allows me to work only with data I need, have multiple endpoints using the same entity (with different DTO), having composite DTOs (a mix of multiples entities) and never expose directly my entities. I created some DTO directly linked to API Platform, for example :

#[ApiResource]
#[Get(
    uriTemplate: 'stock/components/{id}',
    read: true,
    provider: ComponentDtoProviderService::class,
)]
#[GetCollection(
    uriTemplate: 'stock/components/',
    provider: ComponentDtoProviderService::class
)]
#[Post(
    uriTemplate: 'stock/components/',
    validationContext: ['groups' => ['Default', 'postValidation']],
    processor: ComponentDtoProcessorService::class,
)]
#[Delete(
    uriTemplate: 'stock/components/{id}',
    provider: ComponentDtoProviderService::class,
    processor: ComponentDtoProcessorService::class
)]
#[Put(
    uriTemplate: 'stock/components/',
    validationContext: ['groups' => ['Default']],
    processor: ComponentDtoProcessorService::class
)]
readonly class ComponentDto
{
    public function __construct(
        public ?string $id,
        #[Assert\NotBlank(groups: ['postValidation'])]
        public string $name,
        public ?string $ean13,
        public ?int $quantity,
        public ?int $quantityThreshold,
    ) {
    }
}

When I use the built-in API test at /api address, everything works fine with every operation (GET, POST, PUT, DELETE).

I use PHPunit to test my API, so I created tests for get, getAll, post, delete and put/patch operations using previously added fixtures (that I get from name).

    public function testComponentsPut(): void
    {
        $componentBlueRose = $this->componentRepository->findByName(ComponentFixtures::COMPONENT_BLUE_ROSE);
        self::assertNotNull($componentBlueRose);

        // test new EAN13 and quantity and quantityThreshold
        $componentBlueRoseDtoModified = new ComponentDto(
            $componentBlueRose->getId()->toString(),
            $componentBlueRose->getName(),
            '9234569990123', // data are modified here
            400, // data are modified here
            8, // data are modified here
        );

        $response = parent::createClient()->request('PUT', 'api/stock/components/',
            ['json' => $componentBlueRoseDtoModified]);

        self::assertResponseIsSuccessful();
        $componentBlueRoseUpdated = $this->componentRepository->findByName(ComponentFixtures::COMPONENT_BLUE_ROSE);

        // todo : its not working here
        self::assertSame($componentBlueRoseDtoModified->ean13, $componentBlueRoseUpdated->getEan13());
        self::assertSame($componentBlueRoseDtoModified->quantity, $componentBlueRoseUpdated->getQuantity());
        self::assertSame($componentBlueRoseDtoModified->quantityThreshold, $componentBlueRoseUpdated->getQuantityThreshold());
}

Previous tests are made with Get, Post, Delete and GetAll and everything works fine, but not in the case of PUT and PATCH : when I get the saved data in database, the data is never modified. To test, I take an object in database, I map it in a DTO, then I modify the DTO and send it in API with PUT verb (I also did the same with PATCH). Then, I get the object with same ID in database and I compare both objects properties to check that modifications have been saved. But they are not.

This made me discovering a strange behavior of API Platform : When PATCH or PUT operations are called, the Provider is always called even if it's not specified, and it's called BEFORE the processor witch, in fact does :

The problem is that the Processor saves the DTO taken from the database and not the one given in the first place, and modifications are never saved.

Since I know that when /{id} is specified, a Provider is required for Patch and Put operations (in recent Api Platform versions), I tried both paths :

Here is the Provider code :

public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|ComponentDto
    {
        // get all components
        if ($operation instanceof CollectionOperationInterface) {
            $components = $this->componentRepository->getAll();
            $componentDtos = [];
            foreach ($components as $component) {
                $componentDtos[] = $this->getComponentDtoFromEntity($component);
            }

            return $componentDtos;
        }

        // get one component
        if (!isset($uriVariables['id'])) {
            throw new \Exception('Id is required');
        }
        $component = $this->componentRepository->findByIdAsString($uriVariables['id']);

        if (null === $component) {
            throw new \Exception('Component not found');
        }

        return $this->getComponentDtoFromEntity($component);
    }

    public function getComponentDtoFromEntity(Component $component): ComponentDto
    {
        return new ComponentDto(
            $component->getId() === null ? null : (string) $component->getId(),
            $component->getName(),
            $component->getEan13(),
            $component->getQuantity(),
            $component->getQuantityThreshold(),
        );
    }

Well the questions are :

Upvotes: 0

Views: 302

Answers (2)

PIYUSH CHAUDHARI
PIYUSH CHAUDHARI

Reputation: 1

add next

Implement a Data Transformer Data transformers are crucial for converting between DTOs and entities. Ensure your transformer is correctly implemented.

Upvotes: -2

PIYUSH CHAUDHARI
PIYUSH CHAUDHARI

Reputation: 1

When dealing with issues in PUT and PATCH requests in API Platform, especially when custom DTOs and components are involved, there are several factors to consider. Below is a solution based on common pitfalls:

Solution Ensure DTO and Entity Alignment

Make sure your DTO (Data Transfer Object) aligns with your entity structure. A mismatch here can cause issues with PUT and PATCH operations.

// src/Dto/UserInput.php
namespace App\Dto;

use Symfony\Component\Validator\Constraints as Assert;

class UserInput
{
    /**
     * @Assert\NotBlank
     * @Assert\Length(max=100)
     */
    public $name;

    /**
     * @Assert\NotBlank
     * @Assert\Email
     * @Assert\Length(max=100)
     */
    public $email;
}

Upvotes: -3

Related Questions