api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.38k stars 846 forks source link

Doctrine ODM Documents return inconsistent data for GET Collection and Item Operations in Symfony production environment #6428

Open nasAtchia opened 2 weeks ago

nasAtchia commented 2 weeks ago

API Platform version(s) affected: 3.3.5

Description

The GET Collection and Item operations do not always return the latest data for document resources after updating a resource via the PATCH or PUT operations; sometimes the old data is returned.

Important: This happens only in the production environment docker compose -f compose.yaml -f compose.prod.yaml up --build. I don't know if this is related to the production setup.

How to reproduce

<?php

declare(strict_types=1);

namespace App\Document;

use ApiPlatform\Metadata\ApiResource;
use App\Repository\ProductRepository;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Types\Type;

#[ApiResource]
#[ODM\Document(collection: 'products', repositoryClass: ProductRepository::class)]
class Product
{
    #[ODM\Id(type: Type::INT, strategy: 'INCREMENT')]
    private ?int $id = null;

    #[ODM\Field(type: Type::STRING, nullable: true)]
    private ?string $name = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(?string $name): static
    {
        $this->name = $name;

        return $this;
    }
}
  1. Create a new Product via the POST API.
  2. Update the Product data via the PATCH or PUT API.
  3. Hit the GET Collection of Products or the GET Single Product APIs multiple times, and you'll see that sometimes the latest data is not returned.

Use this repo for testing the above.

Possible Solution

I had to create custom state provider for the GET item operation and call the refresh method after retrieving the object from the built-in state provider.

Custom state provider:

<?php

declare(strict_types=1);

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Document\Product;
use App\Repository\ProductRepository;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final readonly class ProductProvider implements ProviderInterface
{
    public function __construct(
        #[Autowire(service: 'api_platform.doctrine_mongodb.odm.state.item_provider')]
        private ProviderInterface $itemProvider,
        private ProductRepository $productRepository
    ) {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?Product
    {
        /** @var Product|null $product */
        $product = $this->itemProvider->provide($operation, $uriVariables, $context);

        if (!$product) {
            return null;
        }

        $this->productRepository->getDocumentManager()->refresh($product);

        return $product;
    }
}

Updated document:

<?php

declare(strict_types=1);

namespace App\Document;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Repository\ProductRepository;
use App\State\ProductProvider;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Types\Type;

#[ApiResource(
    operations: [
        new GetCollection(),
        new Post(),
        new Get(provider: ProductProvider::class),
        new Patch(),
        new Put(),
        new Delete(),
    ],
)]
#[ODM\Document(collection: 'products', repositoryClass: ProductRepository::class)]
class Product
{
    #[ODM\Id(type: Type::INT, strategy: 'INCREMENT')]
    private ?int $id = null;

    #[ODM\Field(type: Type::STRING, nullable: true)]
    private ?string $name = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(?string $name): static
    {
        $this->name = $name;

        return $this;
    }
}

I haven't found a solution for the Get Collection operation yet.

Additional Context

Recording: https://www.awesomescreenshot.com/video/28637492?key=483e51522dd7bd03d915244bf090e6ff