core icon indicating copy to clipboard operation
core copied to clipboard

JsonLD: plain array converted to hydra:member inside Get operation in custom StateProvider

Open karrakoliko opened this issue 1 year ago • 5 comments

API Platform version(s) affected: "api-platform/core": "^3.1"

Description

By some reason, same dto inside CollectionProvider and GetProvider have different output.

I have custom resource Store:

#[ApiResource(
    operations: [],
    normalizationContext: [
        'groups' => ['store:read'],
        'skip_null_values' => false
    ]

)]
#[GetCollection(
    provider: StoreCollectionProvider::class,
)]
#[Get(
    uriTemplate: '/stores/uuid/{uuid}',
    uriVariables: ['uuid'],
    provider: StoreByUuidProvider::class
)]
class Store
{
 // empty here
}

Both state provider's return same StoreDTO (array of DTO or single DTO):

class StoreDto
{
    #[Groups(['store:read'])]
    public string $uuid;

    #[Groups(['store:read'])]
    public function getRatings(): array
    {
        /**
         * ratings array look like this (json encoded):
         * {"googleMaps":{"aggregate":"4.4"}}
         */
        return $this->ratings;
    }
}

Inside StoreCollectionProvider i return array of DTO's, i get following response (notice how ratings field rendered):

{
  "@context": "/api/contexts/Store",
  "@id": "/api/stores",
  "@type": "hydra:Collection",
  "hydra:totalItems": 1,
  "hydra:member": [
    {
      "@type": "StoreDto",
      "@id": "/api/.well-known/genid/81b3efd58cff9348e116",
      "uuid": "7097fc06-6796-4089-9ad2-efae128a5dda",
      "ratings": {
        "googleMaps": {
          "aggregate": "4.4"
        }
      }
    }
  ],
  "hydra:view": {
    "@id": "/api/stores?itemsPerPage=12",
    "@type": "hydra:PartialCollectionView"
  }
}

If i return same DTO inside StoreByUuidProvider, by some reason i get ratings rendered as collection:

{
  "@context": {
    "@vocab": "http://sharmax.karrakoliko.local/api/docs.jsonld#",
    "hydra": "http://www.w3.org/ns/hydra/core#",
    "uuid": "StoreDto/uuid",
    "ratings": {
      "@id": "StoreDto/ratings",
      "@type": "@id"
    }
  },
  "@type": "StoreDto",
  "@id": "/api/.well-known/genid/19ca9bc27f5d80ba23b5",
  "uuid": "7097fc06-6796-4089-9ad2-efae128a5dda",
  "ratings": {
    "@context": "/api/contexts/Store",
    "@id": "/api/stores/uuid/7097fc06-6796-4089-9ad2-efae128a5dda",
    "@type": "hydra:Collection",
    "hydra:totalItems": 1,
    "hydra:member": [
      {
        "aggregate": "4.4"
      }
    ]
  }
}

Expected

ratings field will be the rendered the same for StoreCollectionProvider and StoreByUuidProvider

In StoreByUuidProvider I expected it to be like this:

{
      "ratings": {
        "googleMaps": {
          "aggregate": "4.4"
        }
      }
}

Actual

In StoreByUuidProvider I got this:

{
  "ratings": {
    "@context": "/api/contexts/Store",
    "@id": "/api/stores/uuid/7097fc06-6796-4089-9ad2-efae128a5dda",
    "@type": "hydra:Collection",
    "hydra:totalItems": 1,
    "hydra:member": [
      {
        "aggregate": "4.4"
      }
    ]
  }
}

I tried to modify #[ApiProperty] attribute over ratings (adding readableLink: false, overriding jsonLd/openapi contexts) - no luck

karrakoliko avatar Apr 10 '24 16:04 karrakoliko

Can you show the provider? also why is this typed array if it returns a string?

    public function getRatings(): array

soyuka avatar Apr 11 '24 07:04 soyuka

Can you show the provider? also why is this typed array if it returns a string?

    public function getRatings(): array

no, it returns array:

json_decode('{"googleMaps":{"aggregate":"4.4"}}',true);

Provider code is complicated, as it resolves concrete stores set from config, and then convert each of them to StoreDto. Omitting unrelated details, it looks like this:

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if(!array_key_exists('uuid',$uriVariables)){
            throw new \RuntimeException('No uuid provided');
        }

        $uuid = UuidV4::fromString($uriVariables['uuid']);

        /** @var StoreInterface $store */
        foreach ($this->stores as $store) {

            if (!$store->getUuid()->equals($uuid)) {
                continue;
            }

            $store->addRating(GoogleMapsRatingProvider::NAME, $this->googleRatingProvider->getAggregateRating($store));
            break;
        }
        
        $storeDto = StoreDto::createFromStore($store);

        return $storeDto;
    }

karrakoliko avatar Apr 11 '24 07:04 karrakoliko

Does adding output: StoreDto::class to each of the operations work?

GwendolenLynch avatar Apr 11 '24 07:04 GwendolenLynch

I have rewritten code to use ApiResource directly, with no DTO use at all, and it works now as expected.

karrakoliko avatar Apr 11 '24 07:04 karrakoliko

Does adding output: StoreDto::class to each of the operations work?

it does, but then jsonLd context lost (no @id, etc):

// apiResource
#[Get(
    uriTemplate: 'stores/uuid/{uuid}',
    uriVariables: ['uuid'],
    output: StoreDto::class,
    provider: StoreByUuidProvider::class
)]
class Store {/* ... */}

response:

{
...
  "ratings": {
    "googleMaps": {
      "aggregate": "4.4"
    }
  }
}

karrakoliko avatar Apr 11 '24 07:04 karrakoliko

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.

stale[bot] avatar Jun 10 '24 19:06 stale[bot]