Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
karrakoliko opened this issue Apr 10, 2024 · 5 comments

Comments

@karrakoliko
Copy link

karrakoliko commented Apr 10, 2024

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

@soyuka
Copy link
Member

soyuka commented Apr 11, 2024

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

    public function getRatings(): array

@karrakoliko
Copy link
Author

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;
    }

@GwendolenLynch
Copy link
Contributor

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

@karrakoliko
Copy link
Author

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

@karrakoliko
Copy link
Author

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"
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants