thecodingmachine / graphqlite

Use PHP Attributes/Annotations to declare your GraphQL API

Home Page:https://graphqlite.thecodingmachine.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Mapping multiple php object representations for a single resource to one graphql ObjectType

aszenz opened this issue · comments

commented

We use graphqlite's external type declaration feature to map entities to graphql object types but not all of our services return entities, some of them return dto's, we then have to map these dto's into another graphql type causing the api to have multiple types that actually represent the same underlying resource.

Example:

I would like the ProductEntity and ProductDto to map to the same graphql object type Product.

Basically I need a way to map multiple php objects to the same underlying graphql type.

Here's how i imagine it could work:

class ProductEntity {
 public int $id;
 public string $code;
 public array $descriptions;
}

class ProductDto {
 public int $id;
 public string $code;
}

#[Type(classes: [ProductEntity::class, ProductDto::class], name: 'Product')]
class ProductOutputType
{
    public function __construct(private ProductRepository $productRepository) { }

    #[Field]
    public function getCode(Product|ProductDto $product): string
    {
        return $product->code;
    }

    /**
     * @return string[]
     */
    #[Field]
    // Notice how we are forced to map multiple objects represented by a type union
    // Since dto doesn't contain descriptions we can simply fetch it from another service
    public function getDescriptions(ProductEntity|ProductDto $product): array
    {
        if($product instanceof ProductEntity) {
            return $product->descriptions;
        }
        return $this->productRepository->getDescriptions($product->id);
    }
}

I realize this may seem like a strange feature so I'm curious how other people are solving this issue, are they duplicating graphql types for a single resource or mapping everything manually to one type.

@aszenz we likely have some similar scenarios, but handling it differently.

Firstly, I'd question why your services need to return DTOs that are so similar to your entities. Why not just instantiate and pass around the entity? I realize this may be a Doctrine managed entity, with other associated functionality. But, properly written Doctrine entities do not require persistence or management from Doctrine. That's what makes Doctrine and the DataMapper pattern great.

Additionally, there is the ExtendType attribute that allows you to add custom fields, or modify the output of field values in a GraphQL context. We use a fair number of these for various situations.

Some input on the above would be helpful.

commented

Firstly, I'd question why your services need to return DTOs that are so similar to your entities

Our entities are not similar to dto's, they contain more fields and associations.

This causes two issues:

  • Entities are harder to instantiate, especially from raw sql queries
  • Our team prefers dtos to get the exact data they need for read models, this relates to DDD and reducing coupling, while this is nice for testing, in the api layer i would prefer to expose rich models that can be queried further even if the performance is slower for some fields

Additionally, there is the ExtendType attribute that allows you to add custom fields, or modify the output of field values in a GraphQL context. We use a fair number of these for various situations.

From my understanding it's useful for creating more fields in the schema than available on the object, we already use external type declarations so all of our type mappers are services. So how would we use ExtendType to get a single graphql type for two different objects?

We could map dtos instead of entities for the api layer, but we also have multiple dtos representing the same resources in different modules of the app.

Main challenge is how can I create a unified api model when the underlying domain services represent the same concepts (like Product) slightly differently.

@aszenz I don't think I have the full picture here. I understand what you guys are doing and basically why, and that mostly sounds okay, even though I'd argue that some of it's probably not necessary, or even advantageous, while some of it is likely a big benefit.

That said, the issue is with your queries, correct?

We use DTOs for all of our mutations and that's worked well.

As for your queries... I'm guessing you have issues where you have a model/entity defined as a type and then are trying to get a DTO as a field/reference?

If you based your types on your entities primarily, and then used ExtendType on those entities to add support for your DTO relationships, I think that'd get the job done. And the same could be done on DTOs that need fields to your entity types.

commented

If you based your types on your entities primarily, and then used ExtendType on those entities to add support for your DTO relationships, I think that'd get the job done. And the same could be done on DTOs that need fields to your entity types.

Yes that's certainly a valid option we could use, although it still creates two object types that i have to keep in sync then (i.e they contain the same fields)