[GraphQl] Support for unions and interfaces for search functionality
I am trying to create a general search functionality in my app that can return multiple types of objects. For each type of object I want to retrieve different fields.
In the graphQl specification this is supported using interfaces and unions. See this example below:
type Query {
# Search across all content
search(query: String!): [SearchResult]
}
union SearchResult = Conference | Festival | Concert | Venue
query {
search(query: "Madison") {
... on Venue {
id
name
address
}
... on Festival {
id
name
performers
}
... on Concert {
id
name
performingBand
}
... on Conference {
speakers
workshops
}
}
}
See this guide for more details.
I tried making all my entities extend a 'base' entity and creating the search query on that entity, but that did not work.
Thanks in advance!
Is this something that is planned or any advice how to deal with it meanwhile? This rules out a big part of GraphQL capabilities.
would love to see this implemented. in a meantime I got fragments working for collection for my setup like this.
I have different types, not OneComment and AnotherComment. this is just for demonstration
// api/src/Entity/Comment.php
//...
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
'one' => OneComment::class,
'another' => AnotherComment::class,
])]
class Comment
{
//...
// api/src/Entity/OneComment.php
//...
class OneComment extends Comment
{
//...
// api/src/Entity/AnotherComment.php
//...
class AnotherComment extends Comment
{
//...
# api/config/services.yaml:
# ...
App\Type\TypeConverter:
decorates: api_platform.graphql.type_converter
App\Type\UnionCommentType:
tags:
- { name: api_platform.graphql.type }
ApiPlatform\GraphQl\Type\TypeBuild
// api/src/Type/TypeConverter.php
<?php
namespace App\Type;
use ApiPlatform\GraphQl\Type\TypeConverterInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use App\Entity\Comment;
use GraphQL\Type\Definition\Type as GraphQLType;
use Symfony\Component\PropertyInfo\Type;
final class TypeConverter implements TypeConverterInterface
{
public function __construct(
private TypeConverterInterface $defaultTypeConverter,
private UnionCommentType $unionCommentType,
) {
}
/**
* {@inheritdoc}
*/
public function convertType(
Type $type,
bool $input,
Operation $rootOperation,
string $resourceClass,
string $rootResource,
?string $property,
int $depth
): GraphQLType|string|null {
if ($type->isCollection() && $type->getCollectionValueTypes()[0]->getClassName() === Comment::class) {
return $this->unionCommentType;
}
return $this->defaultTypeConverter->convertType(
type: $type,
input: $input,
rootOperation: $rootOperation,
resourceClass: $resourceClass,
rootResource: $rootResource,
property: $property,
depth: $depth,
);
}
/**
* {@inheritdoc}
*/
public function resolveType(string $type): ?GraphQLType
{
return $this->defaultTypeConverter->resolveType($type);
}
}
// api/src/Type/UnionCommentType.php
<?php
namespace App\Type;
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use App\Entity\OneComment;
use App\Entity\AnotherComment;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\UnionType;
final class UnionCommentType extends UnionType implements TypeInterface
{
/**
* @var array<string, ObjectType>
*/
private array $commentTypes;
public function __construct(
private readonly TypeBuilderEnumInterface $typeBuilder,
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
) {
$this->name = 'UnionComment';
$this->description = 'Union type for all Comment types';
$this->astNode = null;
$this->extensionASTNodes = [];
$this->commentTypes = [
OneComment::class => $this->getCommentType(OneComment::class),
AnotherComment::class => $this->getCommentType(AnotherComment::class),
];
$this->config = [
'name' => 'UnionComment',
'description' => 'Union type for all Comment types',
'types' => $this->commentTypes,
'resolveType' => fn ($objectValue) => $this->commentTypes[$objectValue['#itemResourceClass']],
'astNode' => null,
'extensionASTNodes' => [],
];
}
public function getName(): string
{
return 'UnionComment';
}
private function getCommentType(string $resourceClass): ObjectType
{
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
$operationName = 'collection_query';
$operation = $resourceMetadataCollection->getOperation($operationName);
assert($operation instanceof QueryCollection);
$type = $this->typeBuilder->getResourceObjectType(
resourceClass: $resourceClass,
resourceMetadataCollection: $resourceMetadataCollection,
operation: $operation,
input: false,
wrapped: false,
depth: 0
);
assert($type instanceof ObjectType);
return $type;
}
}
query {
comments() {
collection {
id
... on OneComment {
id
body
}
... on AnotherComment {
id
body
}
}
}
}