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

Implement Apollo Federation v2 #26

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2a39bee
Use Enum class as single source of directive names
a-melnikov Jul 28, 2022
c0ff092
Use constants as single source of entity config options names
a-melnikov Jul 29, 2022
68ec4c5
Fix code style
a-melnikov Jul 29, 2022
90231df
Create TypeEnum as single source of type names
a-melnikov Jul 29, 2022
9af5719
Fix code style
a-melnikov Jul 29, 2022
877810b
Fix code style
a-melnikov Jul 29, 2022
268eee6
Print simple and compound directives' argument "fields"
a-melnikov Aug 1, 2022
0769802
Declare reserved names
a-melnikov Aug 1, 2022
6849706
Fix code style
a-melnikov Aug 1, 2022
df46520
Fixes during investigation
a-melnikov Aug 1, 2022
79e14dc
Update phpDocs
a-melnikov Aug 1, 2022
245f9ad
Make printer better extendable
a-melnikov Aug 1, 2022
cd86cde
Extend base schema printer and reduce copy-paste
a-melnikov Aug 2, 2022
5077032
Reduce copy-paste & fix code style
a-melnikov Aug 2, 2022
d23332d
Refactor code to make more readable
a-melnikov Aug 2, 2022
5724b2c
Fix code style
a-melnikov Aug 2, 2022
3e217de
Extract FederatedSchemaTrait
a-melnikov Aug 2, 2022
4a21ab5
Fix code style
a-melnikov Aug 2, 2022
8ad0047
Fix tests code style
a-melnikov Aug 2, 2022
e0c45d7
Fix method call params order
a-melnikov Aug 2, 2022
10a2a4a
Fix code style
a-melnikov Aug 2, 2022
b5d5e48
Sort schema printer methods
a-melnikov Aug 3, 2022
17bf125
Add directives of Apollo Federation v2
a-melnikov Aug 2, 2022
c0f3941
Implement directive @link
a-melnikov Aug 4, 2022
1bac625
Add handling of argument "resolvable" of directive @key
a-melnikov Aug 4, 2022
970ca0b
Extend validation of type reference
a-melnikov Aug 2, 2022
75f8b3a
Add validation if Referenced entity has only one @key directive.
a-melnikov Aug 2, 2022
86ae658
Rename file "phpunit.xml" to "phpunit.xml.dist" to stick to standard …
a-melnikov Aug 4, 2022
ae58a6e
Extract getting of required federated directives to the schema builder
a-melnikov Aug 5, 2022
78ba78c
Fix extendability of schema
a-melnikov Aug 5, 2022
fe7456c
Update phpdoc
a-melnikov Aug 29, 2022
258c46f
Merge branch 'main' into implement_v2
a-melnikov Aug 29, 2022
408c3e1
Remove deprecated excess classes imports
a-melnikov Aug 29, 2022
07a0f9e
Fix merge conflict: make updates equivalent to the commit 4f958ddb683…
a-melnikov Aug 29, 2022
a34d336
Fix documentation
a-melnikov Aug 30, 2022
edce728
Implement tests
a-melnikov Sep 7, 2022
3562fe3
Implement tests
a-melnikov Sep 7, 2022
7002d92
Fix test failed assertion message.
a-melnikov Sep 7, 2022
1b663b5
Update entity reference resolver validation
a-melnikov Oct 27, 2022
60feec3
Remove excess class import
a-melnikov Oct 27, 2022
b818787
Downgrade to PHP 7.1
a-melnikov Feb 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.idea
.vscode
composer.phar
/vendor/
/node_modules/
*.cache
cov.xml
.idea
.vscode
phpunit.xml
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,28 @@ composer require skillshare/apollo-federation-php

### Entities

An entity is an object type that you define canonically in one subgraph and can then reference and extend in other subgraphs. It can be defined via the `EntityObjectType` which takes the same configuration as the default `ObjectType` plus a `keyFields` and `__resolveReference` properties.
An entity is an object type that you define canonically in one subgraph and can then reference and extend in other subgraphs. It can be defined via the `EntityObjectType` which takes the same configuration as the default `ObjectType` plus a `keys` and `__resolveReference` properties.

```php
use Apollo\Federation\Types\EntityObjectType;
use GraphQL\Type\Definition\Type;

$userType = new EntityObjectType([
'name' => 'User',
'keyFields' => ['id', 'email'],
'keys' => [['fields' => 'id'], ['fields' => 'email']],
'fields' => [
'id' => ['type' => Type::int()],
'email' => ['type' => Type::string()],
'firstName' => ['type' => Type::string()],
'lastName' => ['type' => Type::string()]
],
'__resolveReference' => static function ($ref) {
// .. fetch from a data source.
'__resolveReference' => function ($ref) {
// ... fetch from a data source.
}
]);
```

* `keyFields` — defines the entity's primary key, which consists of one or more of the type's. An entity's key cannot include fields that return a union or interface.
* `keys` — defines the entity's unique keys, which consists of one or more of the fields. An entity's key cannot include fields that return a union or interface.

* `__resolveReference` — resolves the representation of the entity from the provided reference. Subgraphs use representations to reference entities from other subgraphs. A representation requires only an explicit __typename definition and values for the entity's primary key fields.

Expand All @@ -49,7 +50,7 @@ use Apollo\Federation\Types\EntityRefObjectType;

$userType = new EntityRefObjectType([
'name' => 'User',
'keyFields' => ['id', 'email'],
'keys' => [['fields' => 'id', 'resolvable' => false]],
'fields' => [
'id' => ['type' => Type::int()],
'email' => ['type' => Type::string()]
Expand All @@ -68,16 +69,12 @@ use Apollo\Federation\Types\EntityRefObjectType;

$userType = new EntityRefObjectType([
'name' => 'User',
'keyFields' => ['id', 'email'],
'keys' => [['fields' => 'id', 'resolvable' => false]],
'fields' => [
'id' => [
'type' => Type::int(),
'isExternal' => true
],
'email' => [
'type' => Type::string(),
'isExternal' => true
]
]
]);
```
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "library",
"license": "MIT",
"require": {
"php": "^7.1||^8.0",
"php": "^7.4||^8.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider dropping support for PHP 7.4 now?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downgraded to PHP 7.1

But it cannot be tested on this version of PHP. It was locked implicitly on php >=7.3 when required PHPunit ^9.5

"phpunit/phpunit": "^9.5",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if the version bump could happen in #31 , and this PR could become mergeable without this PHP bump first. A minor release can happen first, and the version bumps of #31 can happen in a major breaking release.

When considering the version bump the Stitcher blogposts are helpful: https://stitcher.io/blog/php-version-stats-january-2023
The one thing I am always curious about is how much % of these stats are caused by CI pipelines (and hence the actual PHP <= 7.4 usage is lower).

The PHP supported versions is a better guidance, especially when considering a major release as a library.
The old library versions will still remain available through Packagist, and the need for new functionality will stimulate engineers to get their PHP versions updated.

"webonyx/graphql-php": "^0.13.8 || ^14.0"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion phpunit.xml → phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="test/Bootstrap.php">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="test/Bootstrap.php"
>
<coverage>
<include>
<directory suffix=".php">./src</directory>
Expand Down
95 changes: 79 additions & 16 deletions src/Directives.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,125 @@

namespace Apollo\Federation;

use Apollo\Federation\Directives\KeyDirective;
use Apollo\Federation\Directives\ExternalDirective;
use Apollo\Federation\Directives\InaccessibleDirective;
use Apollo\Federation\Directives\KeyDirective;
use Apollo\Federation\Directives\LinkDirective;
use Apollo\Federation\Directives\OverrideDirective;
use Apollo\Federation\Directives\ProvidesDirective;
use Apollo\Federation\Directives\RequiresDirective;
use Apollo\Federation\Directives\ShareableDirective;
use Apollo\Federation\Enum\DirectiveEnum;

/**
* Helper class to get directives for annotating federated entity types.
*/
class Directives
{
/** @var array */
private static $directives;
/**
* @var array{
* external: ExternalDirective,
* inaccessible: InaccessibleDirective,
* key: KeyDirective,
* link: LinkDirective,
* override: OverrideDirective,
* requires: RequiresDirective,
* provides: ProvidesDirective,
* shareable: ShareableDirective,
* }|null
*/
private static ?array $directives = null;

/**
* Gets the @key directive
* Gets the @key directive.
*/
public static function key(): KeyDirective
{
return self::getDirectives()['key'];
return self::getDirectives()[DirectiveEnum::KEY];
}

/**
* Gets the @external directive
* Gets the @external directive.
*/
public static function external(): ExternalDirective
{
return self::getDirectives()['external'];
return self::getDirectives()[DirectiveEnum::EXTERNAL];
}

/**
* Gets the @inaccessible directive.
*/
public static function inaccessible(): InaccessibleDirective
{
return self::getDirectives()[DirectiveEnum::INACCESSIBLE];
}

/**
* Gets the `link` directive.
*/
public static function link(): LinkDirective
{
return self::getDirectives()[DirectiveEnum::LINK];
}

/**
* Gets the @override directive.
*/
public static function override(): OverrideDirective
{
return self::getDirectives()[DirectiveEnum::OVERRIDE];
}

/**
* Gets the @requires directive
* Gets the @requires directive.
*/
public static function requires(): RequiresDirective
{
return self::getDirectives()['requires'];
return self::getDirectives()[DirectiveEnum::REQUIRES];
}

/**
* Gets the @provides directive
* Gets the @provides directive.
*/
public static function provides(): ProvidesDirective
{
return self::getDirectives()['provides'];
return self::getDirectives()[DirectiveEnum::PROVIDES];
}

/**
* Gets the @shareable directive.
*/
public static function shareable(): ShareableDirective
{
return self::getDirectives()[DirectiveEnum::SHAREABLE];
}

/**
* Gets the directives that can be used on federated entity types
* Gets the directives that can be used on federated entity types.
*
* @return array{
* external: ExternalDirective,
* inaccessible: InaccessibleDirective,
* key: KeyDirective,
* link: LinkDirective,
* override: OverrideDirective,
* requires: RequiresDirective,
* provides: ProvidesDirective,
* shareable: ShareableDirective,
* }
*/
public static function getDirectives(): array
{
if (!self::$directives) {
self::$directives = [
'key' => new KeyDirective(),
'external' => new ExternalDirective(),
'requires' => new RequiresDirective(),
'provides' => new ProvidesDirective()
DirectiveEnum::EXTERNAL => new ExternalDirective(),
DirectiveEnum::INACCESSIBLE => new InaccessibleDirective(),
DirectiveEnum::KEY => new KeyDirective(),
DirectiveEnum::LINK => new LinkDirective(),
DirectiveEnum::OVERRIDE => new OverrideDirective(),
DirectiveEnum::REQUIRES => new RequiresDirective(),
DirectiveEnum::PROVIDES => new ProvidesDirective(),
DirectiveEnum::SHAREABLE => new ShareableDirective(),
];
}

Expand Down
11 changes: 6 additions & 5 deletions src/Directives/ExternalDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@

namespace Apollo\Federation\Directives;

use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use Apollo\Federation\Enum\DirectiveEnum;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Directive;

/**
* The `@external` directive is used to mark a field as owned by another service. This
* allows service A to use fields from service B while also knowing at runtime the
* types of that field.
*
* @see https://www.apollographql.com/docs/federation/federated-types/federated-directives/#external
*/
class ExternalDirective extends Directive
{
public function __construct()
{
parent::__construct([
'name' => 'external',
'locations' => [DirectiveLocation::FIELD_DEFINITION]
'name' => DirectiveEnum::EXTERNAL,
'locations' => [DirectiveLocation::FIELD_DEFINITION],
]);
}
}
31 changes: 31 additions & 0 deletions src/Directives/InaccessibleDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Apollo\Federation\Directives;

use Apollo\Federation\Enum\DirectiveEnum;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Directive;

/**
* The `@inaccessible` directive indicates that a field or type should be omitted from the gateway's API schema,
* even if it's also defined in other subgraphs.
*
* @see https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible
*/
class InaccessibleDirective extends Directive
{
public function __construct()
{
parent::__construct([
'name' => DirectiveEnum::INACCESSIBLE,
'locations' => [
DirectiveLocation::FIELD_DEFINITION,
DirectiveLocation::IFACE,
DirectiveLocation::OBJECT,
DirectiveLocation::UNION,
],
]);
}
}
24 changes: 17 additions & 7 deletions src/Directives/KeyDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,38 @@

namespace Apollo\Federation\Directives;

use GraphQL\Type\Definition\Type;
use Apollo\Federation\Enum\DirectiveEnum;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Type;

/**
* The `@key` directive is used to indicate a combination of fields that can be used to uniquely
* identify and fetch an object or interface.
*
* @see https://www.apollographql.com/docs/federation/federated-types/federated-directives/#key
*/
class KeyDirective extends Directive
{
public const ARGUMENT_FIELDS = 'fields';
public const ARGUMENT_RESOLVABLE = 'resolvable';

public function __construct()
{
parent::__construct([
'name' => 'key',
'name' => DirectiveEnum::KEY,
'locations' => [DirectiveLocation::OBJECT, DirectiveLocation::IFACE],
'args' => [
new FieldArgument([
'name' => 'fields',
'type' => Type::nonNull(Type::string())
])
]
'name' => self::ARGUMENT_FIELDS,
'type' => Type::nonNull(Type::string()),
]),
new FieldArgument([
'name' => self::ARGUMENT_RESOLVABLE,
'type' => Type::boolean(),
]),
],
]);
}
}
Loading