Skip to content

Instantly share code, notes, and snippets.

@nuryagdym
Last active January 15, 2024 09:16
Show Gist options
  • Save nuryagdym/57f4734639213b6f4b81676f425370f5 to your computer and use it in GitHub Desktop.
Save nuryagdym/57f4734639213b6f4b81676f425370f5 to your computer and use it in GitHub Desktop.
api-platform v3 - Create a service for Bramble GraphQl Federation Support
# if you have TRUSTED_HOSTS config in your environments,
# add host.docker.internal into it so that local development with docker works
TRUSTED_HOSTS=^(localhost|php|host.docker.internal)$
api_platform:
title: GraphQL API
version: 1.0.0
formats:
jsonld: ['application/ld+json']
# Bramble sends content-type: application/json; charset=utf-8
graphql: ['application/graphql', "application/json; charset=utf-8"]
<?php
declare(strict_types=1);
namespace App\Util;
class BrambleSchemaAdapter
{
/**
* Changes service type to be compatible with Bramble requirements
*/
public function execute(string $graphqlSchema): string
{
$fixedSchema = \str_replace("service: Service\n", "service: Service!\n", $graphqlSchema);
$fixedSchema = \str_replace("type Service implements Node {", "type Service {", $fixedSchema);
// removes `id: ID!` field from service schema
$fixedSchema = \preg_replace("/type Service {\n\s+id: ID!/", "type Service {", $fixedSchema);
return $fixedSchema;
}
}
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GraphQl\Query;
use App\Resolver\BrambleServiceResolver;
/**
* Needed resource for Bramble SuperGraph to work
*/
#[ApiResource(
shortName: 'Service',
types: [],
operations: [
// Although we don't need RestAPI endpoint, without this operation we get error: "Not able to retrieve identifiers."
new Get(),
],
graphQlOperations: [
new Query(
resolver: BrambleServiceResolver::class,
args: [ // set args empty so that it does not require ID input
],
),
],
)]
class BrambleService
{
public function __construct(
public readonly string $version,
public readonly string $schema,
public readonly string $name = 'my-service-1',
)
{
}
}
<?php
declare(strict_types=1);
namespace App\Resolver;
use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface;
use App\ApiResource\BrambleService;
use App\Service\GraphQL\SchemaGeneratorService;
class BrambleServiceResolver implements QueryItemResolverInterface
{
public function __construct(
private readonly SchemaGeneratorService $schemaGeneratorService,
) {}
/**
* @param BrambleService|null $item
*
* @return BrambleService
*/
public function __invoke($item, array $context): BrambleService
{
$version = '1.0.0' // make it dynamic
return new BrambleService(
$version,
$this->schemaGeneratorService->getSchema($version),
);
}
}
<?php
declare(strict_types=1);
namespace App\Service\GraphQL;
use App\Util\BrambleSchemaAdapter;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpKernel\KernelInterface;
class SchemaGeneratorService
{
public function __construct(
#[Autowire(param: 'kernel.project_dir')]
private readonly string $projectDir,
private readonly KernelInterface $kernel,
private readonly BrambleSchemaAdapter $brambleSchemaAdapter,
) {}
public function getSchema(string $version): string
{
$schemaPath = \sprintf('%s/var/schema-%s.graphql', $this->projectDir, $version);
if (!\file_exists($schemaPath)) {
$this->generateSchema($schemaPath);
}
$content = \file_get_contents($schemaPath);
return $this->brambleSchemaAdapter->execute($content);
}
private function generateSchema(string $schemaPath): int
{
$application = new Application($this->kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'api:graphql:export',
'--output' => $schemaPath,
]);
return $application->run($input, new NullOutput());
}
}
@nuryagdym
Copy link
Author

nuryagdym commented Jan 15, 2024

API-Platform v3.2

Description about the solution.
Bramble GraphQl Federation requires to creating following type and query:

type Service {
  name: String! # unique name for the service
  version: String! # any string
  schema: String! # the full schema for the service
}

type Query {
  service: Service!
}

The first issue with this type is API-Platform always needs an ID for resources and it always adds it to all resource implicitly.
The second one is to make query type required Service! instead of Service.
In order Bramble to work these services must be defined exactly as it is. For example, having an additional field such as id in type Service will cause Bramble to fail.

I could not find a proper way of solving this issues currently, so I ended up with this solution, by:

  1. disabling id requirement for item_query so that generated Query will be service: Service instead of service(id: ID!): Service. This is done by setting new Query(args: []).
  2. changing service.schema that is shared with Bramble using BrambleSchemaAdapter so that Bramble accepts it. If we generate schema with implementation above (php bin/console api:graphql:export -o ./schema.graphql) it will out put following schema:
type Query {
  node(id: ID!): Node

  "Needed resource for Bramble SuperGraph to work"
  service: Service
}

interface Node {
  "The id of this node."
  id: ID!
}

"Needed resource for Bramble SuperGraph to work"
type Service implements Node {
  id: ID!
  version: String!
  schema: String!
  name: String!
}

and BrambleSchemaAdapter converts it to:

type Query {
  node(id: ID!): Node

  "Needed resource for Bramble SuperGraph to work"
  service: Service!
}

interface Node {
  "The id of this node."
  id: ID!
}

"Needed resource for Bramble SuperGraph to work"
type Service {
  version: String!
  schema: String!
  name: String!
}

You will also need to allow few headers in Bramble config.json:

{
    "services": [
        "http://host.docker.internal:8085/graphql"
    ],
    "plugins": [
        {
            "name": "headers",
            "config": {
                "allowed-headers": [
                    "authorization",
                    "content-type",
                    "origin"
                ]
            }
        }
    ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment