thecodingmachine / graphqlite

Use PHP Attributes/Annotations to declare your GraphQL API
https://graphqlite.thecodingmachine.io
MIT License
555 stars 95 forks source link

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

Open aszenz opened 1 year ago

aszenz commented 1 year ago

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.

oojacoboo commented 1 year ago

@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.

aszenz commented 1 year ago

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:

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.

oojacoboo commented 1 year ago

@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.

aszenz commented 1 year ago

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)