From 2a39beedd2d621212c04f6952aec58385d46c97f Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 17:35:43 +0300 Subject: [PATCH 01/40] Use Enum class as single source of directive names --- src/Directives.php | 24 ++++++++++++++---------- src/Directives/ExternalDirective.php | 7 +++---- src/Directives/KeyDirective.php | 3 ++- src/Directives/ProvidesDirective.php | 3 ++- src/Directives/RequiresDirective.php | 3 ++- src/Enum/DirectiveEnum.php | 26 ++++++++++++++++++++++++++ src/Utils/FederatedSchemaPrinter.php | 3 ++- 7 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 src/Enum/DirectiveEnum.php diff --git a/src/Directives.php b/src/Directives.php index 8e71aa6..8ee46ca 100644 --- a/src/Directives.php +++ b/src/Directives.php @@ -8,21 +8,23 @@ use Apollo\Federation\Directives\ExternalDirective; use Apollo\Federation\Directives\ProvidesDirective; use Apollo\Federation\Directives\RequiresDirective; +use Apollo\Federation\Enum\DirectiveEnum; +use GraphQL\Type\Definition\Directive; /** * Helper class to get directives for annotating federated entity types. */ class Directives { - /** @var array */ - private static $directives; + /** @var array */ + private static $directives = null; /** * Gets the @key directive */ public static function key(): KeyDirective { - return self::getDirectives()['key']; + return self::getDirectives()[DirectiveEnum::KEY]; } /** @@ -30,7 +32,7 @@ public static function key(): KeyDirective */ public static function external(): ExternalDirective { - return self::getDirectives()['external']; + return self::getDirectives()[DirectiveEnum::EXTERNAL]; } /** @@ -38,7 +40,7 @@ public static function external(): ExternalDirective */ public static function requires(): RequiresDirective { - return self::getDirectives()['requires']; + return self::getDirectives()[DirectiveEnum::REQUIRES]; } /** @@ -46,20 +48,22 @@ public static function requires(): RequiresDirective */ public static function provides(): ProvidesDirective { - return self::getDirectives()['provides']; + return self::getDirectives()[DirectiveEnum::PROVIDES]; } /** * Gets the directives that can be used on federated entity types + * + * @return array */ public static function getDirectives(): array { if (!self::$directives) { self::$directives = [ - 'key' => new KeyDirective(), - 'external' => new ExternalDirective(), - 'requires' => new RequiresDirective(), - 'provides' => new ProvidesDirective() + DirectiveEnum::KEY => new KeyDirective(), + DirectiveEnum::EXTERNAL => new ExternalDirective(), + DirectiveEnum::REQUIRES => new RequiresDirective(), + DirectiveEnum::PROVIDES => new ProvidesDirective(), ]; } diff --git a/src/Directives/ExternalDirective.php b/src/Directives/ExternalDirective.php index afd7181..7a837b5 100644 --- a/src/Directives/ExternalDirective.php +++ b/src/Directives/ExternalDirective.php @@ -2,10 +2,9 @@ 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 @@ -17,7 +16,7 @@ class ExternalDirective extends Directive public function __construct() { parent::__construct([ - 'name' => 'external', + 'name' => DirectiveEnum::EXTERNAL, 'locations' => [DirectiveLocation::FIELD_DEFINITION] ]); } diff --git a/src/Directives/KeyDirective.php b/src/Directives/KeyDirective.php index 72469aa..08623d8 100644 --- a/src/Directives/KeyDirective.php +++ b/src/Directives/KeyDirective.php @@ -2,6 +2,7 @@ namespace Apollo\Federation\Directives; +use Apollo\Federation\Enum\DirectiveEnum; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; @@ -16,7 +17,7 @@ class KeyDirective extends Directive public function __construct() { parent::__construct([ - 'name' => 'key', + 'name' => DirectiveEnum::KEY, 'locations' => [DirectiveLocation::OBJECT, DirectiveLocation::IFACE], 'args' => [ new FieldArgument([ diff --git a/src/Directives/ProvidesDirective.php b/src/Directives/ProvidesDirective.php index f33b603..f474cae 100644 --- a/src/Directives/ProvidesDirective.php +++ b/src/Directives/ProvidesDirective.php @@ -2,6 +2,7 @@ namespace Apollo\Federation\Directives; +use Apollo\Federation\Enum\DirectiveEnum; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; @@ -16,7 +17,7 @@ class ProvidesDirective extends Directive public function __construct() { parent::__construct([ - 'name' => 'provides', + 'name' => DirectiveEnum::PROVIDES, 'locations' => [DirectiveLocation::FIELD_DEFINITION], 'args' => [ new FieldArgument([ diff --git a/src/Directives/RequiresDirective.php b/src/Directives/RequiresDirective.php index 6583407..ab8d9e7 100644 --- a/src/Directives/RequiresDirective.php +++ b/src/Directives/RequiresDirective.php @@ -2,6 +2,7 @@ namespace Apollo\Federation\Directives; +use Apollo\Federation\Enum\DirectiveEnum; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; @@ -17,7 +18,7 @@ class RequiresDirective extends Directive public function __construct() { parent::__construct([ - 'name' => 'requires', + 'name' => DirectiveEnum::REQUIRES, 'locations' => [DirectiveLocation::FIELD_DEFINITION], 'args' => [ new FieldArgument([ diff --git a/src/Enum/DirectiveEnum.php b/src/Enum/DirectiveEnum.php new file mode 100644 index 0000000..97ef22b --- /dev/null +++ b/src/Enum/DirectiveEnum.php @@ -0,0 +1,26 @@ +name, ['key', 'provides', 'requires', 'external']); + return \in_array($type->name, DirectiveEnum::getAll()); } /** From c0ff092bf3f79fe93cf03dfdc20fd430b81b3711 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 29 Jul 2022 07:53:33 +0300 Subject: [PATCH 02/40] Use constants as single source of entity config options names --- src/Types/EntityObjectType.php | 26 ++++++++++++++++---------- src/Utils/FederatedSchemaPrinter.php | 14 ++++++++------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index f2e70ad..fe6c421 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -7,28 +7,27 @@ use GraphQL\Utils\Utils; use GraphQL\Type\Definition\ObjectType; -use array_key_exists; - /** * An entity is a type that can be referenced by another service. Entities create * connection points between services and form the basic building blocks of a federated * graph. Entities have a primary key whose value uniquely identifies a specific instance * of the type, similar to the function of a primary key in a SQL table - * (see [related docs](https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#entities-and-keys)). + * {@see https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#entities-and-keys }. * * The `keyFields` property is required in the configuration, indicating the fields that * serve as the unique keys or identifiers of the entity. * * Sample usage: - * + * * $userType = new Apollo\Federation\Types\EntityObjectType([ * 'name' => 'User', * 'keyFields' => ['id', 'email'], * 'fields' => [...] * ]); + * * * Entity types can also set attributes to its fields to hint the gateway on how to resolve them. - * + * * $userType = new Apollo\Federation\Types\EntityObjectType([ * 'name' => 'User', * 'keyFields' => ['id', 'email'], @@ -39,10 +38,17 @@ * ] * ] * ]); - * + * */ class EntityObjectType extends ObjectType { + public const FIELD_KEY_FIELDS = 'keyFields'; + public const FIELD_REFERENCE_RESOLVER = '__resolveReference'; + + public const FIELD_DIRECTIVE_IS_EXTERNAL = 'isExternal'; + public const FIELD_DIRECTIVE_PROVIDES = 'provides'; + public const FIELD_DIRECTIVE_REQUIRES = 'requires'; + /** @var array */ private $keyFields; @@ -54,11 +60,11 @@ class EntityObjectType extends ObjectType */ public function __construct(array $config) { - $this->keyFields = $config['keyFields']; + $this->keyFields = $config[self::FIELD_KEY_FIELDS]; - if (isset($config['__resolveReference'])) { + if (isset($config[self::FIELD_REFERENCE_RESOLVER])) { self::validateResolveReference($config); - $this->referenceResolver = $config['__resolveReference']; + $this->referenceResolver = $config[self::FIELD_REFERENCE_RESOLVER]; } parent::__construct($config); @@ -113,6 +119,6 @@ private function validateReferenceKeys($ref) public static function validateResolveReference(array $config) { - Utils::invariant(is_callable($config['__resolveReference']), 'Reference resolver has to be callable.'); + Utils::invariant(is_callable($config[self::FIELD_REFERENCE_RESOLVER]), 'Reference resolver has to be callable.'); } } diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index bdce6ff..5faa2ee 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -462,16 +462,18 @@ private static function printFieldFederatedDirectives($field) { $directives = []; - if (isset($field->config['isExternal']) && $field->config['isExternal'] === true) { - array_push($directives, '@external'); + if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL]) + && $field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL] === true + ) { + $directives[] = '@external'; } - if (isset($field->config['provides'])) { - array_push($directives, sprintf('@provides(fields: "%s")', $field->config['provides'])); + if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])) { + $directives[] = sprintf('@provides(fields: "%s")', $field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES]); } - if (isset($field->config['requires'])) { - array_push($directives, sprintf('@requires(fields: "%s")', $field->config['requires'])); + if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])) { + $directives[] = sprintf('@requires(fields: "%s")', $field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES]); } return implode(' ', $directives); From 68ec4c503def2a22a138bc145096222f8ec2e8b8 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 29 Jul 2022 11:12:26 +0300 Subject: [PATCH 03/40] Fix code style --- src/Types/EntityObjectType.php | 39 ++++++++----- src/Utils/FederatedSchemaPrinter.php | 87 +++++++++++++++++++--------- 2 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index fe6c421..fd617c4 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -4,8 +4,8 @@ namespace Apollo\Federation\Types; -use GraphQL\Utils\Utils; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Utils\Utils; /** * An entity is a type that can be referenced by another service. Entities create @@ -52,8 +52,8 @@ class EntityObjectType extends ObjectType /** @var array */ private $keyFields; - /** @var callable */ - public $referenceResolver; + /** @var callable|null */ + public $referenceResolver = null; /** * @param mixed[] $config @@ -81,9 +81,7 @@ public function getKeyFields(): array } /** - * Gets whether this entity has a resolver set - * - * @return bool + * Gets whether this entity has a resolver set. */ public function hasReferenceResolver(): bool { @@ -91,34 +89,47 @@ public function hasReferenceResolver(): bool } /** - * Resolves an entity from a reference + * Resolves an entity from a reference. * - * @param mixed $ref - * @param mixed $context - * @param mixed $info + * @param mixed|null $ref + * @param mixed|null $context + * @param mixed|null $info + * + * @retrun mixed|null */ public function resolveReference($ref, $context = null, $info = null) { $this->validateReferenceResolver(); $this->validateReferenceKeys($ref); - $entity = ($this->referenceResolver)($ref, $context, $info); - - return $entity; + return ($this->referenceResolver)($ref, $context, $info); } + /** + * @return void + */ private function validateReferenceResolver() { Utils::invariant(isset($this->referenceResolver), 'No reference resolver was set in the configuration.'); } + /** + * @param array{ __typename: mixed } $ref + * + * @return void + */ private function validateReferenceKeys($ref) { Utils::invariant(isset($ref['__typename']), 'Type name must be provided in the reference.'); } + /** + * @param array{ __resolveReference: mixed } $config + * + * @return void + */ public static function validateResolveReference(array $config) { - Utils::invariant(is_callable($config[self::FIELD_REFERENCE_RESOLVER]), 'Reference resolver has to be callable.'); + Utils::invariant(\is_callable($config[self::FIELD_REFERENCE_RESOLVER]), 'Reference resolver has to be callable.'); } } diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 5faa2ee..d2ad466 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -37,11 +37,16 @@ use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\EnumValueDefinition; +use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\TypeWithFields; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; @@ -86,27 +91,27 @@ public static function doPrint(Schema $schema, array $options = []): string { return self::printFilteredSchema( $schema, - static function ($type) { + static function (Directive $type): bool { return !Directive::isSpecifiedDirective($type) && !self::isFederatedDirective($type); }, - static function ($type) { + static function (Type $type): bool { return !Type::isBuiltInType($type); }, $options ); } - public static function isFederatedDirective($type): bool + public static function isFederatedDirective(Directive $type): bool { - return \in_array($type->name, DirectiveEnum::getAll()); + return \in_array($type->name, DirectiveEnum::getAll(), true); } /** * @param bool[] $options */ - private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options): string + private static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string { - $directives = array_filter($schema->getDirectives(), static function ($directive) use ($directiveFilter) { + $directives = array_filter($schema->getDirectives(), static function (Directive $directive) use ($directiveFilter): bool { return $directiveFilter($directive); }); @@ -120,10 +125,10 @@ private static function printFilteredSchema(Schema $schema, $directiveFilter, $t "\n\n", array_filter( array_merge( - array_map(static function ($directive) use ($options) { + array_map(static function (Directive $directive) use ($options) { return self::printDirective($directive, $options); }, $directives), - array_map(static function ($type) use ($options) { + array_map(static function (Type $type) use ($options) { return self::printType($type, $options); }, $types) ) @@ -132,7 +137,10 @@ private static function printFilteredSchema(Schema $schema, $directiveFilter, $t ); } - private static function printDirective($directive, $options): string + /** + * @param bool[] $options + */ + private static function printDirective(Directive $directive, array $options): string { return self::printDescription($options, $directive) . 'directive @' . @@ -142,7 +150,11 @@ private static function printDirective($directive, $options): string implode(' | ', $directive->locations); } - private static function printDescription($options, $def, $indentation = '', $firstInBlock = true): string + /** + * @param bool[] $options + * @param Directive|EnumValueDefinition|FieldArgument|Type|object $def + */ + private static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string { if (!$def->description) { return ''; @@ -162,13 +174,13 @@ private static function printDescription($options, $def, $indentation = '', $fir } // Format a multi-line block quote to account for leading space. - $hasLeadingSpace = isset($lines[0]) && (substr($lines[0], 0, 1) === ' ' || substr($lines[0], 0, 1) === '\t'); + $hasLeadingSpace = isset($lines[0]) && \in_array(substr($lines[0], 0, 1), [' ', '\t'], true); if (!$hasLeadingSpace) { $description .= "\n"; } - $lineLength = count($lines); + $lineLength = \count($lines); for ($i = 0; $i < $lineLength; $i++) { if ($i !== 0 || !$hasLeadingSpace) { @@ -223,7 +235,10 @@ private static function breakLine(string $line, int $maxLen): array return array_map('trim', $parts); } - private static function printDescriptionWithComments($lines, $indentation, $firstInBlock): string + /** + * @param string[] $lines + */ + private static function printDescriptionWithComments(array $lines, string $indentation, bool $firstInBlock): string { $description = $indentation && !$firstInBlock ? "\n" : ''; @@ -238,12 +253,16 @@ private static function printDescriptionWithComments($lines, $indentation, $firs return $description; } - private static function escapeQuote($line): string + private static function escapeQuote(string $line): string { return str_replace('"""', '\\"""', $line); } - private static function printArgs($options, $args, $indentation = ''): string + /** + * @param bool[] $options + * @param FieldArgument[]|null $args + */ + private static function printArgs(array $options, $args, string $indentation = ''): string { if (!$args) { return ''; @@ -263,7 +282,7 @@ private static function printArgs($options, $args, $indentation = ''): string implode( "\n", array_map( - static function ($arg, $i) use ($indentation, $options) { + static function (FieldArgument $arg, $i) use ($indentation, $options): string { return self::printDescription($options, $arg, ' ' . $indentation, !$i) . ' ' . $indentation . @@ -277,6 +296,9 @@ static function ($arg, $i) use ($indentation, $options) { ); } + /** + * @param InputObjectField|FieldArgument $arg + */ private static function printInputValue($arg): string { $argDecl = $arg->name . ': ' . (string) $arg->getType(); @@ -294,11 +316,12 @@ private static function printInputValue($arg): string public static function printType(Type $type, array $options = []): string { if ($type instanceof ScalarType) { + // TODO: use constant instead of magic scalar value if ($type->name !== '_Any') { return self::printScalar($type, $options); - } else { - return ''; } + + return ''; } if ($type instanceof EntityObjectType || $type instanceof EntityRefObjectType) { @@ -306,11 +329,12 @@ public static function printType(Type $type, array $options = []): string } if ($type instanceof ObjectType) { + // TODO: use constant instead of magic scalar value if ($type->name !== '_Service') { return self::printObject($type, $options); - } else { - return ''; } + + return ''; } if ($type instanceof InterfaceType) { @@ -318,11 +342,12 @@ public static function printType(Type $type, array $options = []): string } if ($type instanceof UnionType) { + // TODO: use constant instead of magic scalar value if ($type->name !== '_Entity') { return self::printUnion($type, $options); - } else { - return ''; } + + return ''; } if ($type instanceof EnumType) { @@ -364,6 +389,7 @@ private static function printObject(ObjectType $type, array $options): string ) : ''; + // FIXME: hardcoded names! They can be different in real. $queryExtends = $type->name === 'Query' || $type->name === 'Mutation' ? 'extend ' : ''; return self::printDescription($options, $type) . @@ -414,13 +440,16 @@ private static function printEntityObject(EntityObjectType $type, array $options /** * @param bool[] $options + * @param EntityObjectType|InterfaceType|ObjectType $type */ - private static function printFields($options, $type): string + private static function printFields(array $options, TypeWithFields $type): string { $fields = array_values($type->getFields()); + // FIXME it looks like hardcoded name. Potentially, it can be different! if ($type->name === 'Query') { - $fields = array_filter($fields, function ($field) { + $fields = array_filter($fields, static function (FieldDefinition $field): bool { + //TODO use constants instead of magic scalar values return $field->name !== '_service' && $field->name !== '_entities'; }); } @@ -428,7 +457,7 @@ private static function printFields($options, $type): string return implode( "\n", array_map( - static function ($f, $i) use ($options) { + static function (FieldDefinition $f, $i) use ($options) { return self::printDescription($options, $f, ' ', !$i) . ' ' . $f->name . @@ -445,6 +474,9 @@ static function ($f, $i) use ($options) { ); } + /** + * @param EnumValueDefinition|FieldDefinition $fieldOrEnumVal + */ private static function printDeprecated($fieldOrEnumVal): string { $reason = $fieldOrEnumVal->deprecationReason; @@ -458,7 +490,7 @@ private static function printDeprecated($fieldOrEnumVal): string return ' @deprecated(reason: ' . Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; } - private static function printFieldFederatedDirectives($field) + private static function printFieldFederatedDirectives(FieldDefinition $field): string { $directives = []; @@ -507,9 +539,10 @@ private static function printEnum(EnumType $type, array $options): string } /** + * @param EnumValueDefinition[] $values * @param bool[] $options */ - private static function printEnumValues($values, $options): string + private static function printEnumValues(array $values, array $options): string { return implode( "\n", From 90231df42ab401c498f25ad028b5bf13a6ecf5af Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 29 Jul 2022 11:22:38 +0300 Subject: [PATCH 04/40] Create TypeEnum as single source of type names --- src/Enum/TypeEnum.php | 25 +++++++++++++++++++++++++ src/FederatedSchema.php | 7 ++++--- src/Utils/FederatedSchemaPrinter.php | 16 +++++++--------- 3 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 src/Enum/TypeEnum.php diff --git a/src/Enum/TypeEnum.php b/src/Enum/TypeEnum.php new file mode 100644 index 0000000..0cbacbd --- /dev/null +++ b/src/Enum/TypeEnum.php @@ -0,0 +1,25 @@ + '_Service', + 'name' => TypeEnum::SERVICE, 'fields' => [ 'sdl' => [ 'type' => Type::string(), @@ -153,12 +154,12 @@ private function getQueryTypeEntitiesFieldConfig(?array $config): array } $entityType = new UnionType([ - 'name' => '_Entity', + 'name' => TypeEnum::ENTITY, 'types' => array_values($this->getEntityTypes()) ]); $anyType = new CustomScalarType([ - 'name' => '_Any', + 'name' => TypeEnum::ANY, 'serialize' => function ($value) { return $value; } diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index d2ad466..6e44ba8 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -33,6 +33,10 @@ namespace Apollo\Federation\Utils; use Apollo\Federation\Enum\DirectiveEnum; +use Apollo\Federation\Enum\TypeEnum; +use Apollo\Federation\Types\EntityObjectType; +use Apollo\Federation\Types\EntityRefObjectType; + use GraphQL\Error\Error; use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; @@ -53,9 +57,6 @@ use GraphQL\Utils\AST; use GraphQL\Utils\Utils; -use Apollo\Federation\Types\EntityObjectType; -use Apollo\Federation\Types\EntityRefObjectType; - use function array_filter; use function array_keys; use function array_map; @@ -316,8 +317,7 @@ private static function printInputValue($arg): string public static function printType(Type $type, array $options = []): string { if ($type instanceof ScalarType) { - // TODO: use constant instead of magic scalar value - if ($type->name !== '_Any') { + if ($type->name !== TypeEnum::ANY) { return self::printScalar($type, $options); } @@ -329,8 +329,7 @@ public static function printType(Type $type, array $options = []): string } if ($type instanceof ObjectType) { - // TODO: use constant instead of magic scalar value - if ($type->name !== '_Service') { + if (TypeEnum::SERVICE !== $type->name) { return self::printObject($type, $options); } @@ -342,8 +341,7 @@ public static function printType(Type $type, array $options = []): string } if ($type instanceof UnionType) { - // TODO: use constant instead of magic scalar value - if ($type->name !== '_Entity') { + if (TypeEnum::ENTITY !== $type->name) { return self::printUnion($type, $options); } From 9af5719170a85f5b1f8ac31490aff9aba9dd7016 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 29 Jul 2022 11:35:26 +0300 Subject: [PATCH 05/40] Fix code style --- src/FederatedSchema.php | 86 +++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index 7b70cd8..6e9970d 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -5,19 +5,19 @@ namespace Apollo\Federation; use Apollo\Federation\Enum\TypeEnum; -use GraphQL\Type\Schema; +use Apollo\Federation\Types\EntityObjectType; +use Apollo\Federation\Utils\FederatedSchemaPrinter; use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; +use GraphQL\Type\Schema; use GraphQL\Utils\TypeInfo; use GraphQL\Utils\Utils; -use Apollo\Federation\Types\EntityObjectType; -use Apollo\Federation\Utils\FederatedSchemaPrinter; - /** - * A federated GraphQL schema definition (see [related docs](https://www.apollographql.com/docs/apollo-server/federation/introduction)) + * A federated GraphQL schema definition {@see https://www.apollographql.com/docs/apollo-server/federation/introduction }. * * A federated schema defines a self-contained GraphQL service that can be merged with * other services by the [Apollo Gateway](https://www.apollographql.com/docs/intro/platform/#gateway) @@ -27,7 +27,7 @@ * directives to hint the gateway on how entity types and references should be resolved. * * Usage example: - * + * * $userType = new Apollo\Federation\Types\EntityObjectType([ * 'name' => 'User', * 'fields' => [ @@ -52,6 +52,7 @@ * $schema = new Apollo\Federation\FederatedSchema([ * 'query' => $queryType * ]); + * */ class FederatedSchema extends Schema { @@ -61,7 +62,10 @@ class FederatedSchema extends Schema /** @var Directive[] */ protected $entityDirectives; - public function __construct($config) + /** + * @param array $config + */ + public function __construct(array $config) { $this->entityTypes = $this->extractEntityTypes($config); $this->entityDirectives = Directives::getDirectives(); @@ -72,7 +76,7 @@ public function __construct($config) } /** - * Returns all the resolved entity types in the schema + * Returns all the resolved entity types in the schema. * * @return EntityObjectType[] */ @@ -82,9 +86,7 @@ public function getEntityTypes(): array } /** - * Indicates whether the schema has entity types resolved - * - * @return bool + * Indicates whether the schema has entity types resolved. */ public function hasEntityTypes(): bool { @@ -102,11 +104,15 @@ private function getEntityDirectivesConfig(array $config): array return $config; } - /** @var array */ + /** + * @param array $config + * + * @return array{ query: ObjectType } + */ private function getQueryTypeConfig(array $config): array { $queryTypeConfig = $config['query']->config; - if (is_callable($queryTypeConfig['fields'])) { + if (\is_callable($queryTypeConfig['fields'])) { $queryTypeConfig['fields'] = $queryTypeConfig['fields'](); } @@ -117,11 +123,13 @@ private function getQueryTypeConfig(array $config): array ); return [ - 'query' => new ObjectType($queryTypeConfig) + 'query' => new ObjectType($queryTypeConfig), ]; } - /** @var array */ + /** + * @return array{ _service: array } + */ private function getQueryTypeServiceFieldConfig(): array { $serviceType = new ObjectType([ @@ -131,9 +139,9 @@ private function getQueryTypeServiceFieldConfig(): array 'type' => Type::string(), 'resolve' => function () { return FederatedSchemaPrinter::doPrint($this); - } - ] - ] + }, + ], + ], ]); return [ @@ -141,12 +149,16 @@ private function getQueryTypeServiceFieldConfig(): array 'type' => Type::nonNull($serviceType), 'resolve' => function () { return []; - } - ] + }, + ], ]; } - /** @var array */ + /** + * @param array|null $config + * + * @return array + */ private function getQueryTypeEntitiesFieldConfig(?array $config): array { if (!$this->hasEntityTypes()) { @@ -155,14 +167,14 @@ private function getQueryTypeEntitiesFieldConfig(?array $config): array $entityType = new UnionType([ 'name' => TypeEnum::ENTITY, - 'types' => array_values($this->getEntityTypes()) + 'types' => array_values($this->getEntityTypes()), ]); $anyType = new CustomScalarType([ 'name' => TypeEnum::ANY, 'serialize' => function ($value) { return $value; - } + }, ]); return [ @@ -170,23 +182,23 @@ private function getQueryTypeEntitiesFieldConfig(?array $config): array 'type' => Type::listOf($entityType), 'args' => [ 'representations' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))) - ] + 'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))), + ], ], 'resolve' => function ($root, $args, $context, $info) use ($config) { - if (isset($config) && isset($config['resolve']) && is_callable($config['resolve'])) { - return $config['resolve']($root, $args, $context, $info);; - } else { - return $this->resolve($root, $args, $context, $info); + if ($config && isset($config['resolve']) && \is_callable($config['resolve'])) { + return $config['resolve']($root, $args, $context, $info); } - } - ] + + return $this->resolve($root, $args, $context, $info); + }, + ], ]; } - private function resolve($root, $args, $context, $info) + private function resolve($root, $args, $context, $info): array { - return array_map(function ($ref) use ($context, $info) { + return array_map(static function ($ref) use ($context, $info) { Utils::invariant(isset($ref['__typename']), 'Type name must be provided in the reference.'); $typeName = $ref['__typename']; @@ -204,12 +216,12 @@ private function resolve($root, $args, $context, $info) return $ref; } - $r = $type->resolveReference($ref, $context, $info); - return $r; + return $type->resolveReference($ref, $context, $info); }, $args['representations']); } + /** - * @param array $config + * @param array $config * * @return EntityObjectType[] */ From 877810b79fd114176c456810fe4476b07a988571 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 29 Jul 2022 12:57:59 +0300 Subject: [PATCH 06/40] Fix code style --- src/FederatedSchema.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index 6e9970d..e1d16fc 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -9,6 +9,7 @@ use Apollo\Federation\Utils\FederatedSchemaPrinter; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; @@ -94,7 +95,9 @@ public function hasEntityTypes(): bool } /** - * @return Directive[] + * @param array $config + * + * @return array */ private function getEntityDirectivesConfig(array $config): array { @@ -157,7 +160,7 @@ private function getQueryTypeServiceFieldConfig(): array /** * @param array|null $config * - * @return array + * @return array>, resolve: callable }> */ private function getQueryTypeEntitiesFieldConfig(?array $config): array { From 268eee6b629b4a64e0c342b6da2ca0564463abbd Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 13:28:04 +0300 Subject: [PATCH 07/40] Print simple and compound directives' argument "fields" --- src/Utils/FederatedSchemaPrinter.php | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 6e44ba8..369b3fd 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -419,7 +419,7 @@ private static function printEntityObject(EntityObjectType $type, array $options $keyDirective = ''; foreach ($type->getKeyFields() as $keyField) { - $keyDirective = $keyDirective . sprintf(' @key(fields: "%s")', $keyField); + $keyDirective = $keyDirective . sprintf(' @key(fields: "%s")', self::printKeyFields($keyField)); } $isEntityRef = $type instanceof EntityRefObjectType; @@ -499,11 +499,11 @@ private static function printFieldFederatedDirectives(FieldDefinition $field): s } if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])) { - $directives[] = sprintf('@provides(fields: "%s")', $field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES]); + $directives[] = sprintf('@provides(fields: "%s")', self::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])); } if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])) { - $directives[] = sprintf('@requires(fields: "%s")', $field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES]); + $directives[] = sprintf('@requires(fields: "%s")', self::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])); } return implode(' ', $directives); @@ -518,6 +518,28 @@ private static function printInterface(InterfaceType $type, array $options): str sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type)); } + /** + * Print simple and compound primary key fields + * {@see https://www.apollographql.com/docs/federation/v1/entities#compound-primary-keys }. + * + * @param string|array $keyFields + */ + private static function printKeyFields($keyFields): string + { + $parts = []; + foreach (((array) $keyFields) as $index => $keyField) { + if (\is_string($keyField)) { + $parts[] = $keyField; + } elseif (\is_array($keyField)) { + $parts[] = sprintf('%s { %s }', $index, self::printKeyFields($keyField)); + } else { + throw new \InvalidArgumentException('Invalid keyField config'); + } + } + + return implode(' ', $parts); + } + /** * @param bool[] $options */ From 0769802506b6dbf17b7dac353a4da687f8fb5d26 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 15:05:25 +0300 Subject: [PATCH 08/40] Declare reserved names --- src/Enum/TypeEnum.php | 25 --------------------- src/FederatedSchema.php | 33 ++++++++++++++++++---------- src/Types/EntityObjectType.php | 3 ++- src/Utils/FederatedSchemaPrinter.php | 20 ++++++++--------- test/StarWarsSchema.php | 12 +++++----- 5 files changed, 40 insertions(+), 53 deletions(-) delete mode 100644 src/Enum/TypeEnum.php diff --git a/src/Enum/TypeEnum.php b/src/Enum/TypeEnum.php deleted file mode 100644 index 0cbacbd..0000000 --- a/src/Enum/TypeEnum.php +++ /dev/null @@ -1,25 +0,0 @@ - TypeEnum::SERVICE, + 'name' => self::RESERVED_TYPE_SERVICE, 'fields' => [ - 'sdl' => [ + self::RESERVED_FIELD_SDL => [ 'type' => Type::string(), 'resolve' => function () { return FederatedSchemaPrinter::doPrint($this); @@ -148,7 +159,7 @@ private function getQueryTypeServiceFieldConfig(): array ]); return [ - '_service' => [ + self::RESERVED_FIELD_SERVICE => [ 'type' => Type::nonNull($serviceType), 'resolve' => function () { return []; @@ -169,22 +180,22 @@ private function getQueryTypeEntitiesFieldConfig(?array $config): array } $entityType = new UnionType([ - 'name' => TypeEnum::ENTITY, + 'name' => self::RESERVED_TYPE_ENTITY, 'types' => array_values($this->getEntityTypes()), ]); $anyType = new CustomScalarType([ - 'name' => TypeEnum::ANY, + 'name' => self::RESERVED_TYPE_ANY, 'serialize' => function ($value) { return $value; }, ]); return [ - '_entities' => [ + self::RESERVED_FIELD_ENTITIES => [ 'type' => Type::listOf($entityType), 'args' => [ - 'representations' => [ + self::RESERVED_FIELD_REPRESENTATIONS => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))), ], ], @@ -202,9 +213,9 @@ private function getQueryTypeEntitiesFieldConfig(?array $config): array private function resolve($root, $args, $context, $info): array { return array_map(static function ($ref) use ($context, $info) { - Utils::invariant(isset($ref['__typename']), 'Type name must be provided in the reference.'); + Utils::invariant(isset($ref[self::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); - $typeName = $ref['__typename']; + $typeName = $ref[self::RESERVED_FIELD_TYPE_NAME]; $type = $info->schema->getType($typeName); Utils::invariant( @@ -220,7 +231,7 @@ private function resolve($root, $args, $context, $info): array } return $type->resolveReference($ref, $context, $info); - }, $args['representations']); + }, $args[self::RESERVED_FIELD_REPRESENTATIONS]); } /** diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index fd617c4..a7aa5d7 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -4,6 +4,7 @@ namespace Apollo\Federation\Types; +use Apollo\Federation\FederatedSchema; use GraphQL\Type\Definition\ObjectType; use GraphQL\Utils\Utils; @@ -120,7 +121,7 @@ private function validateReferenceResolver() */ private function validateReferenceKeys($ref) { - Utils::invariant(isset($ref['__typename']), 'Type name must be provided in the reference.'); + Utils::invariant(isset($ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); } /** diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 369b3fd..732141c 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -33,7 +33,7 @@ namespace Apollo\Federation\Utils; use Apollo\Federation\Enum\DirectiveEnum; -use Apollo\Federation\Enum\TypeEnum; +use Apollo\Federation\FederatedSchema; use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; @@ -317,7 +317,7 @@ private static function printInputValue($arg): string public static function printType(Type $type, array $options = []): string { if ($type instanceof ScalarType) { - if ($type->name !== TypeEnum::ANY) { + if ($type->name !== FederatedSchema::RESERVED_TYPE_ANY) { return self::printScalar($type, $options); } @@ -329,7 +329,7 @@ public static function printType(Type $type, array $options = []): string } if ($type instanceof ObjectType) { - if (TypeEnum::SERVICE !== $type->name) { + if (FederatedSchema::RESERVED_TYPE_SERVICE !== $type->name) { return self::printObject($type, $options); } @@ -341,7 +341,7 @@ public static function printType(Type $type, array $options = []): string } if ($type instanceof UnionType) { - if (TypeEnum::ENTITY !== $type->name) { + if (FederatedSchema::RESERVED_TYPE_ENTITY !== $type->name) { return self::printUnion($type, $options); } @@ -387,8 +387,9 @@ private static function printObject(ObjectType $type, array $options): string ) : ''; - // FIXME: hardcoded names! They can be different in real. - $queryExtends = $type->name === 'Query' || $type->name === 'Mutation' ? 'extend ' : ''; + $queryExtends = \in_array($type->name, [FederatedSchema::RESERVED_TYPE_QUERY, FederatedSchema::RESERVED_TYPE_MUTATION], true) + ? 'extend ' + : ''; return self::printDescription($options, $type) . sprintf( @@ -444,11 +445,10 @@ private static function printFields(array $options, TypeWithFields $type): strin { $fields = array_values($type->getFields()); - // FIXME it looks like hardcoded name. Potentially, it can be different! - if ($type->name === 'Query') { + if ($type->name === FederatedSchema::RESERVED_TYPE_QUERY) { $fields = array_filter($fields, static function (FieldDefinition $field): bool { - //TODO use constants instead of magic scalar values - return $field->name !== '_service' && $field->name !== '_entities'; + return $field->name !== FederatedSchema::RESERVED_FIELD_SERVICE + && $field->name !== FederatedSchema::RESERVED_FIELD_ENTITIES; }); } diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index 5594896..9ec1f8b 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -37,7 +37,7 @@ public static function getEpisodesSchemaCustomResolver(): FederatedSchema $type = $info->schema->getType($typeName); $ref["id"] = $ref["id"] + 1; return $type->resolveReference($ref); - }, $args['representations']); + }, $args[FederatedSchema::RESERVED_FIELD_REPRESENTATIONS]); } ]); } @@ -49,7 +49,7 @@ private static function getQueryType(): ObjectType $episodeType = self::getEpisodeType(); $queryType = new ObjectType([ - 'name' => 'Query', + 'name' => FederatedSchema::RESERVED_TYPE_QUERY, 'fields' => [ 'episodes' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($episodeType))), @@ -86,8 +86,8 @@ private static function getEpisodeType(): EntityObjectType 'provides' => 'name' ] ], - 'keyFields' => ['id'], - '__resolveReference' => function ($ref) { + EntityObjectType::FIELD_KEY_FIELDS => ['id'], + EntityObjectType::FIELD_REFERENCE_RESOLVER => function ($ref) { // print_r($ref); $entity = StarWarsData::getEpisodeById($ref['id']); $entity["__typename"] = "Episode"; @@ -118,7 +118,7 @@ private static function getCharacterType(): EntityRefObjectType 'requires' => 'name' ] ], - 'keyFields' => ['id'] + EntityObjectType::FIELD_KEY_FIELDS => ['id'] ]); } @@ -137,7 +137,7 @@ private static function getLocationType(): EntityRefObjectType 'isExternal' => true ] ], - 'keyFields' => ['id'] + EntityObjectType::FIELD_KEY_FIELDS => ['id'] ]); } } From 684970629f3f9111a90a12adc5828997796a33b2 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 17:51:28 +0300 Subject: [PATCH 09/40] Fix code style --- src/Utils/FederatedSchemaPrinter.php | 37 ++++++++++++---------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 732141c..24820be 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -1,7 +1,6 @@ description) { + if (!isset($def->description) || !$def->description) { return ''; } - $lines = self::descriptionLines($def->description, 120 - strlen($indentation)); + $lines = self::descriptionLines($def->description, 120 - \strlen($indentation)); if (isset($options['commentDescriptions'])) { return self::printDescriptionWithComments($lines, $indentation, $firstInBlock); @@ -170,7 +165,7 @@ private static function printDescription(array $options, $def, string $indentati $description = $indentation && !$firstInBlock ? "\n" . $indentation . '"""' : $indentation . '"""'; // In some circumstances, a single line can be used for the description. - if (count($lines) === 1 && mb_strlen($lines[0]) < 70 && substr($lines[0], -1) !== '"') { + if (1 === \count($lines) && mb_strlen($lines[0]) < 70 && '"' !== substr($lines[0], -1)) { return $description . self::escapeQuote($lines[0]) . "\"\"\"\n"; } @@ -183,8 +178,8 @@ private static function printDescription(array $options, $def, string $indentati $lineLength = \count($lines); - for ($i = 0; $i < $lineLength; $i++) { - if ($i !== 0 || !$hasLeadingSpace) { + for ($i = 0; $i < $lineLength; ++$i) { + if (0 !== $i || !$hasLeadingSpace) { $description .= $indentation; } $description .= self::escapeQuote($lines[$i]) . "\n"; @@ -204,7 +199,7 @@ private static function descriptionLines(string $description, int $maxLen): arra $rawLines = explode("\n", $description); foreach ($rawLines as $line) { - if ($line === '') { + if ('' === $line) { $lines[] = $line; } else { // For > 120 character long lines, cut at space boundaries into sublines @@ -225,7 +220,7 @@ private static function descriptionLines(string $description, int $maxLen): arra */ private static function breakLine(string $line, int $maxLen): array { - if (strlen($line) < $maxLen + 5) { + if (\strlen($line) < $maxLen + 5) { return [$line]; } @@ -244,7 +239,7 @@ private static function printDescriptionWithComments(array $lines, string $inden $description = $indentation && !$firstInBlock ? "\n" : ''; foreach ($lines as $line) { - if ($line === '') { + if ('' === $line) { $description .= $indentation . "#\n"; } else { $description .= $indentation . '# ' . $line . "\n"; @@ -317,7 +312,7 @@ private static function printInputValue($arg): string public static function printType(Type $type, array $options = []): string { if ($type instanceof ScalarType) { - if ($type->name !== FederatedSchema::RESERVED_TYPE_ANY) { + if (FederatedSchema::RESERVED_TYPE_ANY !== $type->name) { return self::printScalar($type, $options); } @@ -445,10 +440,10 @@ private static function printFields(array $options, TypeWithFields $type): strin { $fields = array_values($type->getFields()); - if ($type->name === FederatedSchema::RESERVED_TYPE_QUERY) { + if (FederatedSchema::RESERVED_TYPE_QUERY === $type->name) { $fields = array_filter($fields, static function (FieldDefinition $field): bool { - return $field->name !== FederatedSchema::RESERVED_FIELD_SERVICE - && $field->name !== FederatedSchema::RESERVED_FIELD_ENTITIES; + return FederatedSchema::RESERVED_FIELD_SERVICE !== $field->name + && FederatedSchema::RESERVED_FIELD_ENTITIES !== $field->name; }); } @@ -481,7 +476,7 @@ private static function printDeprecated($fieldOrEnumVal): string if (empty($reason)) { return ''; } - if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) { + if ('' === $reason || Directive::DEFAULT_DEPRECATION_REASON === $reason) { return ' @deprecated'; } @@ -493,7 +488,7 @@ private static function printFieldFederatedDirectives(FieldDefinition $field): s $directives = []; if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL]) - && $field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL] === true + && true === $field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL] ) { $directives[] = '@external'; } @@ -604,7 +599,7 @@ static function ($f, $i) use ($options) { } /** - * @param bool[] $options + * @param array{ commentDescriptions: bool } $options * * @api */ From df465205ca933b204e5541ca38fcd3e47c41240d Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 18:32:25 +0300 Subject: [PATCH 10/40] Fixes during investigation --- .gitignore | 1 + README.md | 3 ++- src/FederatedSchema.php | 2 +- src/Types/EntityObjectType.php | 2 +- src/Types/EntityRefObjectType.php | 12 +----------- test/DirectivesTest.php | 14 ++++++-------- test/EntitiesTest.php | 9 +++------ test/SchemaTest.php | 9 ++------- 8 files changed, 17 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 7303759..f5aea2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea composer.phar /vendor/ /node_modules/ diff --git a/README.md b/README.md index cb391f7..589c85c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ An entity is an object type that you define canonically in one subgraph and can ```php use Apollo\Federation\Types\EntityObjectType; +use GraphQL\Type\Definition\Type; $userType = new EntityObjectType([ 'name' => 'User', @@ -34,7 +35,7 @@ $userType = new EntityObjectType([ ]); ``` -* `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. +* `keyFields` — defines the entity's primary key, 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. diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index ebdc19a..757109e 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -17,7 +17,7 @@ use GraphQL\Utils\Utils; /** - * A federated GraphQL schema definition {@see https://www.apollographql.com/docs/apollo-server/federation/introduction }. + * A federated GraphQL schema definition see related docs {@see https://www.apollographql.com/docs/apollo-server/federation/introduction }. * * A federated schema defines a self-contained GraphQL service that can be merged with * other services by the [Apollo Gateway](https://www.apollographql.com/docs/intro/platform/#gateway) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index a7aa5d7..227e6bb 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -13,7 +13,7 @@ * connection points between services and form the basic building blocks of a federated * graph. Entities have a primary key whose value uniquely identifies a specific instance * of the type, similar to the function of a primary key in a SQL table - * {@see https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#entities-and-keys }. + * see related docs {@see https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#entities-and-keys }. * * The `keyFields` property is required in the configuration, indicating the fields that * serve as the unique keys or identifiers of the entity. diff --git a/src/Types/EntityRefObjectType.php b/src/Types/EntityRefObjectType.php index a798ec4..3789755 100644 --- a/src/Types/EntityRefObjectType.php +++ b/src/Types/EntityRefObjectType.php @@ -7,19 +7,9 @@ /** * An entity reference is a type referencing an entity owned by another service. Usually, * entity references are stub types containing only the key fields necessary for the - * [Apollo Gateway](https://www.apollographql.com/docs/intro/platform/#gateway) to + * Apollo Gateway {@see https://www.apollographql.com/docs/intro/platform/#gateway } to * resolve the entity during query execution. */ class EntityRefObjectType extends EntityObjectType { - /** @var array */ - private $keyFields; - - /** - * @param mixed[] $config - */ - public function __construct(array $config) - { - parent::__construct($config); - } } diff --git a/test/DirectivesTest.php b/test/DirectivesTest.php index 67c4871..474e477 100644 --- a/test/DirectivesTest.php +++ b/test/DirectivesTest.php @@ -4,16 +4,14 @@ namespace Apollo\Federation\Tests; -use PHPUnit\Framework\TestCase; -use Spatie\Snapshots\MatchesSnapshots; - -use GraphQL\Type\Schema; -use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ObjectType; +use Apollo\Federation\Directives; use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; +use GraphQL\Type\Schema; use GraphQL\Utils\SchemaPrinter; - -use Apollo\Federation\Directives; +use PHPUnit\Framework\TestCase; +use Spatie\Snapshots\MatchesSnapshots; class DirectivesTest extends TestCase { diff --git a/test/EntitiesTest.php b/test/EntitiesTest.php index de61813..2acb5d5 100644 --- a/test/EntitiesTest.php +++ b/test/EntitiesTest.php @@ -4,14 +4,11 @@ namespace Apollo\Federation\Tests; -use PHPUnit\Framework\TestCase; -use Spatie\Snapshots\MatchesSnapshots; - -use GraphQL\Type\Definition\Type; -use GraphQL\Error\InvariantViolation; - use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; +use GraphQL\Type\Definition\Type; +use PHPUnit\Framework\TestCase; +use Spatie\Snapshots\MatchesSnapshots; class EntitiesTest extends TestCase { diff --git a/test/SchemaTest.php b/test/SchemaTest.php index 8f9b882..4262bba 100644 --- a/test/SchemaTest.php +++ b/test/SchemaTest.php @@ -4,15 +4,10 @@ namespace Apollo\Federation\Tests; -use PHPUnit\Framework\TestCase; -use Spatie\Snapshots\MatchesSnapshots; - use GraphQL\GraphQL; -use GraphQL\Type\Definition\Type; use GraphQL\Utils\SchemaPrinter; - -use Apollo\Federation\Tests\StarWarsSchema; -use Apollo\Federation\Tests\DungeonsAndDragonsSchema; +use PHPUnit\Framework\TestCase; +use Spatie\Snapshots\MatchesSnapshots; class SchemaTest extends TestCase { From 79e14dce2bf41cd7566d593ae0c03b552eaab4a9 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 18:45:36 +0300 Subject: [PATCH 11/40] Update phpDocs --- src/Utils/FederatedSchemaPrinter.php | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 24820be..8014deb 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -79,7 +79,7 @@ class FederatedSchemaPrinter * - commentDescriptions: * Provide true to use preceding comments as the description. * - * @param array{ commentDescriptions: bool } $options + * @param array $options * * @api */ @@ -103,7 +103,7 @@ public static function isFederatedDirective(Directive $type): bool } /** - * @param bool[] $options + * @param array $options */ private static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string { @@ -134,7 +134,7 @@ private static function printFilteredSchema(Schema $schema, callable $directiveF } /** - * @param bool[] $options + * @param array $options */ private static function printDirective(Directive $directive, array $options): string { @@ -147,7 +147,7 @@ private static function printDirective(Directive $directive, array $options): st } /** - * @param bool[] $options + * @param array $options * @param Directive|EnumValueDefinition|FieldArgument|Type|object $def */ private static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string @@ -307,7 +307,7 @@ private static function printInputValue($arg): string } /** - * @param bool[] $options + * @param array $options */ public static function printType(Type $type, array $options = []): string { @@ -355,7 +355,7 @@ public static function printType(Type $type, array $options = []): string } /** - * @param bool[] $options + * @param array $options */ private static function printScalar(ScalarType $type, array $options): string { @@ -363,7 +363,7 @@ private static function printScalar(ScalarType $type, array $options): string } /** - * @param bool[] $options + * @param array $options */ private static function printObject(ObjectType $type, array $options): string { @@ -397,7 +397,7 @@ private static function printObject(ObjectType $type, array $options): string } /** - * @param bool[] $options + * @param array $options */ private static function printEntityObject(EntityObjectType $type, array $options): string { @@ -433,7 +433,7 @@ private static function printEntityObject(EntityObjectType $type, array $options } /** - * @param bool[] $options + * @param array $options * @param EntityObjectType|InterfaceType|ObjectType $type */ private static function printFields(array $options, TypeWithFields $type): string @@ -505,7 +505,7 @@ private static function printFieldFederatedDirectives(FieldDefinition $field): s } /** - * @param bool[] $options + * @param array $options */ private static function printInterface(InterfaceType $type, array $options): string { @@ -536,7 +536,7 @@ private static function printKeyFields($keyFields): string } /** - * @param bool[] $options + * @param array $options */ private static function printUnion(UnionType $type, array $options): string { @@ -545,7 +545,7 @@ private static function printUnion(UnionType $type, array $options): string } /** - * @param bool[] $options + * @param array $options */ private static function printEnum(EnumType $type, array $options): string { @@ -555,7 +555,7 @@ private static function printEnum(EnumType $type, array $options): string /** * @param EnumValueDefinition[] $values - * @param bool[] $options + * @param array $options */ private static function printEnumValues(array $values, array $options): string { @@ -575,7 +575,7 @@ static function ($value, $i) use ($options) { } /** - * @param bool[] $options + * @param array $options */ private static function printInputObject(InputObjectType $type, array $options): string { @@ -599,7 +599,7 @@ static function ($f, $i) use ($options) { } /** - * @param array{ commentDescriptions: bool } $options + * @param array $options * * @api */ From 245f9adc7d36cf0a73a714aa4481660d393f82c3 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 18:58:34 +0300 Subject: [PATCH 12/40] Make printer better extendable --- src/Utils/FederatedSchemaPrinter.php | 130 +++++++++++++-------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 8014deb..419014e 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -85,10 +85,10 @@ class FederatedSchemaPrinter */ public static function doPrint(Schema $schema, array $options = []): string { - return self::printFilteredSchema( + return static::printFilteredSchema( $schema, static function (Directive $type): bool { - return !Directive::isSpecifiedDirective($type) && !self::isFederatedDirective($type); + return !Directive::isSpecifiedDirective($type) && !static::isFederatedDirective($type); }, static function (Type $type): bool { return !Type::isBuiltInType($type); @@ -105,7 +105,7 @@ public static function isFederatedDirective(Directive $type): bool /** * @param array $options */ - private static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string + protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string { $directives = array_filter($schema->getDirectives(), static function (Directive $directive) use ($directiveFilter): bool { return $directiveFilter($directive); @@ -122,10 +122,10 @@ private static function printFilteredSchema(Schema $schema, callable $directiveF array_filter( array_merge( array_map(static function (Directive $directive) use ($options) { - return self::printDirective($directive, $options); + return static::printDirective($directive, $options); }, $directives), array_map(static function (Type $type) use ($options) { - return self::printType($type, $options); + return static::printType($type, $options); }, $types) ) ) @@ -136,12 +136,12 @@ private static function printFilteredSchema(Schema $schema, callable $directiveF /** * @param array $options */ - private static function printDirective(Directive $directive, array $options): string + protected static function printDirective(Directive $directive, array $options): string { - return self::printDescription($options, $directive) . + return static::printDescription($options, $directive) . 'directive @' . $directive->name . - self::printArgs($options, $directive->args) . + static::printArgs($options, $directive->args) . ' on ' . implode(' | ', $directive->locations); } @@ -150,23 +150,23 @@ private static function printDirective(Directive $directive, array $options): st * @param array $options * @param Directive|EnumValueDefinition|FieldArgument|Type|object $def */ - private static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string + protected static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string { if (!isset($def->description) || !$def->description) { return ''; } - $lines = self::descriptionLines($def->description, 120 - \strlen($indentation)); + $lines = static::descriptionLines($def->description, 120 - \strlen($indentation)); if (isset($options['commentDescriptions'])) { - return self::printDescriptionWithComments($lines, $indentation, $firstInBlock); + return static::printDescriptionWithComments($lines, $indentation, $firstInBlock); } $description = $indentation && !$firstInBlock ? "\n" . $indentation . '"""' : $indentation . '"""'; // In some circumstances, a single line can be used for the description. if (1 === \count($lines) && mb_strlen($lines[0]) < 70 && '"' !== substr($lines[0], -1)) { - return $description . self::escapeQuote($lines[0]) . "\"\"\"\n"; + return $description . static::escapeQuote($lines[0]) . "\"\"\"\n"; } // Format a multi-line block quote to account for leading space. @@ -182,7 +182,7 @@ private static function printDescription(array $options, $def, string $indentati if (0 !== $i || !$hasLeadingSpace) { $description .= $indentation; } - $description .= self::escapeQuote($lines[$i]) . "\n"; + $description .= static::escapeQuote($lines[$i]) . "\n"; } $description .= $indentation . "\"\"\"\n"; @@ -193,7 +193,7 @@ private static function printDescription(array $options, $def, string $indentati /** * @return string[] */ - private static function descriptionLines(string $description, int $maxLen): array + protected static function descriptionLines(string $description, int $maxLen): array { $lines = []; $rawLines = explode("\n", $description); @@ -204,7 +204,7 @@ private static function descriptionLines(string $description, int $maxLen): arra } else { // For > 120 character long lines, cut at space boundaries into sublines // of ~80 chars. - $sublines = self::breakLine($line, $maxLen); + $sublines = static::breakLine($line, $maxLen); foreach ($sublines as $subline) { $lines[] = $subline; @@ -218,7 +218,7 @@ private static function descriptionLines(string $description, int $maxLen): arra /** * @return string[] */ - private static function breakLine(string $line, int $maxLen): array + protected static function breakLine(string $line, int $maxLen): array { if (\strlen($line) < $maxLen + 5) { return [$line]; @@ -234,7 +234,7 @@ private static function breakLine(string $line, int $maxLen): array /** * @param string[] $lines */ - private static function printDescriptionWithComments(array $lines, string $indentation, bool $firstInBlock): string + protected static function printDescriptionWithComments(array $lines, string $indentation, bool $firstInBlock): string { $description = $indentation && !$firstInBlock ? "\n" : ''; @@ -249,7 +249,7 @@ private static function printDescriptionWithComments(array $lines, string $inden return $description; } - private static function escapeQuote(string $line): string + protected static function escapeQuote(string $line): string { return str_replace('"""', '\\"""', $line); } @@ -258,7 +258,7 @@ private static function escapeQuote(string $line): string * @param bool[] $options * @param FieldArgument[]|null $args */ - private static function printArgs(array $options, $args, string $indentation = ''): string + protected static function printArgs(array $options, $args, string $indentation = ''): string { if (!$args) { return ''; @@ -270,7 +270,7 @@ private static function printArgs(array $options, $args, string $indentation = ' return empty($arg->description); }) ) { - return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')'; + return '(' . implode(', ', array_map('static::printInputValue', $args)) . ')'; } return sprintf( @@ -279,10 +279,10 @@ private static function printArgs(array $options, $args, string $indentation = ' "\n", array_map( static function (FieldArgument $arg, $i) use ($indentation, $options): string { - return self::printDescription($options, $arg, ' ' . $indentation, !$i) . + return static::printDescription($options, $arg, ' ' . $indentation, !$i) . ' ' . $indentation . - self::printInputValue($arg); + static::printInputValue($arg); }, $args, array_keys($args) @@ -295,7 +295,7 @@ static function (FieldArgument $arg, $i) use ($indentation, $options): string { /** * @param InputObjectField|FieldArgument $arg */ - private static function printInputValue($arg): string + protected static function printInputValue($arg): string { $argDecl = $arg->name . ': ' . (string) $arg->getType(); @@ -313,42 +313,42 @@ public static function printType(Type $type, array $options = []): string { if ($type instanceof ScalarType) { if (FederatedSchema::RESERVED_TYPE_ANY !== $type->name) { - return self::printScalar($type, $options); + return static::printScalar($type, $options); } return ''; } if ($type instanceof EntityObjectType || $type instanceof EntityRefObjectType) { - return self::printEntityObject($type, $options); + return static::printEntityObject($type, $options); } if ($type instanceof ObjectType) { if (FederatedSchema::RESERVED_TYPE_SERVICE !== $type->name) { - return self::printObject($type, $options); + return static::printObject($type, $options); } return ''; } if ($type instanceof InterfaceType) { - return self::printInterface($type, $options); + return static::printInterface($type, $options); } if ($type instanceof UnionType) { if (FederatedSchema::RESERVED_TYPE_ENTITY !== $type->name) { - return self::printUnion($type, $options); + return static::printUnion($type, $options); } return ''; } if ($type instanceof EnumType) { - return self::printEnum($type, $options); + return static::printEnum($type, $options); } if ($type instanceof InputObjectType) { - return self::printInputObject($type, $options); + return static::printInputObject($type, $options); } throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type))); @@ -357,15 +357,15 @@ public static function printType(Type $type, array $options = []): string /** * @param array $options */ - private static function printScalar(ScalarType $type, array $options): string + protected static function printScalar(ScalarType $type, array $options): string { - return sprintf('%sscalar %s', self::printDescription($options, $type), $type->name); + return sprintf('%sscalar %s', static::printDescription($options, $type), $type->name); } /** * @param array $options */ - private static function printObject(ObjectType $type, array $options): string + protected static function printObject(ObjectType $type, array $options): string { if (empty($type->getFields())) { return ''; @@ -386,20 +386,20 @@ private static function printObject(ObjectType $type, array $options): string ? 'extend ' : ''; - return self::printDescription($options, $type) . + return static::printDescription($options, $type) . sprintf( "%stype %s%s {\n%s\n}", $queryExtends, $type->name, $implementedInterfaces, - self::printFields($options, $type) + static::printFields($options, $type) ); } /** * @param array $options */ - private static function printEntityObject(EntityObjectType $type, array $options): string + protected static function printEntityObject(EntityObjectType $type, array $options): string { $interfaces = $type->getInterfaces(); $implementedInterfaces = !empty($interfaces) @@ -415,20 +415,20 @@ private static function printEntityObject(EntityObjectType $type, array $options $keyDirective = ''; foreach ($type->getKeyFields() as $keyField) { - $keyDirective = $keyDirective . sprintf(' @key(fields: "%s")', self::printKeyFields($keyField)); + $keyDirective = $keyDirective . sprintf(' @key(fields: "%s")', static::printKeyFields($keyField)); } $isEntityRef = $type instanceof EntityRefObjectType; $extends = $isEntityRef ? 'extend ' : ''; - return self::printDescription($options, $type) . + return static::printDescription($options, $type) . sprintf( "%stype %s%s%s {\n%s\n}", $extends, $type->name, $implementedInterfaces, $keyDirective, - self::printFields($options, $type) + static::printFields($options, $type) ); } @@ -436,7 +436,7 @@ private static function printEntityObject(EntityObjectType $type, array $options * @param array $options * @param EntityObjectType|InterfaceType|ObjectType $type */ - private static function printFields(array $options, TypeWithFields $type): string + protected static function printFields(array $options, TypeWithFields $type): string { $fields = array_values($type->getFields()); @@ -451,15 +451,15 @@ private static function printFields(array $options, TypeWithFields $type): strin "\n", array_map( static function (FieldDefinition $f, $i) use ($options) { - return self::printDescription($options, $f, ' ', !$i) . + return static::printDescription($options, $f, ' ', !$i) . ' ' . $f->name . - self::printArgs($options, $f->args, ' ') . + static::printArgs($options, $f->args, ' ') . ': ' . (string) $f->getType() . - self::printDeprecated($f) . + static::printDeprecated($f) . ' ' . - self::printFieldFederatedDirectives($f); + static::printFieldFederatedDirectives($f); }, $fields, array_keys($fields) @@ -470,7 +470,7 @@ static function (FieldDefinition $f, $i) use ($options) { /** * @param EnumValueDefinition|FieldDefinition $fieldOrEnumVal */ - private static function printDeprecated($fieldOrEnumVal): string + protected static function printDeprecated($fieldOrEnumVal): string { $reason = $fieldOrEnumVal->deprecationReason; if (empty($reason)) { @@ -483,7 +483,7 @@ private static function printDeprecated($fieldOrEnumVal): string return ' @deprecated(reason: ' . Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; } - private static function printFieldFederatedDirectives(FieldDefinition $field): string + protected static function printFieldFederatedDirectives(FieldDefinition $field): string { $directives = []; @@ -494,11 +494,11 @@ private static function printFieldFederatedDirectives(FieldDefinition $field): s } if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])) { - $directives[] = sprintf('@provides(fields: "%s")', self::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])); + $directives[] = sprintf('@provides(fields: "%s")', static::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])); } if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])) { - $directives[] = sprintf('@requires(fields: "%s")', self::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])); + $directives[] = sprintf('@requires(fields: "%s")', static::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])); } return implode(' ', $directives); @@ -507,10 +507,10 @@ private static function printFieldFederatedDirectives(FieldDefinition $field): s /** * @param array $options */ - private static function printInterface(InterfaceType $type, array $options): string + protected static function printInterface(InterfaceType $type, array $options): string { - return self::printDescription($options, $type) . - sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type)); + return static::printDescription($options, $type) . + sprintf("interface %s {\n%s\n}", $type->name, static::printFields($options, $type)); } /** @@ -519,14 +519,14 @@ private static function printInterface(InterfaceType $type, array $options): str * * @param string|array $keyFields */ - private static function printKeyFields($keyFields): string + protected static function printKeyFields($keyFields): string { $parts = []; foreach (((array) $keyFields) as $index => $keyField) { if (\is_string($keyField)) { $parts[] = $keyField; } elseif (\is_array($keyField)) { - $parts[] = sprintf('%s { %s }', $index, self::printKeyFields($keyField)); + $parts[] = sprintf('%s { %s }', $index, static::printKeyFields($keyField)); } else { throw new \InvalidArgumentException('Invalid keyField config'); } @@ -538,35 +538,35 @@ private static function printKeyFields($keyFields): string /** * @param array $options */ - private static function printUnion(UnionType $type, array $options): string + protected static function printUnion(UnionType $type, array $options): string { - return self::printDescription($options, $type) . + return static::printDescription($options, $type) . sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes())); } /** * @param array $options */ - private static function printEnum(EnumType $type, array $options): string + protected static function printEnum(EnumType $type, array $options): string { - return self::printDescription($options, $type) . - sprintf("enum %s {\n%s\n}", $type->name, self::printEnumValues($type->getValues(), $options)); + return static::printDescription($options, $type) . + sprintf("enum %s {\n%s\n}", $type->name, static::printEnumValues($type->getValues(), $options)); } /** * @param EnumValueDefinition[] $values * @param array $options */ - private static function printEnumValues(array $values, array $options): string + protected static function printEnumValues(array $values, array $options): string { return implode( "\n", array_map( static function ($value, $i) use ($options) { - return self::printDescription($options, $value, ' ', !$i) . + return static::printDescription($options, $value, ' ', !$i) . ' ' . $value->name . - self::printDeprecated($value); + static::printDeprecated($value); }, $values, array_keys($values) @@ -577,11 +577,11 @@ static function ($value, $i) use ($options) { /** * @param array $options */ - private static function printInputObject(InputObjectType $type, array $options): string + protected static function printInputObject(InputObjectType $type, array $options): string { $fields = array_values($type->getFields()); - return self::printDescription($options, $type) . + return static::printDescription($options, $type) . sprintf( "input %s {\n%s\n}", $type->name, @@ -589,7 +589,7 @@ private static function printInputObject(InputObjectType $type, array $options): "\n", array_map( static function ($f, $i) use ($options) { - return self::printDescription($options, $f, ' ', !$i) . ' ' . self::printInputValue($f); + return static::printDescription($options, $f, ' ', !$i) . ' ' . static::printInputValue($f); }, $fields, array_keys($fields) @@ -605,7 +605,7 @@ static function ($f, $i) use ($options) { */ public static function printIntrospectionSchema(Schema $schema, array $options = []): string { - return self::printFilteredSchema( + return static::printFilteredSchema( $schema, [Directive::class, 'isSpecifiedDirective'], [Introspection::class, 'isIntrospectionType'], From cd86cde752d526daffff1525431dd58439b4dae9 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 09:41:13 +0300 Subject: [PATCH 13/40] Extend base schema printer and reduce copy-paste --- src/Utils/FederatedSchemaPrinter.php | 379 +-------------------------- 1 file changed, 9 insertions(+), 370 deletions(-) diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 419014e..096291d 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -35,44 +35,28 @@ use Apollo\Federation\FederatedSchema; use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; -use GraphQL\Error\Error; -use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\EnumValueDefinition; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; -use GraphQL\Type\Definition\InputObjectField; -use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\TypeWithFields; use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; -use GraphQL\Utils\AST; -use GraphQL\Utils\Utils; +use GraphQL\Utils\SchemaPrinter; use function array_filter; use function array_keys; use function array_map; -use function array_merge; use function array_values; -use function explode; use function implode; -use function ksort; -use function mb_strlen; -use function preg_match_all; use function sprintf; -use function str_replace; -use function substr; /** * Given an instance of Schema, prints it in GraphQL type language. */ -class FederatedSchemaPrinter +class FederatedSchemaPrinter extends SchemaPrinter { /** * Accepts options as a second argument: @@ -102,264 +86,22 @@ public static function isFederatedDirective(Directive $type): bool return \in_array($type->name, DirectiveEnum::getAll(), true); } - /** - * @param array $options - */ - protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string - { - $directives = array_filter($schema->getDirectives(), static function (Directive $directive) use ($directiveFilter): bool { - return $directiveFilter($directive); - }); - - $types = $schema->getTypeMap(); - ksort($types); - $types = array_filter($types, $typeFilter); - - return sprintf( - "%s\n", - implode( - "\n\n", - array_filter( - array_merge( - array_map(static function (Directive $directive) use ($options) { - return static::printDirective($directive, $options); - }, $directives), - array_map(static function (Type $type) use ($options) { - return static::printType($type, $options); - }, $types) - ) - ) - ) - ); - } - - /** - * @param array $options - */ - protected static function printDirective(Directive $directive, array $options): string - { - return static::printDescription($options, $directive) . - 'directive @' . - $directive->name . - static::printArgs($options, $directive->args) . - ' on ' . - implode(' | ', $directive->locations); - } - - /** - * @param array $options - * @param Directive|EnumValueDefinition|FieldArgument|Type|object $def - */ - protected static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string - { - if (!isset($def->description) || !$def->description) { - return ''; - } - - $lines = static::descriptionLines($def->description, 120 - \strlen($indentation)); - - if (isset($options['commentDescriptions'])) { - return static::printDescriptionWithComments($lines, $indentation, $firstInBlock); - } - - $description = $indentation && !$firstInBlock ? "\n" . $indentation . '"""' : $indentation . '"""'; - - // In some circumstances, a single line can be used for the description. - if (1 === \count($lines) && mb_strlen($lines[0]) < 70 && '"' !== substr($lines[0], -1)) { - return $description . static::escapeQuote($lines[0]) . "\"\"\"\n"; - } - - // Format a multi-line block quote to account for leading space. - $hasLeadingSpace = isset($lines[0]) && \in_array(substr($lines[0], 0, 1), [' ', '\t'], true); - - if (!$hasLeadingSpace) { - $description .= "\n"; - } - - $lineLength = \count($lines); - - for ($i = 0; $i < $lineLength; ++$i) { - if (0 !== $i || !$hasLeadingSpace) { - $description .= $indentation; - } - $description .= static::escapeQuote($lines[$i]) . "\n"; - } - - $description .= $indentation . "\"\"\"\n"; - - return $description; - } - - /** - * @return string[] - */ - protected static function descriptionLines(string $description, int $maxLen): array - { - $lines = []; - $rawLines = explode("\n", $description); - - foreach ($rawLines as $line) { - if ('' === $line) { - $lines[] = $line; - } else { - // For > 120 character long lines, cut at space boundaries into sublines - // of ~80 chars. - $sublines = static::breakLine($line, $maxLen); - - foreach ($sublines as $subline) { - $lines[] = $subline; - } - } - } - - return $lines; - } - - /** - * @return string[] - */ - protected static function breakLine(string $line, int $maxLen): array - { - if (\strlen($line) < $maxLen + 5) { - return [$line]; - } - - preg_match_all('/((?: |^).{15,' . ($maxLen - 40) . '}(?= |$))/', $line, $parts); - - $parts = $parts[0]; - - return array_map('trim', $parts); - } - - /** - * @param string[] $lines - */ - protected static function printDescriptionWithComments(array $lines, string $indentation, bool $firstInBlock): string - { - $description = $indentation && !$firstInBlock ? "\n" : ''; - - foreach ($lines as $line) { - if ('' === $line) { - $description .= $indentation . "#\n"; - } else { - $description .= $indentation . '# ' . $line . "\n"; - } - } - - return $description; - } - - protected static function escapeQuote(string $line): string - { - return str_replace('"""', '\\"""', $line); - } - - /** - * @param bool[] $options - * @param FieldArgument[]|null $args - */ - protected static function printArgs(array $options, $args, string $indentation = ''): string - { - if (!$args) { - return ''; - } - - // If every arg does not have a description, print them on one line. - if ( - Utils::every($args, static function ($arg) { - return empty($arg->description); - }) - ) { - return '(' . implode(', ', array_map('static::printInputValue', $args)) . ')'; - } - - return sprintf( - "(\n%s\n%s)", - implode( - "\n", - array_map( - static function (FieldArgument $arg, $i) use ($indentation, $options): string { - return static::printDescription($options, $arg, ' ' . $indentation, !$i) . - ' ' . - $indentation . - static::printInputValue($arg); - }, - $args, - array_keys($args) - ) - ), - $indentation - ); - } - - /** - * @param InputObjectField|FieldArgument $arg - */ - protected static function printInputValue($arg): string - { - $argDecl = $arg->name . ': ' . (string) $arg->getType(); - - if ($arg->defaultValueExists()) { - $argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType())); - } - - return $argDecl; - } - /** * @param array $options */ public static function printType(Type $type, array $options = []): string { - if ($type instanceof ScalarType) { - if (FederatedSchema::RESERVED_TYPE_ANY !== $type->name) { - return static::printScalar($type, $options); - } - - return ''; - } - - if ($type instanceof EntityObjectType || $type instanceof EntityRefObjectType) { + if ($type instanceof EntityObjectType /* || $type instanceof EntityRefObjectType */) { return static::printEntityObject($type, $options); } - if ($type instanceof ObjectType) { - if (FederatedSchema::RESERVED_TYPE_SERVICE !== $type->name) { - return static::printObject($type, $options); - } - - return ''; - } - - if ($type instanceof InterfaceType) { - return static::printInterface($type, $options); - } - - if ($type instanceof UnionType) { - if (FederatedSchema::RESERVED_TYPE_ENTITY !== $type->name) { - return static::printUnion($type, $options); - } - + if (($type instanceof ScalarType && FederatedSchema::RESERVED_TYPE_ANY === $type->name) + || ($type instanceof ObjectType && FederatedSchema::RESERVED_TYPE_SERVICE === $type->name) + || ($type instanceof UnionType && FederatedSchema::RESERVED_TYPE_ENTITY === $type->name)) { return ''; } - if ($type instanceof EnumType) { - return static::printEnum($type, $options); - } - - if ($type instanceof InputObjectType) { - return static::printInputObject($type, $options); - } - - throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type))); - } - - /** - * @param array $options - */ - protected static function printScalar(ScalarType $type, array $options): string - { - return sprintf('%sscalar %s', static::printDescription($options, $type), $type->name); + return parent::printType($type, $options); } /** @@ -434,9 +176,9 @@ protected static function printEntityObject(EntityObjectType $type, array $optio /** * @param array $options - * @param EntityObjectType|InterfaceType|ObjectType $type + * @param EntityObjectType|InterfaceType|ObjectType|TypeWithFields $type */ - protected static function printFields(array $options, TypeWithFields $type): string + protected static function printFields(array $options, $type): string { $fields = array_values($type->getFields()); @@ -467,22 +209,6 @@ static function (FieldDefinition $f, $i) use ($options) { ); } - /** - * @param EnumValueDefinition|FieldDefinition $fieldOrEnumVal - */ - protected static function printDeprecated($fieldOrEnumVal): string - { - $reason = $fieldOrEnumVal->deprecationReason; - if (empty($reason)) { - return ''; - } - if ('' === $reason || Directive::DEFAULT_DEPRECATION_REASON === $reason) { - return ' @deprecated'; - } - - return ' @deprecated(reason: ' . Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; - } - protected static function printFieldFederatedDirectives(FieldDefinition $field): string { $directives = []; @@ -504,15 +230,6 @@ protected static function printFieldFederatedDirectives(FieldDefinition $field): return implode(' ', $directives); } - /** - * @param array $options - */ - protected static function printInterface(InterfaceType $type, array $options): string - { - return static::printDescription($options, $type) . - sprintf("interface %s {\n%s\n}", $type->name, static::printFields($options, $type)); - } - /** * Print simple and compound primary key fields * {@see https://www.apollographql.com/docs/federation/v1/entities#compound-primary-keys }. @@ -534,82 +251,4 @@ protected static function printKeyFields($keyFields): string return implode(' ', $parts); } - - /** - * @param array $options - */ - protected static function printUnion(UnionType $type, array $options): string - { - return static::printDescription($options, $type) . - sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes())); - } - - /** - * @param array $options - */ - protected static function printEnum(EnumType $type, array $options): string - { - return static::printDescription($options, $type) . - sprintf("enum %s {\n%s\n}", $type->name, static::printEnumValues($type->getValues(), $options)); - } - - /** - * @param EnumValueDefinition[] $values - * @param array $options - */ - protected static function printEnumValues(array $values, array $options): string - { - return implode( - "\n", - array_map( - static function ($value, $i) use ($options) { - return static::printDescription($options, $value, ' ', !$i) . - ' ' . - $value->name . - static::printDeprecated($value); - }, - $values, - array_keys($values) - ) - ); - } - - /** - * @param array $options - */ - protected static function printInputObject(InputObjectType $type, array $options): string - { - $fields = array_values($type->getFields()); - - return static::printDescription($options, $type) . - sprintf( - "input %s {\n%s\n}", - $type->name, - implode( - "\n", - array_map( - static function ($f, $i) use ($options) { - return static::printDescription($options, $f, ' ', !$i) . ' ' . static::printInputValue($f); - }, - $fields, - array_keys($fields) - ) - ) - ); - } - - /** - * @param array $options - * - * @api - */ - public static function printIntrospectionSchema(Schema $schema, array $options = []): string - { - return static::printFilteredSchema( - $schema, - [Directive::class, 'isSpecifiedDirective'], - [Introspection::class, 'isIntrospectionType'], - $options - ); - } } From 5077032875d4a4fa3e9b910fdbb73afe13343ca5 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 10:09:35 +0300 Subject: [PATCH 14/40] Reduce copy-paste & fix code style --- composer.json | 2 +- composer.lock | 67 +++++++++++++--------------- src/Utils/FederatedSchemaPrinter.php | 62 ++++++++++++------------- 3 files changed, 61 insertions(+), 70 deletions(-) diff --git a/composer.json b/composer.json index 1c12db3..9f615c0 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "library", "license": "MIT", "require": { - "php": "^7.1||^8.0", + "php": "^7.4||^8.0", "webonyx/graphql-php": "^0.13.8 || ^14.0" }, "scripts": { diff --git a/composer.lock b/composer.lock index 6aaf28b..2d31147 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "88c069015f1fba351e03821fd8011767", + "content-hash": "31bbd417e676ab9e2fa653e55383f263", "packages": [ { "name": "webonyx/graphql-php", @@ -160,9 +160,6 @@ "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" - }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", @@ -170,12 +167,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1006,12 +1003,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1927,12 +1924,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2006,12 +2003,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2087,12 +2084,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -2171,12 +2168,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2248,12 +2245,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -2606,12 +2603,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -2849,8 +2846,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.1||^8.0" + "php": "^7.4||^8.0" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 096291d..14ea4d2 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -71,12 +71,8 @@ public static function doPrint(Schema $schema, array $options = []): string { return static::printFilteredSchema( $schema, - static function (Directive $type): bool { - return !Directive::isSpecifiedDirective($type) && !static::isFederatedDirective($type); - }, - static function (Type $type): bool { - return !Type::isBuiltInType($type); - }, + static fn (Directive $type): bool => !Directive::isSpecifiedDirective($type) && !static::isFederatedDirective($type), + static fn (Type $type): bool => !Type::isBuiltInType($type), $options ); } @@ -113,16 +109,7 @@ protected static function printObject(ObjectType $type, array $options): string return ''; } - $interfaces = $type->getInterfaces(); - $implementedInterfaces = !empty($interfaces) - ? ' implements ' . - implode( - ' & ', - array_map(static function ($i) { - return $i->name; - }, $interfaces) - ) - : ''; + $implementedInterfaces = static::printImplementedInterfaces($type); $queryExtends = \in_array($type->name, [FederatedSchema::RESERVED_TYPE_QUERY, FederatedSchema::RESERVED_TYPE_MUTATION], true) ? 'extend ' @@ -143,22 +130,8 @@ protected static function printObject(ObjectType $type, array $options): string */ protected static function printEntityObject(EntityObjectType $type, array $options): string { - $interfaces = $type->getInterfaces(); - $implementedInterfaces = !empty($interfaces) - ? ' implements ' . - implode( - ' & ', - array_map(static function ($i) { - return $i->name; - }, $interfaces) - ) - : ''; - - $keyDirective = ''; - - foreach ($type->getKeyFields() as $keyField) { - $keyDirective = $keyDirective . sprintf(' @key(fields: "%s")', static::printKeyFields($keyField)); - } + $implementedInterfaces = static::printImplementedInterfaces($type); + $keyDirective = static::printKeyDirective($type); $isEntityRef = $type instanceof EntityRefObjectType; $extends = $isEntityRef ? 'extend ' : ''; @@ -184,8 +157,9 @@ protected static function printFields(array $options, $type): string if (FederatedSchema::RESERVED_TYPE_QUERY === $type->name) { $fields = array_filter($fields, static function (FieldDefinition $field): bool { - return FederatedSchema::RESERVED_FIELD_SERVICE !== $field->name - && FederatedSchema::RESERVED_FIELD_ENTITIES !== $field->name; + $excludedFields = [FederatedSchema::RESERVED_FIELD_SERVICE, FederatedSchema::RESERVED_FIELD_ENTITIES]; + + return !\in_array($field->name, $excludedFields, true); }); } @@ -230,6 +204,26 @@ protected static function printFieldFederatedDirectives(FieldDefinition $field): return implode(' ', $directives); } + protected static function printImplementedInterfaces(ObjectType $type): string + { + $interfaces = $type->getInterfaces(); + + return !empty($interfaces) + ? ' implements ' . implode(' & ', array_map(static fn (InterfaceType $i): string => $i->name, $interfaces)) + : ''; + } + + protected static function printKeyDirective(EntityObjectType $type): string + { + $keyDirective = ''; + + foreach ($type->getKeyFields() as $keyField) { + $keyDirective .= sprintf(' @key(fields: "%s")', static::printKeyFields($keyField)); + } + + return $keyDirective; + } + /** * Print simple and compound primary key fields * {@see https://www.apollographql.com/docs/federation/v1/entities#compound-primary-keys }. From d23332d062d7c031c8a61babc6b5cfac183ebf07 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 10:21:46 +0300 Subject: [PATCH 15/40] Refactor code to make more readable --- src/FederatedSchema.php | 5 +++++ src/Utils/FederatedSchemaPrinter.php | 11 +++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index 757109e..0530afd 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -74,6 +74,11 @@ class FederatedSchema extends Schema /** @var Directive[] */ protected $entityDirectives; + public static function isReservedRootType(string $name): bool + { + return \in_array($name, [self::RESERVED_TYPE_QUERY, self::RESERVED_TYPE_MUTATION], true); + } + /** * @param array $config */ diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 14ea4d2..035864d 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -110,15 +110,12 @@ protected static function printObject(ObjectType $type, array $options): string } $implementedInterfaces = static::printImplementedInterfaces($type); - - $queryExtends = \in_array($type->name, [FederatedSchema::RESERVED_TYPE_QUERY, FederatedSchema::RESERVED_TYPE_MUTATION], true) - ? 'extend ' - : ''; + $extends = FederatedSchema::isReservedRootType($type->name) ? 'extend ' : ''; return static::printDescription($options, $type) . sprintf( "%stype %s%s {\n%s\n}", - $queryExtends, + $extends, $type->name, $implementedInterfaces, static::printFields($options, $type) @@ -132,9 +129,7 @@ protected static function printEntityObject(EntityObjectType $type, array $optio { $implementedInterfaces = static::printImplementedInterfaces($type); $keyDirective = static::printKeyDirective($type); - - $isEntityRef = $type instanceof EntityRefObjectType; - $extends = $isEntityRef ? 'extend ' : ''; + $extends = $type instanceof EntityRefObjectType ? 'extend ' : ''; return static::printDescription($options, $type) . sprintf( From 5724b2c038a0f610ea10c386c635a45a67d6e56b Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 10:25:55 +0300 Subject: [PATCH 16/40] Fix code style --- src/Directives.php | 14 +++++++------- src/Directives/ExternalDirective.php | 2 +- src/Directives/KeyDirective.php | 10 +++++----- src/Directives/ProvidesDirective.php | 10 +++++----- src/Directives/RequiresDirective.php | 10 +++++----- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Directives.php b/src/Directives.php index 8ee46ca..ec21a05 100644 --- a/src/Directives.php +++ b/src/Directives.php @@ -4,8 +4,8 @@ namespace Apollo\Federation; -use Apollo\Federation\Directives\KeyDirective; use Apollo\Federation\Directives\ExternalDirective; +use Apollo\Federation\Directives\KeyDirective; use Apollo\Federation\Directives\ProvidesDirective; use Apollo\Federation\Directives\RequiresDirective; use Apollo\Federation\Enum\DirectiveEnum; @@ -20,7 +20,7 @@ class Directives private static $directives = null; /** - * Gets the @key directive + * Gets the @key directive. */ public static function key(): KeyDirective { @@ -28,7 +28,7 @@ public static function key(): KeyDirective } /** - * Gets the @external directive + * Gets the @external directive. */ public static function external(): ExternalDirective { @@ -36,7 +36,7 @@ public static function external(): ExternalDirective } /** - * Gets the @requires directive + * Gets the @requires directive. */ public static function requires(): RequiresDirective { @@ -44,7 +44,7 @@ public static function requires(): RequiresDirective } /** - * Gets the @provides directive + * Gets the @provides directive. */ public static function provides(): ProvidesDirective { @@ -52,7 +52,7 @@ public static function provides(): ProvidesDirective } /** - * Gets the directives that can be used on federated entity types + * Gets the directives that can be used on federated entity types. * * @return array */ @@ -60,8 +60,8 @@ public static function getDirectives(): array { if (!self::$directives) { self::$directives = [ - DirectiveEnum::KEY => new KeyDirective(), DirectiveEnum::EXTERNAL => new ExternalDirective(), + DirectiveEnum::KEY => new KeyDirective(), DirectiveEnum::REQUIRES => new RequiresDirective(), DirectiveEnum::PROVIDES => new ProvidesDirective(), ]; diff --git a/src/Directives/ExternalDirective.php b/src/Directives/ExternalDirective.php index 7a837b5..3c8d024 100644 --- a/src/Directives/ExternalDirective.php +++ b/src/Directives/ExternalDirective.php @@ -17,7 +17,7 @@ public function __construct() { parent::__construct([ 'name' => DirectiveEnum::EXTERNAL, - 'locations' => [DirectiveLocation::FIELD_DEFINITION] + 'locations' => [DirectiveLocation::FIELD_DEFINITION], ]); } } diff --git a/src/Directives/KeyDirective.php b/src/Directives/KeyDirective.php index 08623d8..4129547 100644 --- a/src/Directives/KeyDirective.php +++ b/src/Directives/KeyDirective.php @@ -3,10 +3,10 @@ namespace Apollo\Federation\Directives; use Apollo\Federation\Enum\DirectiveEnum; -use GraphQL\Type\Definition\Type; +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 @@ -22,9 +22,9 @@ public function __construct() 'args' => [ new FieldArgument([ 'name' => 'fields', - 'type' => Type::nonNull(Type::string()) - ]) - ] + 'type' => Type::nonNull(Type::string()), + ]), + ], ]); } } diff --git a/src/Directives/ProvidesDirective.php b/src/Directives/ProvidesDirective.php index f474cae..24d9be1 100644 --- a/src/Directives/ProvidesDirective.php +++ b/src/Directives/ProvidesDirective.php @@ -3,10 +3,10 @@ namespace Apollo\Federation\Directives; use Apollo\Federation\Enum\DirectiveEnum; -use GraphQL\Type\Definition\Type; +use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; -use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\Definition\Type; /** * The `@provides` directive is used to annotate the expected returned fieldset from a field @@ -22,9 +22,9 @@ public function __construct() 'args' => [ new FieldArgument([ 'name' => 'fields', - 'type' => Type::nonNull(Type::string()) - ]) - ] + 'type' => Type::nonNull(Type::string()), + ]), + ], ]); } } diff --git a/src/Directives/RequiresDirective.php b/src/Directives/RequiresDirective.php index ab8d9e7..d33bec3 100644 --- a/src/Directives/RequiresDirective.php +++ b/src/Directives/RequiresDirective.php @@ -3,10 +3,10 @@ namespace Apollo\Federation\Directives; use Apollo\Federation\Enum\DirectiveEnum; -use GraphQL\Type\Definition\Type; +use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; -use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\Definition\Type; /** * The `@requires` directive is used to annotate the required input fieldset from a base type @@ -23,9 +23,9 @@ public function __construct() 'args' => [ new FieldArgument([ 'name' => 'fields', - 'type' => Type::nonNull(Type::string()) - ]) - ] + 'type' => Type::nonNull(Type::string()), + ]), + ], ]); } } From 3e217deab7dce39aaeddfd04485d444af3a84567 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 10:58:13 +0300 Subject: [PATCH 17/40] Extract FederatedSchemaTrait --- src/FederatedSchema.php | 174 +------------------------------ src/FederatedSchemaTrait.php | 195 +++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 172 deletions(-) create mode 100644 src/FederatedSchemaTrait.php diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index 0530afd..f3b3fd4 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -56,6 +56,8 @@ */ class FederatedSchema extends Schema { + use FederatedSchemaTrait; + public const RESERVED_TYPE_ANY = '_Any'; public const RESERVED_TYPE_ENTITY = '_Entity'; public const RESERVED_TYPE_SERVICE = '_Service'; @@ -68,12 +70,6 @@ class FederatedSchema extends Schema public const RESERVED_FIELD_SERVICE = '_service'; public const RESERVED_FIELD_TYPE_NAME = '__typename'; - /** @var EntityObjectType[] */ - protected $entityTypes; - - /** @var Directive[] */ - protected $entityDirectives; - public static function isReservedRootType(string $name): bool { return \in_array($name, [self::RESERVED_TYPE_QUERY, self::RESERVED_TYPE_MUTATION], true); @@ -91,170 +87,4 @@ public function __construct(array $config) parent::__construct($config); } - - /** - * Returns all the resolved entity types in the schema. - * - * @return EntityObjectType[] - */ - public function getEntityTypes(): array - { - return $this->entityTypes; - } - - /** - * Indicates whether the schema has entity types resolved. - */ - public function hasEntityTypes(): bool - { - return !empty($this->getEntityTypes()); - } - - /** - * @param array $config - * - * @return array - */ - private function getEntityDirectivesConfig(array $config): array - { - $directives = isset($config['directives']) ? $config['directives'] : []; - $config['directives'] = array_merge($directives, $this->entityDirectives); - - return $config; - } - - /** - * @param array $config - * - * @return array{ query: ObjectType } - */ - private function getQueryTypeConfig(array $config): array - { - $queryTypeConfig = $config['query']->config; - if (\is_callable($queryTypeConfig['fields'])) { - $queryTypeConfig['fields'] = $queryTypeConfig['fields'](); - } - - $queryTypeConfig['fields'] = array_merge( - $queryTypeConfig['fields'], - $this->getQueryTypeServiceFieldConfig(), - $this->getQueryTypeEntitiesFieldConfig($config) - ); - - return [ - 'query' => new ObjectType($queryTypeConfig), - ]; - } - - /** - * @return array{ _service: array } - */ - private function getQueryTypeServiceFieldConfig(): array - { - $serviceType = new ObjectType([ - 'name' => self::RESERVED_TYPE_SERVICE, - 'fields' => [ - self::RESERVED_FIELD_SDL => [ - 'type' => Type::string(), - 'resolve' => function () { - return FederatedSchemaPrinter::doPrint($this); - }, - ], - ], - ]); - - return [ - self::RESERVED_FIELD_SERVICE => [ - 'type' => Type::nonNull($serviceType), - 'resolve' => function () { - return []; - }, - ], - ]; - } - - /** - * @param array|null $config - * - * @return array>, resolve: callable }> - */ - private function getQueryTypeEntitiesFieldConfig(?array $config): array - { - if (!$this->hasEntityTypes()) { - return []; - } - - $entityType = new UnionType([ - 'name' => self::RESERVED_TYPE_ENTITY, - 'types' => array_values($this->getEntityTypes()), - ]); - - $anyType = new CustomScalarType([ - 'name' => self::RESERVED_TYPE_ANY, - 'serialize' => function ($value) { - return $value; - }, - ]); - - return [ - self::RESERVED_FIELD_ENTITIES => [ - 'type' => Type::listOf($entityType), - 'args' => [ - self::RESERVED_FIELD_REPRESENTATIONS => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))), - ], - ], - 'resolve' => function ($root, $args, $context, $info) use ($config) { - if ($config && isset($config['resolve']) && \is_callable($config['resolve'])) { - return $config['resolve']($root, $args, $context, $info); - } - - return $this->resolve($root, $args, $context, $info); - }, - ], - ]; - } - - private function resolve($root, $args, $context, $info): array - { - return array_map(static function ($ref) use ($context, $info) { - Utils::invariant(isset($ref[self::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); - - $typeName = $ref[self::RESERVED_FIELD_TYPE_NAME]; - $type = $info->schema->getType($typeName); - - Utils::invariant( - $type && $type instanceof EntityObjectType, - sprintf( - 'The _entities resolver tried to load an entity for type "%s", but no object type of that name was found in the schema', - $type->name - ) - ); - - if (!$type->hasReferenceResolver()) { - return $ref; - } - - return $type->resolveReference($ref, $context, $info); - }, $args[self::RESERVED_FIELD_REPRESENTATIONS]); - } - - /** - * @param array $config - * - * @return EntityObjectType[] - */ - private function extractEntityTypes(array $config): array - { - $resolvedTypes = TypeInfo::extractTypes($config['query']); - $entityTypes = []; - - foreach ($resolvedTypes as $type) { - if ($type instanceof EntityObjectType) { - $entityTypes[$type->name] = $type; - } - } - - return $entityTypes; - } } diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php new file mode 100644 index 0000000..cc57ebc --- /dev/null +++ b/src/FederatedSchemaTrait.php @@ -0,0 +1,195 @@ +entityTypes; + } + + /** + * Indicates whether the schema has entity types resolved. + */ + public function hasEntityTypes(): bool + { + return !empty($this->getEntityTypes()); + } + + /** + * @param array $config + * + * @return array + */ + protected function getEntityDirectivesConfig(array $config): array + { + $directives = $config['directives'] ?? []; + $config['directives'] = array_merge($directives, $this->entityDirectives); + + return $config; + } + + /** + * @param array $config + * + * @return array{ query: ObjectType } + */ + protected function getQueryTypeConfig(array $config): array + { + $queryTypeConfig = $config['query']->config; + if (\is_callable($queryTypeConfig['fields'])) { + $queryTypeConfig['fields'] = $queryTypeConfig['fields'](); + } + + $queryTypeConfig['fields'] = array_merge( + $queryTypeConfig['fields'], + $this->getQueryTypeServiceFieldConfig(), + $this->getQueryTypeEntitiesFieldConfig($config) + ); + + return [ + 'query' => new ObjectType($queryTypeConfig), + ]; + } + + /** + * @return array{ _service: array } + */ + protected function getQueryTypeServiceFieldConfig(): array + { + $serviceType = new ObjectType([ + 'name' => self::RESERVED_TYPE_SERVICE, + 'fields' => [ + self::RESERVED_FIELD_SDL => [ + 'type' => Type::string(), + 'resolve' => function () { + return FederatedSchemaPrinter::doPrint($this); + }, + ], + ], + ]); + + return [ + self::RESERVED_FIELD_SERVICE => [ + 'type' => Type::nonNull($serviceType), + 'resolve' => function () { + return []; + }, + ], + ]; + } + + /** + * @param array|null $config + * + * @return array>, resolve: callable }> + */ + protected function getQueryTypeEntitiesFieldConfig(?array $config): array + { + if (!$this->hasEntityTypes()) { + return []; + } + + $entityType = new UnionType([ + 'name' => self::RESERVED_TYPE_ENTITY, + 'types' => array_values($this->getEntityTypes()), + ]); + + $anyType = new CustomScalarType([ + 'name' => self::RESERVED_TYPE_ANY, + 'serialize' => function ($value) { + return $value; + }, + ]); + + return [ + self::RESERVED_FIELD_ENTITIES => [ + 'type' => Type::listOf($entityType), + 'args' => [ + self::RESERVED_FIELD_REPRESENTATIONS => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))), + ], + ], + 'resolve' => function ($root, $args, $context, $info) use ($config) { + if ($config && isset($config['resolve']) && \is_callable($config['resolve'])) { + return $config['resolve']($root, $args, $context, $info); + } + + return $this->resolve($root, $args, $context, $info); + }, + ], + ]; + } + + protected function resolve($root, $args, $context, $info): array + { + return array_map(static function ($ref) use ($context, $info) { + Utils::invariant(isset($ref[self::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); + + $typeName = $ref[self::RESERVED_FIELD_TYPE_NAME]; + $type = $info->schema->getType($typeName); + + Utils::invariant( + $type && $type instanceof EntityObjectType, + sprintf( + 'The _entities resolver tried to load an entity for type "%s", but no object type of that name was found in the schema', + $type->name + ) + ); + + if (!$type->hasReferenceResolver()) { + return $ref; + } + + return $type->resolveReference($ref, $context, $info); + }, $args[self::RESERVED_FIELD_REPRESENTATIONS]); + } + + /** + * @param array $config + * + * @return EntityObjectType[] + */ + protected function extractEntityTypes(array $config): array + { + $resolvedTypes = TypeInfo::extractTypes($config['query']); + $entityTypes = []; + + foreach ($resolvedTypes as $type) { + if ($type instanceof EntityObjectType) { + $entityTypes[$type->name] = $type; + } + } + + return $entityTypes; + } +} From 4a21ab5338d873bfd696b4903e64597648b6f6b4 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 11:22:06 +0300 Subject: [PATCH 18/40] Fix code style --- src/FederatedSchemaTrait.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php index cc57ebc..5afbffe 100644 --- a/src/FederatedSchemaTrait.php +++ b/src/FederatedSchemaTrait.php @@ -91,9 +91,7 @@ protected function getQueryTypeServiceFieldConfig(): array 'fields' => [ self::RESERVED_FIELD_SDL => [ 'type' => Type::string(), - 'resolve' => function () { - return FederatedSchemaPrinter::doPrint($this); - }, + 'resolve' => fn (): string => FederatedSchemaPrinter::doPrint($this), ], ], ]); @@ -101,9 +99,7 @@ protected function getQueryTypeServiceFieldConfig(): array return [ self::RESERVED_FIELD_SERVICE => [ 'type' => Type::nonNull($serviceType), - 'resolve' => function () { - return []; - }, + 'resolve' => static fn (): array => [], ], ]; } @@ -126,9 +122,7 @@ protected function getQueryTypeEntitiesFieldConfig(?array $config): array $anyType = new CustomScalarType([ 'name' => self::RESERVED_TYPE_ANY, - 'serialize' => function ($value) { - return $value; - }, + 'serialize' => static fn ($value) => $value, ]); return [ From 8ad004784e345b74e12b2be970b29ac03ff0a3ed Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 11:22:17 +0300 Subject: [PATCH 19/40] Fix tests code style --- test/DirectivesTest.php | 16 ++++---- test/EntitiesTest.php | 26 ++++++------ test/SchemaTest.php | 30 +++++++------- test/StarWarsData.php | 88 ++++++++++++++++++++++++++--------------- test/StarWarsSchema.php | 83 ++++++++++++++++++-------------------- 5 files changed, 131 insertions(+), 112 deletions(-) diff --git a/test/DirectivesTest.php b/test/DirectivesTest.php index 474e477..7023c6f 100644 --- a/test/DirectivesTest.php +++ b/test/DirectivesTest.php @@ -17,7 +17,7 @@ class DirectivesTest extends TestCase { use MatchesSnapshots; - public function testKeyDirective() + public function testKeyDirective(): void { $config = Directives::key()->config; @@ -27,7 +27,7 @@ public function testKeyDirective() $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); } - public function testExternalDirective() + public function testExternalDirective(): void { $config = Directives::external()->config; @@ -37,7 +37,7 @@ public function testExternalDirective() $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); } - public function testRequiresDirective() + public function testRequiresDirective(): void { $config = Directives::requires()->config; @@ -47,7 +47,7 @@ public function testRequiresDirective() $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); } - public function testProvidesDirective() + public function testProvidesDirective(): void { $config = Directives::provides()->config; @@ -57,16 +57,16 @@ public function testProvidesDirective() $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); } - public function testItAddsDirectivesToSchema() + public function testItAddsDirectivesToSchema(): void { $schema = new Schema([ 'query' => new ObjectType([ 'name' => 'Query', 'fields' => [ - '_' => ['type' => Type::string()] - ] + '_' => ['type' => Type::string()], + ], ]), - 'directives' => Directives::getDirectives() + 'directives' => Directives::getDirectives(), ]); $schemaSdl = SchemaPrinter::doPrint($schema); diff --git a/test/EntitiesTest.php b/test/EntitiesTest.php index 2acb5d5..e0e51ec 100644 --- a/test/EntitiesTest.php +++ b/test/EntitiesTest.php @@ -14,7 +14,7 @@ class EntitiesTest extends TestCase { use MatchesSnapshots; - public function testCreatingEntityType() + public function testCreatingEntityType(): void { $userTypeKeyFields = ['id', 'email']; @@ -25,15 +25,15 @@ public function testCreatingEntityType() 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], 'firstName' => ['type' => Type::string()], - 'lastName' => ['type' => Type::string()] - ] + 'lastName' => ['type' => Type::string()], + ], ]); $this->assertEqualsCanonicalizing($userType->getKeyFields(), $userTypeKeyFields); $this->assertMatchesSnapshot($userType->config); } - public function testCreatingEntityTypeWithCallable() + public function testCreatingEntityTypeWithCallable(): void { $userTypeKeyFields = ['id', 'email']; @@ -45,23 +45,23 @@ public function testCreatingEntityTypeWithCallable() 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], 'firstName' => ['type' => Type::string()], - 'lastName' => ['type' => Type::string()] + 'lastName' => ['type' => Type::string()], ]; - } + }, ]); $this->assertEqualsCanonicalizing($userType->getKeyFields(), $userTypeKeyFields); $this->assertMatchesSnapshot($userType->config); } - public function testResolvingEntityReference() + public function testResolvingEntityReference(): void { $expectedRef = [ 'id' => 1, 'email' => 'luke@skywalker.com', 'firstName' => 'Luke', 'lastName' => 'Skywalker', - '__typename' => 'User' + '__typename' => 'User', ]; $userType = new EntityObjectType([ @@ -71,11 +71,11 @@ public function testResolvingEntityReference() 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], 'firstName' => ['type' => Type::string()], - 'lastName' => ['type' => Type::string()] + 'lastName' => ['type' => Type::string()], ], '__resolveReference' => function () use ($expectedRef) { return $expectedRef; - } + }, ]); $actualRef = $userType->resolveReference(['id' => 1, 'email' => 'luke@skywalker.com', '__typename' => 'User']); @@ -83,7 +83,7 @@ public function testResolvingEntityReference() $this->assertEquals($expectedRef, $actualRef); } - public function testCreatingEntityRefType() + public function testCreatingEntityRefType(): void { $userTypeKeyFields = ['id', 'email']; @@ -92,8 +92,8 @@ public function testCreatingEntityRefType() 'keyFields' => $userTypeKeyFields, 'fields' => [ 'id' => ['type' => Type::int()], - 'email' => ['type' => Type::string()] - ] + 'email' => ['type' => Type::string()], + ], ]); $this->assertEqualsCanonicalizing($userType->getKeyFields(), $userTypeKeyFields); diff --git a/test/SchemaTest.php b/test/SchemaTest.php index 4262bba..e18de09 100644 --- a/test/SchemaTest.php +++ b/test/SchemaTest.php @@ -13,7 +13,7 @@ class SchemaTest extends TestCase { use MatchesSnapshots; - public function testRunningQueries() + public function testRunningQueries(): void { $schema = StarWarsSchema::getEpisodesSchema(); $query = 'query GetEpisodes { episodes { id title characters { id name } } }'; @@ -23,7 +23,7 @@ public function testRunningQueries() $this->assertMatchesSnapshot($result->toArray()); } - public function testEntityTypes() + public function testEntityTypes(): void { $schema = StarWarsSchema::getEpisodesSchema(); @@ -36,7 +36,7 @@ public function testEntityTypes() $this->assertArrayHasKey('Location', $entityTypes); } - public function testMetaTypes() + public function testMetaTypes(): void { $schema = StarWarsSchema::getEpisodesSchema(); @@ -48,7 +48,7 @@ public function testMetaTypes() $this->assertEqualsCanonicalizing($entitiesType->getTypes(), array_values($schema->getEntityTypes())); } - public function testDirectives() + public function testDirectives(): void { $schema = StarWarsSchema::getEpisodesSchema(); $directives = $schema->getDirectives(); @@ -59,7 +59,7 @@ public function testDirectives() $this->assertArrayHasKey('requires', $directives); } - public function testServiceSdl() + public function testServiceSdl(): void { $schema = StarWarsSchema::getEpisodesSchema(); $query = 'query GetServiceSdl { _service { sdl } }'; @@ -69,7 +69,7 @@ public function testServiceSdl() $this->assertMatchesSnapshot($result->toArray()); } - public function testSchemaSdl() + public function testSchemaSdl(): void { $schema = StarWarsSchema::getEpisodesSchema(); $schemaSdl = SchemaPrinter::doPrint($schema); @@ -77,11 +77,10 @@ public function testSchemaSdl() $this->assertMatchesSnapshot($schemaSdl); } - public function testResolvingEntityReferences() + public function testResolvingEntityReferences(): void { $schema = StarWarsSchema::getEpisodesSchema(); - $query = ' query GetEpisodes($representations: [_Any!]!) { _entities(representations: $representations) { @@ -98,8 +97,8 @@ public function testResolvingEntityReferences() [ '__typename' => 'Episode', 'id' => 1, - ] - ] + ], + ], ]; $result = GraphQL::executeQuery($schema, $query, null, null, $variables); @@ -107,7 +106,7 @@ public function testResolvingEntityReferences() $this->assertMatchesSnapshot($result->toArray()); } - public function testOverrideSchemaResolver() + public function testOverrideSchemaResolver(): void { $schema = StarWarsSchema::getEpisodesSchemaCustomResolver(); @@ -126,16 +125,15 @@ public function testOverrideSchemaResolver() 'representations' => [ [ '__typename' => 'Episode', - 'id' => 1 - ] - ] + 'id' => 1, + ], + ], ]; $result = GraphQL::executeQuery($schema, $query, null, null, $variables); // The custom resolver for this schema, always adds 1 to the id and gets the next // episode for the sake of testing the ability to change the resolver in the configuration - $this->assertEquals("The Empire Strikes Back", $result->data['_entities'][0]["title"]); + $this->assertEquals('The Empire Strikes Back', $result->data['_entities'][0]['title']); $this->assertMatchesSnapshot($result->toArray()); } } - \ No newline at end of file diff --git a/test/StarWarsData.php b/test/StarWarsData.php index c1cefa2..27bd28e 100644 --- a/test/StarWarsData.php +++ b/test/StarWarsData.php @@ -6,100 +6,126 @@ class StarWarsData { - private static $episodes; + /** + * @var array>|null + */ + private static ?array $episodes = null; - private static $characters; + /** + * @var array>|null + */ + private static ?array $characters = null; - private static $locations; + /** + * @var array>|null + */ + private static ?array $locations = null; - public static function getEpisodeById($id) + /** + * @return array|null + */ + public static function getEpisodeById(int $id): ?array { - $matches = array_filter(self::getEpisodes(), function ($episode) use ($id) { - return $episode['id'] === $id; - }); - return reset($matches); + $matches = array_filter(self::getEpisodes(), static fn (array $episode): bool => $episode['id'] === $id); + + return reset($matches) ?: null; } - public static function getEpisodes() + /** + * @return array> + */ + public static function getEpisodes(): array { if (!self::$episodes) { self::$episodes = [ [ 'id' => 1, 'title' => 'A New Hope', - 'characters' => [1, 2, 3] + 'characters' => [1, 2, 3], ], [ 'id' => 2, 'title' => 'The Empire Strikes Back', - 'characters' => [1, 2, 3] + 'characters' => [1, 2, 3], ], [ 'id' => 3, 'title' => 'Return of the Jedi', - 'characters' => [1, 2, 3] - ] + 'characters' => [1, 2, 3], + ], ]; } return self::$episodes; } - public static function getCharactersByIds($ids) + /** + * @param int[] $ids + * + * @return array> + */ + public static function getCharactersByIds(array $ids): array { - return array_filter(self::getCharacters(), function ($character) use ($ids) { - return in_array($character['id'], $ids); - }); + return array_filter(self::getCharacters(), static fn ($item): bool => \in_array($item['id'], $ids, true)); } - public static function getCharacters() + /** + * @return array> + */ + public static function getCharacters(): array { if (!self::$characters) { self::$characters = [ [ 'id' => 1, 'name' => 'Luke Skywalker', - 'locations' => [1, 2, 3] + 'locations' => [1, 2, 3], ], [ 'id' => 2, 'name' => 'Han Solo', - 'locations' => [1, 2] + 'locations' => [1, 2], ], [ 'id' => 3, 'name' => 'Leia Skywalker', - 'locations' => [3] - ] + 'locations' => [3], + ], ]; } return self::$characters; } - public static function getLocationsByIds($ids) + /** + * @param int[] $ids + * + * @return array> + */ + public static function getLocationsByIds(array $ids): array { - return array_filter(self::getLocations(), function ($location) use ($ids) { - return in_array($location['id'], $ids); - }); + return array_filter(self::getLocations(), static fn ($item): bool => \in_array($item['id'], $ids, true)); } - public static function getLocations() + /** + * @return array> + */ + public static function getLocations(): array { if (!self::$locations) { self::$locations = [ [ 'id' => 1, - 'name' => 'Tatooine' + 'name' => 'Tatooine', ], [ 'id' => 2, - 'name' => 'Endor' + 'name' => 'Endor', ], [ 'id' => 3, - 'name' => 'Hoth' - ] + 'name' => 'Hoth', + ], ]; } diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index 9ec1f8b..6786d4b 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -4,66 +4,65 @@ namespace Apollo\Federation\Tests; -use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ObjectType; - use Apollo\Federation\FederatedSchema; use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; class StarWarsSchema { - public static $episodesSchema; - public static $overRiddedEpisodesSchema; + public static ?FederatedSchema $episodesSchema = null; + public static ?FederatedSchema $overriddenEpisodesSchema = null; public static function getEpisodesSchema(): FederatedSchema { if (!self::$episodesSchema) { self::$episodesSchema = new FederatedSchema([ - 'query' => self::getQueryType() + 'query' => self::getQueryType(), ]); } + return self::$episodesSchema; } public static function getEpisodesSchemaCustomResolver(): FederatedSchema { - if (!self::$overRiddedEpisodesSchema) { - self::$overRiddedEpisodesSchema = new FederatedSchema([ + if (!self::$overriddenEpisodesSchema) { + self::$overriddenEpisodesSchema = new FederatedSchema([ 'query' => self::getQueryType(), - 'resolve' => function ($root, $args, $context, $info) { - return array_map(function ($ref) use ($info) { + 'resolve' => function ($root, $args, $context, $info): array { + return array_map(static function (array $ref) use ($info) { $typeName = $ref['__typename']; $type = $info->schema->getType($typeName); - $ref["id"] = $ref["id"] + 1; + ++$ref['id']; + return $type->resolveReference($ref); }, $args[FederatedSchema::RESERVED_FIELD_REPRESENTATIONS]); - } + }, ]); } - return self::$overRiddedEpisodesSchema; + + return self::$overriddenEpisodesSchema; } private static function getQueryType(): ObjectType { $episodeType = self::getEpisodeType(); - $queryType = new ObjectType([ + return new ObjectType([ 'name' => FederatedSchema::RESERVED_TYPE_QUERY, 'fields' => [ 'episodes' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($episodeType))), - 'resolve' => function () { - return StarWarsData::getEpisodes(); - } + 'resolve' => static fn (): array => StarWarsData::getEpisodes(), ], 'deprecatedEpisodes' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($episodeType))), - 'deprecationReason' => 'Because you should use the other one.' - ] - ] + 'deprecationReason' => 'Because you should use the other one.', + ], + ], ]); - return $queryType; } private static function getEpisodeType(): EntityObjectType @@ -73,26 +72,24 @@ private static function getEpisodeType(): EntityObjectType 'description' => 'A film in the Star Wars Trilogy', 'fields' => [ 'id' => [ - 'type' => Type::nonNull(Type::int()) + 'type' => Type::nonNull(Type::int()), ], 'title' => [ - 'type' => Type::nonNull(Type::string()) + 'type' => Type::nonNull(Type::string()), ], 'characters' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::getCharacterType()))), - 'resolve' => function ($root) { - return StarWarsData::getCharactersByIds($root['characters']); - }, - 'provides' => 'name' - ] + 'resolve' => static fn ($root): array => StarWarsData::getCharactersByIds($root['characters']), + 'provides' => 'name', + ], ], EntityObjectType::FIELD_KEY_FIELDS => ['id'], - EntityObjectType::FIELD_REFERENCE_RESOLVER => function ($ref) { - // print_r($ref); + EntityObjectType::FIELD_REFERENCE_RESOLVER => static function (array $ref): array { $entity = StarWarsData::getEpisodeById($ref['id']); - $entity["__typename"] = "Episode"; + $entity['__typename'] = 'Episode'; + return $entity; - } + }, ]); } @@ -104,21 +101,19 @@ private static function getCharacterType(): EntityRefObjectType 'fields' => [ 'id' => [ 'type' => Type::nonNull(Type::int()), - 'isExternal' => true + 'isExternal' => true, ], 'name' => [ 'type' => Type::nonNull(Type::string()), - 'isExternal' => true + 'isExternal' => true, ], 'locations' => [ 'type' => Type::nonNull(Type::listOf(self::getLocationType())), - 'resolve' => function ($root) { - return StarWarsData::getLocationsByIds($root['locations']); - }, - 'requires' => 'name' - ] + 'resolve' => static fn ($root): array => StarWarsData::getLocationsByIds($root['locations']), + 'requires' => 'name', + ], ], - EntityObjectType::FIELD_KEY_FIELDS => ['id'] + EntityObjectType::FIELD_KEY_FIELDS => ['id'], ]); } @@ -130,14 +125,14 @@ private static function getLocationType(): EntityRefObjectType 'fields' => [ 'id' => [ 'type' => Type::nonNull(Type::int()), - 'isExternal' => true + 'isExternal' => true, ], 'name' => [ 'type' => Type::nonNull(Type::string()), - 'isExternal' => true - ] + 'isExternal' => true, + ], ], - EntityObjectType::FIELD_KEY_FIELDS => ['id'] + EntityObjectType::FIELD_KEY_FIELDS => ['id'], ]); } } From e0c45d7528f688dcfc68095866b7db523b84f4c5 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 11:41:42 +0300 Subject: [PATCH 20/40] Fix method call params order --- test/DirectivesTest.php | 16 ++++++++-------- test/EntitiesTest.php | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/DirectivesTest.php b/test/DirectivesTest.php index 7023c6f..a71aa04 100644 --- a/test/DirectivesTest.php +++ b/test/DirectivesTest.php @@ -23,8 +23,8 @@ public function testKeyDirective(): void $expectedLocations = [DirectiveLocation::OBJECT, DirectiveLocation::IFACE]; - $this->assertEquals($config['name'], 'key'); - $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); + $this->assertEquals('key', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } public function testExternalDirective(): void @@ -33,8 +33,8 @@ public function testExternalDirective(): void $expectedLocations = [DirectiveLocation::FIELD_DEFINITION]; - $this->assertEquals($config['name'], 'external'); - $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); + $this->assertEquals('external', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } public function testRequiresDirective(): void @@ -43,8 +43,8 @@ public function testRequiresDirective(): void $expectedLocations = [DirectiveLocation::FIELD_DEFINITION]; - $this->assertEquals($config['name'], 'requires'); - $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); + $this->assertEquals('requires', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } public function testProvidesDirective(): void @@ -53,8 +53,8 @@ public function testProvidesDirective(): void $expectedLocations = [DirectiveLocation::FIELD_DEFINITION]; - $this->assertEquals($config['name'], 'provides'); - $this->assertEqualsCanonicalizing($config['locations'], $expectedLocations); + $this->assertEquals('provides', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } public function testItAddsDirectivesToSchema(): void diff --git a/test/EntitiesTest.php b/test/EntitiesTest.php index e0e51ec..9c03f73 100644 --- a/test/EntitiesTest.php +++ b/test/EntitiesTest.php @@ -16,11 +16,11 @@ class EntitiesTest extends TestCase public function testCreatingEntityType(): void { - $userTypeKeyFields = ['id', 'email']; + $expectedKeyFields = ['id', 'email']; $userType = new EntityObjectType([ 'name' => 'User', - 'keyFields' => $userTypeKeyFields, + 'keyFields' => $expectedKeyFields, 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], @@ -29,17 +29,17 @@ public function testCreatingEntityType(): void ], ]); - $this->assertEqualsCanonicalizing($userType->getKeyFields(), $userTypeKeyFields); + $this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields()); $this->assertMatchesSnapshot($userType->config); } public function testCreatingEntityTypeWithCallable(): void { - $userTypeKeyFields = ['id', 'email']; + $expectedKeyFields = ['id', 'email']; $userType = new EntityObjectType([ 'name' => 'User', - 'keyFields' => $userTypeKeyFields, + 'keyFields' => $expectedKeyFields, 'fields' => function () { return [ 'id' => ['type' => Type::int()], @@ -50,7 +50,7 @@ public function testCreatingEntityTypeWithCallable(): void }, ]); - $this->assertEqualsCanonicalizing($userType->getKeyFields(), $userTypeKeyFields); + $this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields()); $this->assertMatchesSnapshot($userType->config); } @@ -85,18 +85,18 @@ public function testResolvingEntityReference(): void public function testCreatingEntityRefType(): void { - $userTypeKeyFields = ['id', 'email']; + $expectedKeyFields = ['id', 'email']; $userType = new EntityRefObjectType([ 'name' => 'User', - 'keyFields' => $userTypeKeyFields, + 'keyFields' => $expectedKeyFields, 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], ], ]); - $this->assertEqualsCanonicalizing($userType->getKeyFields(), $userTypeKeyFields); + $this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields()); $this->assertMatchesSnapshot($userType->config); } } From 10a2a4ae410385431f5cc5042c96f528745e3b3d Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 12:58:53 +0300 Subject: [PATCH 21/40] Fix code style --- src/Types/EntityObjectType.php | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index 227e6bb..db3a72b 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -50,14 +50,16 @@ class EntityObjectType extends ObjectType public const FIELD_DIRECTIVE_PROVIDES = 'provides'; public const FIELD_DIRECTIVE_REQUIRES = 'requires'; - /** @var array */ - private $keyFields; - /** @var callable|null */ public $referenceResolver = null; /** - * @param mixed[] $config + * @var array|array> + */ + private array $keyFields; + + /** + * @param array $config */ public function __construct(array $config) { @@ -74,7 +76,7 @@ public function __construct(array $config) /** * Gets the fields that serve as the unique key or identifier of the entity. * - * @return array + * @return array|array> */ public function getKeyFields(): array { @@ -106,30 +108,23 @@ public function resolveReference($ref, $context = null, $info = null) return ($this->referenceResolver)($ref, $context, $info); } - /** - * @return void - */ - private function validateReferenceResolver() + private function validateReferenceResolver(): void { Utils::invariant(isset($this->referenceResolver), 'No reference resolver was set in the configuration.'); } /** * @param array{ __typename: mixed } $ref - * - * @return void */ - private function validateReferenceKeys($ref) + private function validateReferenceKeys(array $ref): void { Utils::invariant(isset($ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); } /** * @param array{ __resolveReference: mixed } $config - * - * @return void */ - public static function validateResolveReference(array $config) + public static function validateResolveReference(array $config): void { Utils::invariant(\is_callable($config[self::FIELD_REFERENCE_RESOLVER]), 'Reference resolver has to be callable.'); } From b5d5e481f0445bdc0c05c6b1070f7a5ff189354d Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 3 Aug 2022 09:12:40 +0300 Subject: [PATCH 22/40] Sort schema printer methods --- src/Utils/FederatedSchemaPrinter.php | 86 ++++++++++++++-------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index 035864d..ee4960d 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -100,28 +100,6 @@ public static function printType(Type $type, array $options = []): string return parent::printType($type, $options); } - /** - * @param array $options - */ - protected static function printObject(ObjectType $type, array $options): string - { - if (empty($type->getFields())) { - return ''; - } - - $implementedInterfaces = static::printImplementedInterfaces($type); - $extends = FederatedSchema::isReservedRootType($type->name) ? 'extend ' : ''; - - return static::printDescription($options, $type) . - sprintf( - "%stype %s%s {\n%s\n}", - $extends, - $type->name, - $implementedInterfaces, - static::printFields($options, $type) - ); - } - /** * @param array $options */ @@ -142,6 +120,27 @@ protected static function printEntityObject(EntityObjectType $type, array $optio ); } + protected static function printFieldFederatedDirectives(FieldDefinition $field): string + { + $directives = []; + + if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL]) + && true === $field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL] + ) { + $directives[] = '@external'; + } + + if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])) { + $directives[] = sprintf('@provides(fields: "%s")', static::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])); + } + + if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])) { + $directives[] = sprintf('@requires(fields: "%s")', static::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])); + } + + return implode(' ', $directives); + } + /** * @param array $options * @param EntityObjectType|InterfaceType|ObjectType|TypeWithFields $type @@ -178,27 +177,6 @@ static function (FieldDefinition $f, $i) use ($options) { ); } - protected static function printFieldFederatedDirectives(FieldDefinition $field): string - { - $directives = []; - - if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL]) - && true === $field->config[EntityObjectType::FIELD_DIRECTIVE_IS_EXTERNAL] - ) { - $directives[] = '@external'; - } - - if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])) { - $directives[] = sprintf('@provides(fields: "%s")', static::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_PROVIDES])); - } - - if (isset($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])) { - $directives[] = sprintf('@requires(fields: "%s")', static::printKeyFields($field->config[EntityObjectType::FIELD_DIRECTIVE_REQUIRES])); - } - - return implode(' ', $directives); - } - protected static function printImplementedInterfaces(ObjectType $type): string { $interfaces = $type->getInterfaces(); @@ -240,4 +218,26 @@ protected static function printKeyFields($keyFields): string return implode(' ', $parts); } + + /** + * @param array $options + */ + protected static function printObject(ObjectType $type, array $options): string + { + if (empty($type->getFields())) { + return ''; + } + + $implementedInterfaces = static::printImplementedInterfaces($type); + $extends = FederatedSchema::isReservedRootType($type->name) ? 'extend ' : ''; + + return static::printDescription($options, $type) . + sprintf( + "%stype %s%s {\n%s\n}", + $extends, + $type->name, + $implementedInterfaces, + static::printFields($options, $type) + ); + } } From 17bf125b15c072ee50ee48923990690a4b57b97d Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 10:52:06 +0300 Subject: [PATCH 23/40] Add directives of Apollo Federation v2 --- src/Directives.php | 55 +++++++++++++++++-- src/Directives/ExternalDirective.php | 2 + src/Directives/InaccessibleDirective.php | 31 +++++++++++ src/Directives/OverrideDirective.php | 34 ++++++++++++ src/Directives/ProvidesDirective.php | 2 + src/Directives/RequiresDirective.php | 2 + src/Directives/ShareableDirective.php | 26 +++++++++ src/Enum/DirectiveEnum.php | 16 +++++- test/DirectivesTest.php | 25 +++++++++ ...sTest__testItAddsDirectivesToSchema__1.txt | 8 ++- .../SchemaTest__testSchemaSdl__1.txt | 8 ++- 11 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 src/Directives/InaccessibleDirective.php create mode 100644 src/Directives/OverrideDirective.php create mode 100644 src/Directives/ShareableDirective.php diff --git a/src/Directives.php b/src/Directives.php index ec21a05..29d8de2 100644 --- a/src/Directives.php +++ b/src/Directives.php @@ -5,19 +5,31 @@ namespace Apollo\Federation; use Apollo\Federation\Directives\ExternalDirective; +use Apollo\Federation\Directives\InaccessibleDirective; use Apollo\Federation\Directives\KeyDirective; +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; -use GraphQL\Type\Definition\Directive; /** * Helper class to get directives for annotating federated entity types. */ class Directives { - /** @var array */ - private static $directives = null; + /** + * @var array{ + * external: ExternalDirective, + * inaccessible: InaccessibleDirective, + * key: KeyDirective, + * override: OverrideDirective, + * requires: RequiresDirective, + * provides: ProvidesDirective, + * shareable: ShareableDirective, + * }|null + */ + private static ?array $directives = null; /** * Gets the @key directive. @@ -35,6 +47,22 @@ public static function external(): ExternalDirective return self::getDirectives()[DirectiveEnum::EXTERNAL]; } + /** + * Gets the @inaccessible directive. + */ + public static function inaccessible(): InaccessibleDirective + { + return self::getDirectives()[DirectiveEnum::INACCESSIBLE]; + } + + /** + * Gets the @override directive. + */ + public static function override(): OverrideDirective + { + return self::getDirectives()[DirectiveEnum::OVERRIDE]; + } + /** * Gets the @requires directive. */ @@ -51,19 +79,38 @@ public static function provides(): ProvidesDirective 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. * - * @return array + * @return array{ + * external: ExternalDirective, + * inaccessible: InaccessibleDirective, + * key: KeyDirective, + * override: OverrideDirective, + * requires: RequiresDirective, + * provides: ProvidesDirective, + * shareable: ShareableDirective, + * } */ public static function getDirectives(): array { if (!self::$directives) { self::$directives = [ DirectiveEnum::EXTERNAL => new ExternalDirective(), + DirectiveEnum::INACCESSIBLE => new InaccessibleDirective(), DirectiveEnum::KEY => new KeyDirective(), + DirectiveEnum::OVERRIDE => new OverrideDirective(), DirectiveEnum::REQUIRES => new RequiresDirective(), DirectiveEnum::PROVIDES => new ProvidesDirective(), + DirectiveEnum::SHAREABLE => new ShareableDirective(), ]; } diff --git a/src/Directives/ExternalDirective.php b/src/Directives/ExternalDirective.php index 3c8d024..4b15f6a 100644 --- a/src/Directives/ExternalDirective.php +++ b/src/Directives/ExternalDirective.php @@ -10,6 +10,8 @@ * 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 { diff --git a/src/Directives/InaccessibleDirective.php b/src/Directives/InaccessibleDirective.php new file mode 100644 index 0000000..aa43dbc --- /dev/null +++ b/src/Directives/InaccessibleDirective.php @@ -0,0 +1,31 @@ + DirectiveEnum::INACCESSIBLE, + 'locations' => [ + DirectiveLocation::FIELD_DEFINITION, + DirectiveLocation::IFACE, + DirectiveLocation::OBJECT, + DirectiveLocation::UNION, + ], + ]); + } +} diff --git a/src/Directives/OverrideDirective.php b/src/Directives/OverrideDirective.php new file mode 100644 index 0000000..61e62e3 --- /dev/null +++ b/src/Directives/OverrideDirective.php @@ -0,0 +1,34 @@ + DirectiveEnum::OVERRIDE, + 'locations' => [DirectiveLocation::FIELD_DEFINITION], + 'args' => [ + new FieldArgument([ + 'name' => 'from', + 'type' => Type::nonNull(Type::string()), + ]), + ], + ]); + } +} diff --git a/src/Directives/ProvidesDirective.php b/src/Directives/ProvidesDirective.php index 24d9be1..63d70c3 100644 --- a/src/Directives/ProvidesDirective.php +++ b/src/Directives/ProvidesDirective.php @@ -11,6 +11,8 @@ /** * The `@provides` directive is used to annotate the expected returned fieldset from a field * on a base type that is guaranteed to be selectable by the gateway. + * + * @see https://www.apollographql.com/docs/federation/federated-types/federated-directives/#provides */ class ProvidesDirective extends Directive { diff --git a/src/Directives/RequiresDirective.php b/src/Directives/RequiresDirective.php index d33bec3..42853ad 100644 --- a/src/Directives/RequiresDirective.php +++ b/src/Directives/RequiresDirective.php @@ -12,6 +12,8 @@ * The `@requires` directive is used to annotate the required input fieldset from a base type * for a resolver. It is used to develop a query plan where the required fields may not be * needed by the client, but the service may need additional information from other services. + * + * @see https://www.apollographql.com/docs/federation/federated-types/federated-directives/#requires */ class RequiresDirective extends Directive { diff --git a/src/Directives/ShareableDirective.php b/src/Directives/ShareableDirective.php new file mode 100644 index 0000000..fd0d524 --- /dev/null +++ b/src/Directives/ShareableDirective.php @@ -0,0 +1,26 @@ + DirectiveEnum::SHAREABLE, + 'locations' => [DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::OBJECT], + ]); + } +} diff --git a/src/Enum/DirectiveEnum.php b/src/Enum/DirectiveEnum.php index 97ef22b..a0c039b 100644 --- a/src/Enum/DirectiveEnum.php +++ b/src/Enum/DirectiveEnum.php @@ -7,19 +7,31 @@ class DirectiveEnum { public const EXTERNAL = 'external'; + public const INACCESSIBLE = 'inaccessible'; public const KEY = 'key'; + public const OVERRIDE = 'override'; public const PROVIDES = 'provides'; public const REQUIRES = 'requires'; + public const SHAREABLE = 'shareable'; + + /** + * @var string[]|null + */ + protected static ?array $constants = null; /** * @return string[] */ public static function getAll(): array { - return [self::EXTERNAL, self::KEY, self::PROVIDES, self::REQUIRES]; + if (null === static::$constants) { + static::$constants = (new \ReflectionClass(static::class))->getConstants(); + } + + return static::$constants; } - private function __construct() + protected function __construct() { // forbid creation of an object } diff --git a/test/DirectivesTest.php b/test/DirectivesTest.php index a71aa04..6b3137a 100644 --- a/test/DirectivesTest.php +++ b/test/DirectivesTest.php @@ -37,6 +37,21 @@ public function testExternalDirective(): void $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } + public function testInaccessibleDirective(): void + { + $config = Directives::inaccessible()->config; + + $expectedLocations = [ + DirectiveLocation::FIELD_DEFINITION, + DirectiveLocation::IFACE, + DirectiveLocation::OBJECT, + DirectiveLocation::UNION, + ]; + + $this->assertEquals('inaccessible', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); + } + public function testRequiresDirective(): void { $config = Directives::requires()->config; @@ -57,6 +72,16 @@ public function testProvidesDirective(): void $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } + public function testShareableDirective(): void + { + $config = Directives::shareable()->config; + + $expectedLocations = [DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::OBJECT]; + + $this->assertEquals('shareable', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); + } + public function testItAddsDirectivesToSchema(): void { $schema = new Schema([ diff --git a/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt b/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt index fe9e678..b682900 100644 --- a/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt +++ b/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt @@ -1,11 +1,17 @@ +directive @external on FIELD_DEFINITION + +directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + directive @key(fields: String!) on OBJECT | INTERFACE -directive @external on FIELD_DEFINITION +directive @override(from: String!) on FIELD_DEFINITION directive @requires(fields: String!) on FIELD_DEFINITION directive @provides(fields: String!) on FIELD_DEFINITION +directive @shareable on FIELD_DEFINITION | OBJECT + type Query { _: String } diff --git a/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt b/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt index fd4a936..a3767ed 100644 --- a/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt +++ b/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt @@ -1,11 +1,17 @@ +directive @external on FIELD_DEFINITION + +directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + directive @key(fields: String!) on OBJECT | INTERFACE -directive @external on FIELD_DEFINITION +directive @override(from: String!) on FIELD_DEFINITION directive @requires(fields: String!) on FIELD_DEFINITION directive @provides(fields: String!) on FIELD_DEFINITION +directive @shareable on FIELD_DEFINITION | OBJECT + """A character in the Star Wars Trilogy""" type Character { id: Int! From c0f39411de8aa66d87ae1cc54923f9c460da783c Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 14:42:36 +0300 Subject: [PATCH 24/40] Implement directive @link --- src/Directives.php | 12 +++ src/Directives/LinkDirective.php | 78 +++++++++++++++ src/Enum/DirectiveEnum.php | 1 + src/FederatedSchema.php | 1 + src/FederatedSchemaTrait.php | 94 ++++++++++++++----- src/Types/SchemaExtensionType.php | 23 +++++ src/Utils/FederatedSchemaPrinter.php | 61 +++++++++++- test/DirectivesTest.php | 13 ++- ...sTest__testItAddsDirectivesToSchema__1.txt | 4 + .../SchemaTest__testSchemaSdl__1.txt | 4 + .../SchemaTest__testServiceSdl__1.yml | 2 +- 11 files changed, 264 insertions(+), 29 deletions(-) create mode 100644 src/Directives/LinkDirective.php create mode 100644 src/Types/SchemaExtensionType.php diff --git a/src/Directives.php b/src/Directives.php index 29d8de2..08a1b19 100644 --- a/src/Directives.php +++ b/src/Directives.php @@ -7,6 +7,7 @@ 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; @@ -23,6 +24,7 @@ class Directives * external: ExternalDirective, * inaccessible: InaccessibleDirective, * key: KeyDirective, + * link: LinkDirective, * override: OverrideDirective, * requires: RequiresDirective, * provides: ProvidesDirective, @@ -55,6 +57,14 @@ 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. */ @@ -94,6 +104,7 @@ public static function shareable(): ShareableDirective * external: ExternalDirective, * inaccessible: InaccessibleDirective, * key: KeyDirective, + * link: LinkDirective, * override: OverrideDirective, * requires: RequiresDirective, * provides: ProvidesDirective, @@ -107,6 +118,7 @@ public static function getDirectives(): array 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(), diff --git a/src/Directives/LinkDirective.php b/src/Directives/LinkDirective.php new file mode 100644 index 0000000..87f3c27 --- /dev/null +++ b/src/Directives/LinkDirective.php @@ -0,0 +1,78 @@ + 'link_Import', + 'serialize' => static fn ($value) => json_encode($value, \JSON_THROW_ON_ERROR), + 'parseValue' => static function ($value) { + if (\is_string($value)) { + return $value; + } + + if (!\is_array($value)) { + throw new InvariantViolation('"link_Import" must be a string or an array'); + } + $permittedKeys = ['name', 'as']; + $keys = array_keys($value); + if ($permittedKeys !== array_intersect($permittedKeys, $keys) || array_diff($keys, $permittedKeys)) { + throw new InvariantViolation('"link_Import" must contain only keys "name" and "as" and they are required'); + } + + if (2 !== \count(array_filter($value))) { + throw new InvariantViolation('The "name" and "as" part of "link_Import" be not empty'); + } + + $prefixes = array_unique([$value['name'][0], $value['as'][0]]); + if (\in_array('@', $prefixes, true) && 2 !== \count($prefixes)) { + // https://specs.apollo.dev/link/v1.0/#Import + throw new InvariantViolation('The "name" and "as" part of "link_Import" be of the same type'); + } + + return $value; + }, + ]); + + parent::__construct([ + 'name' => DirectiveEnum::LINK, + 'locations' => [DirectiveLocation::SCHEMA], + 'args' => [ + new FieldArgument([ + 'name' => 'url', + 'type' => Type::nonNull(Type::string()), + ]), + new FieldArgument([ + 'name' => 'as', + 'type' => Type::string(), + ]), + new FieldArgument([ + 'name' => 'for', + // TODO use union type (enum & string) and declare required enum + 'type' => Type::string(), + ]), + new FieldArgument([ + 'name' => 'import', + 'type' => Type::listOf(Type::nonNull($linkImport)), + ]), + ], + 'isRepeatable' => true, + ]); + } +} diff --git a/src/Enum/DirectiveEnum.php b/src/Enum/DirectiveEnum.php index a0c039b..06abf9a 100644 --- a/src/Enum/DirectiveEnum.php +++ b/src/Enum/DirectiveEnum.php @@ -9,6 +9,7 @@ class DirectiveEnum public const EXTERNAL = 'external'; public const INACCESSIBLE = 'inaccessible'; public const KEY = 'key'; + public const LINK = 'link'; public const OVERRIDE = 'override'; public const PROVIDES = 'provides'; public const REQUIRES = 'requires'; diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index f3b3fd4..8ce2c7c 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -82,6 +82,7 @@ public function __construct(array $config) { $this->entityTypes = $this->extractEntityTypes($config); $this->entityDirectives = Directives::getDirectives(); + $this->schemaExtensionTypes = $this->extractSchemaExtensionTypes($config); $config = array_merge($config, $this->getEntityDirectivesConfig($config), $this->getQueryTypeConfig($config)); diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php index 5afbffe..6942362 100644 --- a/src/FederatedSchemaTrait.php +++ b/src/FederatedSchemaTrait.php @@ -5,6 +5,7 @@ namespace Apollo\Federation; use Apollo\Federation\Types\EntityObjectType; +use Apollo\Federation\Types\SchemaExtensionType; use Apollo\Federation\Utils\FederatedSchemaPrinter; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; @@ -22,6 +23,11 @@ trait FederatedSchemaTrait */ protected array $entityTypes = []; + /** + * @var SchemaExtensionType[] + */ + protected array $schemaExtensionTypes = []; + /** * @var Directive[] */ @@ -42,7 +48,15 @@ public function getEntityTypes(): array */ public function hasEntityTypes(): bool { - return !empty($this->getEntityTypes()); + return !empty($this->entityTypes); + } + + /** + * @return SchemaExtensionType[] + */ + public function getSchemaExtensionTypes(): array + { + return $this->schemaExtensionTypes; } /** @@ -59,22 +73,25 @@ protected function getEntityDirectivesConfig(array $config): array } /** - * @param array $config + * @param array{ query: ObjectType } $config * * @return array{ query: ObjectType } */ protected function getQueryTypeConfig(array $config): array { $queryTypeConfig = $config['query']->config; - if (\is_callable($queryTypeConfig['fields'])) { - $queryTypeConfig['fields'] = $queryTypeConfig['fields'](); - } + $fields = $queryTypeConfig['fields']; + $queryTypeConfig['fields'] = function () use ($config, $fields) { + if (\is_callable($fields)) { + $fields = $fields(); + } - $queryTypeConfig['fields'] = array_merge( - $queryTypeConfig['fields'], - $this->getQueryTypeServiceFieldConfig(), - $this->getQueryTypeEntitiesFieldConfig($config) - ); + return array_merge( + $fields, + $this->getQueryTypeServiceFieldConfig(), + $this->getQueryTypeEntitiesFieldConfig($config) + ); + }; return [ 'query' => new ObjectType($queryTypeConfig), @@ -87,9 +104,9 @@ protected function getQueryTypeConfig(array $config): array protected function getQueryTypeServiceFieldConfig(): array { $serviceType = new ObjectType([ - 'name' => self::RESERVED_TYPE_SERVICE, + 'name' => FederatedSchema::RESERVED_TYPE_SERVICE, 'fields' => [ - self::RESERVED_FIELD_SDL => [ + FederatedSchema::RESERVED_FIELD_SDL => [ 'type' => Type::string(), 'resolve' => fn (): string => FederatedSchemaPrinter::doPrint($this), ], @@ -97,7 +114,7 @@ protected function getQueryTypeServiceFieldConfig(): array ]); return [ - self::RESERVED_FIELD_SERVICE => [ + FederatedSchema::RESERVED_FIELD_SERVICE => [ 'type' => Type::nonNull($serviceType), 'resolve' => static fn (): array => [], ], @@ -111,25 +128,25 @@ protected function getQueryTypeServiceFieldConfig(): array */ protected function getQueryTypeEntitiesFieldConfig(?array $config): array { - if (!$this->hasEntityTypes()) { + if (!$this->entityTypes) { return []; } $entityType = new UnionType([ - 'name' => self::RESERVED_TYPE_ENTITY, + 'name' => FederatedSchema::RESERVED_TYPE_ENTITY, 'types' => array_values($this->getEntityTypes()), ]); $anyType = new CustomScalarType([ - 'name' => self::RESERVED_TYPE_ANY, + 'name' => FederatedSchema::RESERVED_TYPE_ANY, 'serialize' => static fn ($value) => $value, ]); return [ - self::RESERVED_FIELD_ENTITIES => [ + FederatedSchema::RESERVED_FIELD_ENTITIES => [ 'type' => Type::listOf($entityType), 'args' => [ - self::RESERVED_FIELD_REPRESENTATIONS => [ + FederatedSchema::RESERVED_FIELD_REPRESENTATIONS => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))), ], ], @@ -147,25 +164,24 @@ protected function getQueryTypeEntitiesFieldConfig(?array $config): array protected function resolve($root, $args, $context, $info): array { return array_map(static function ($ref) use ($context, $info) { - Utils::invariant(isset($ref[self::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); + Utils::invariant(isset($ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); - $typeName = $ref[self::RESERVED_FIELD_TYPE_NAME]; + $typeName = $ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]; $type = $info->schema->getType($typeName); Utils::invariant( - $type && $type instanceof EntityObjectType, - sprintf( - 'The _entities resolver tried to load an entity for type "%s", but no object type of that name was found in the schema', - $type->name - ) + $type instanceof EntityObjectType, + 'The _entities resolver tried to load an entity for type "%s", but no object type of that name was found in the schema', + $type->name ); + /** @var EntityObjectType $type */ if (!$type->hasReferenceResolver()) { return $ref; } return $type->resolveReference($ref, $context, $info); - }, $args[self::RESERVED_FIELD_REPRESENTATIONS]); + }, $args[FederatedSchema::RESERVED_FIELD_REPRESENTATIONS]); } /** @@ -186,4 +202,30 @@ protected function extractEntityTypes(array $config): array return $entityTypes; } + + /** + * @param array $config + * + * @return SchemaExtensionType[] + */ + protected function extractSchemaExtensionTypes(array $config): array + { + $typeMap = []; + $configTypes = $config['types'] ?? []; + if (\is_array($configTypes)) { + $typeMap = $configTypes; + } elseif (\is_callable($configTypes)) { + $typeMap = $configTypes(); + } + + $types = []; + + foreach ($typeMap as $type) { + if ($type instanceof SchemaExtensionType) { + $types[$type->name] = $type; + } + } + + return $types; + } } diff --git a/src/Types/SchemaExtensionType.php b/src/Types/SchemaExtensionType.php new file mode 100644 index 0000000..b19843a --- /dev/null +++ b/src/Types/SchemaExtensionType.php @@ -0,0 +1,23 @@ + $config + */ + public function __construct(array $config) + { + $this->config = $config; + } +} diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index ee4960d..a90d94a 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -35,6 +35,7 @@ use Apollo\Federation\FederatedSchema; use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; +use Apollo\Federation\Types\SchemaExtensionType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InterfaceType; @@ -93,7 +94,8 @@ public static function printType(Type $type, array $options = []): string if (($type instanceof ScalarType && FederatedSchema::RESERVED_TYPE_ANY === $type->name) || ($type instanceof ObjectType && FederatedSchema::RESERVED_TYPE_SERVICE === $type->name) - || ($type instanceof UnionType && FederatedSchema::RESERVED_TYPE_ENTITY === $type->name)) { + || ($type instanceof UnionType && FederatedSchema::RESERVED_TYPE_ENTITY === $type->name) + || ($type instanceof SchemaExtensionType)) { return ''; } @@ -219,6 +221,40 @@ protected static function printKeyFields($keyFields): string return implode(' ', $parts); } + /** + * @param array $linkConfig + */ + protected static function printLinkDirectiveConfig(array $linkConfig): string + { + $arguments = []; + foreach ($linkConfig as $name => $value) { + if (null === $value) { + continue; + } + $arguments[] = sprintf('%s: %s', $name, static::printLinkDirectiveArgumentValue($value, $name)); + } + + return sprintf('@link(%s)', implode(', ', $arguments)); + } + + /** + * @param string|array> $argument + */ + protected static function printLinkDirectiveArgumentValue($argument, string $name): string + { + if (\is_string($argument)) { + return '"' . $argument . '"'; + } + if ('import' !== $name) { + throw new \InvalidArgumentException(sprintf('Value of %s must be a string', $name)); + } + if (!\is_array($argument)) { + throw new \InvalidArgumentException('Invalid type of "import" argument value'); + } + + return json_encode($argument, \JSON_THROW_ON_ERROR); + } + /** * @param array $options */ @@ -240,4 +276,27 @@ protected static function printObject(ObjectType $type, array $options): string static::printFields($options, $type) ); } + + /** + * @param FederatedSchema $schema + */ + protected static function printSchemaDefinition(Schema $schema): string + { + $parts = [parent::printSchemaDefinition($schema)]; + foreach ($schema->getSchemaExtensionTypes() as $schemaExtensionType) { + $parts[] = self::printSchemaExtensionType($schemaExtensionType); + } + + return implode("\n\n", array_filter($parts)); + } + + protected static function printSchemaExtensionType(SchemaExtensionType $schemaExtensionType): string + { + $links = $schemaExtensionType->config[SchemaExtensionType::FIELD_KEY_LINKS] ?? []; + + return sprintf( + 'extend schema %s\n', + implode("\n", array_map(static fn (array $x): string => static::printLinkDirectiveConfig($x), $links)), + ); + } } diff --git a/test/DirectivesTest.php b/test/DirectivesTest.php index 6b3137a..fe51d53 100644 --- a/test/DirectivesTest.php +++ b/test/DirectivesTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Spatie\Snapshots\MatchesSnapshots; -class DirectivesTest extends TestCase +final class DirectivesTest extends TestCase { use MatchesSnapshots; @@ -52,6 +52,17 @@ public function testInaccessibleDirective(): void $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); } + public function testLinkDirective(): void + { + $config = Directives::link()->config; + + $expectedLocations = [DirectiveLocation::SCHEMA]; + + $this->assertEquals('link', $config['name']); + $this->assertEqualsCanonicalizing($expectedLocations, $config['locations']); + $this->assertTrue($config['isRepeatable']); + } + public function testRequiresDirective(): void { $config = Directives::requires()->config; diff --git a/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt b/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt index b682900..6f55ba2 100644 --- a/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt +++ b/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt @@ -4,6 +4,8 @@ directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION directive @key(fields: String!) on OBJECT | INTERFACE +directive @link(url: String!, as: String, for: String, import: [link_Import!]) repeatable on SCHEMA + directive @override(from: String!) on FIELD_DEFINITION directive @requires(fields: String!) on FIELD_DEFINITION @@ -15,3 +17,5 @@ directive @shareable on FIELD_DEFINITION | OBJECT type Query { _: String } + +scalar link_Import diff --git a/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt b/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt index a3767ed..7718755 100644 --- a/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt +++ b/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt @@ -4,6 +4,8 @@ directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION directive @key(fields: String!) on OBJECT | INTERFACE +directive @link(url: String!, as: String, for: String, import: [link_Import!]) repeatable on SCHEMA + directive @override(from: String!) on FIELD_DEFINITION directive @requires(fields: String!) on FIELD_DEFINITION @@ -46,3 +48,5 @@ union _Entity = Episode | Character | Location type _Service { sdl: String } + +scalar link_Import diff --git a/test/__snapshots__/SchemaTest__testServiceSdl__1.yml b/test/__snapshots__/SchemaTest__testServiceSdl__1.yml index 2cf44a2..799ec1c 100644 --- a/test/__snapshots__/SchemaTest__testServiceSdl__1.yml +++ b/test/__snapshots__/SchemaTest__testServiceSdl__1.yml @@ -1,2 +1,2 @@ data: - _service: { sdl: "\"\"\"A character in the Star Wars Trilogy\"\"\"\nextend type Character @key(fields: \"id\") {\n id: Int! @external\n name: String! @external\n locations: [Location]! @requires(fields: \"name\")\n}\n\n\"\"\"A film in the Star Wars Trilogy\"\"\"\ntype Episode @key(fields: \"id\") {\n id: Int! \n title: String! \n characters: [Character!]! @provides(fields: \"name\")\n}\n\n\"\"\"A location in the Star Wars Trilogy\"\"\"\nextend type Location @key(fields: \"id\") {\n id: Int! @external\n name: String! @external\n}\n\nextend type Query {\n episodes: [Episode!]! \n deprecatedEpisodes: [Episode!]! @deprecated(reason: \"Because you should use the other one.\") \n}\n" } + _service: { sdl: "\"\"\"A character in the Star Wars Trilogy\"\"\"\nextend type Character @key(fields: \"id\") {\n id: Int! @external\n name: String! @external\n locations: [Location]! @requires(fields: \"name\")\n}\n\n\"\"\"A film in the Star Wars Trilogy\"\"\"\ntype Episode @key(fields: \"id\") {\n id: Int! \n title: String! \n characters: [Character!]! @provides(fields: \"name\")\n}\n\n\"\"\"A location in the Star Wars Trilogy\"\"\"\nextend type Location @key(fields: \"id\") {\n id: Int! @external\n name: String! @external\n}\n\nextend type Query {\n episodes: [Episode!]! \n deprecatedEpisodes: [Episode!]! @deprecated(reason: \"Because you should use the other one.\") \n}\n\nscalar link_Import\n" } From 1bac625d985ecaf8b4857c94fd148c5864c98256 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 15:21:03 +0300 Subject: [PATCH 25/40] Add handling of argument "resolvable" of directive @key --- README.md | 12 ++---- src/Directives/KeyDirective.php | 11 +++++- src/FederatedSchema.php | 2 +- src/Types/EntityObjectType.php | 39 +++++++++++++++---- src/Utils/FederatedSchemaPrinter.php | 9 ++++- test/EntitiesTest.php | 20 +++++----- test/StarWarsSchema.php | 6 +-- ...sTest__testItAddsDirectivesToSchema__1.txt | 2 +- ...tiesTest__testCreatingEntityRefType__1.yml | 5 +-- ..._testCreatingEntityTypeWithCallable__1.yml | 6 +-- ...ntitiesTest__testCreatingEntityType__1.yml | 6 +-- .../SchemaTest__testSchemaSdl__1.txt | 2 +- 12 files changed, 77 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 589c85c..f03f5db 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ 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()], @@ -35,7 +35,7 @@ $userType = new EntityObjectType([ ]); ``` -* `keyFields` — defines the entity's primary key, which consists of one or more of the fields. An entity's key cannot include fields that return a union or interface. +* `keys` — defines the entity's primary key, 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. @@ -50,7 +50,7 @@ use Apollo\Federation\Types\EntityRefObjectType; $userType = new EntityRefObjectType([ 'name' => 'User', - 'keyFields' => ['id', 'email'], + 'keys' => [['fields' => 'id'], ['fields' => 'email']], 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()] @@ -69,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 - ] ] ]); ``` diff --git a/src/Directives/KeyDirective.php b/src/Directives/KeyDirective.php index 4129547..c74f814 100644 --- a/src/Directives/KeyDirective.php +++ b/src/Directives/KeyDirective.php @@ -11,9 +11,14 @@ /** * 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([ @@ -21,9 +26,13 @@ public function __construct() 'locations' => [DirectiveLocation::OBJECT, DirectiveLocation::IFACE], 'args' => [ new FieldArgument([ - 'name' => 'fields', + 'name' => self::ARGUMENT_FIELDS, 'type' => Type::nonNull(Type::string()), ]), + new FieldArgument([ + 'name' => self::ARGUMENT_RESOLVABLE, + 'type' => Type::boolean(), + ]), ], ]); } diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index 8ce2c7c..bbb6ec1 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -36,7 +36,7 @@ * 'firstName' => [...], * 'lastName' => [...], * ], - * 'keyFields' => ['id', 'email'] + * 'keys' => [['fields' => 'id'], ['fields' => 'email']] * ]); * * $queryType = new GraphQL\Type\Definition\ObjectType([ diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index db3a72b..5d694ec 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -22,7 +22,7 @@ * * $userType = new Apollo\Federation\Types\EntityObjectType([ * 'name' => 'User', - * 'keyFields' => ['id', 'email'], + * 'keys' => [['fields' => 'id'], ['fields' => 'email']], * 'fields' => [...] * ]); * @@ -31,7 +31,7 @@ * * $userType = new Apollo\Federation\Types\EntityObjectType([ * 'name' => 'User', - * 'keyFields' => ['id', 'email'], + * 'keys' => [['fields' => 'id', 'resolvable': false ]], * 'fields' => [ * 'id' => [ * 'type' => Types::int(), @@ -43,7 +43,7 @@ */ class EntityObjectType extends ObjectType { - public const FIELD_KEY_FIELDS = 'keyFields'; + public const FIELD_KEYS = 'keys'; public const FIELD_REFERENCE_RESOLVER = '__resolveReference'; public const FIELD_DIRECTIVE_IS_EXTERNAL = 'isExternal'; @@ -54,16 +54,22 @@ class EntityObjectType extends ObjectType public $referenceResolver = null; /** - * @var array|array> + * @var array|array>, resolvable: bool }> */ - private array $keyFields; + private array $keys; /** * @param array $config */ public function __construct(array $config) { - $this->keyFields = $config[self::FIELD_KEY_FIELDS]; + Utils::invariant( + !(\array_key_exists(self::FIELD_KEYS, $config) && \array_key_exists('keyFields', $config)), + 'Use only one way to define directives @key.' + ); + + $this->keys = $config[self::FIELD_KEYS] + ?? array_map(static fn ($x): array => ['fields' => $x], $config['keyFields']); if (isset($config[self::FIELD_REFERENCE_RESOLVER])) { self::validateResolveReference($config); @@ -76,11 +82,30 @@ public function __construct(array $config) /** * Gets the fields that serve as the unique key or identifier of the entity. * + * @deprecated Use {@see getKeys()} + * * @return array|array> */ public function getKeyFields(): array { - return $this->keyFields; + @trigger_error( + 'Since skillshare/apollo-federation-php 2.0.0: ' + . 'Method \Apollo\Federation\Types\EntityObjectType::getKeyFields() is deprecated. ' + . 'Use \Apollo\Federation\Types\EntityObjectType::getKeys() instead of it.', + \E_USER_DEPRECATED + ); + + return array_map(static fn(array $x) => $x['fields'], $this->keys); + } + + /** + * Gets the fields that serve as the unique key or identifier of the entity. + * + * @return array|array>, resolvable: bool }> + */ + public function getKeys(): array + { + return $this->keys; } /** diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index a90d94a..d541e20 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -31,6 +31,7 @@ namespace Apollo\Federation\Utils; +use Apollo\Federation\Directives\KeyDirective; use Apollo\Federation\Enum\DirectiveEnum; use Apollo\Federation\FederatedSchema; use Apollo\Federation\Types\EntityObjectType; @@ -192,8 +193,12 @@ protected static function printKeyDirective(EntityObjectType $type): string { $keyDirective = ''; - foreach ($type->getKeyFields() as $keyField) { - $keyDirective .= sprintf(' @key(fields: "%s")', static::printKeyFields($keyField)); + foreach ($type->getKeys() as $keyField) { + $arguments = [sprintf('%s: "%s"', KeyDirective::ARGUMENT_FIELDS, static::printKeyFields($keyField['fields']))]; + if (\array_key_exists('resolvable', $keyField)) { + $arguments[] = sprintf('%s: %s', KeyDirective::ARGUMENT_RESOLVABLE, $keyField['resolvable'] ? 'true' : 'false'); + } + $keyDirective .= sprintf(' @key(%s)', implode(', ', $arguments)); } return $keyDirective; diff --git a/test/EntitiesTest.php b/test/EntitiesTest.php index 9c03f73..f0b4c42 100644 --- a/test/EntitiesTest.php +++ b/test/EntitiesTest.php @@ -16,11 +16,11 @@ class EntitiesTest extends TestCase public function testCreatingEntityType(): void { - $expectedKeyFields = ['id', 'email']; + $expectedKeys = [['fields' => 'id'], ['fields' => 'email']]; $userType = new EntityObjectType([ 'name' => 'User', - 'keyFields' => $expectedKeyFields, + 'keys' => $expectedKeys, 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], @@ -29,17 +29,17 @@ public function testCreatingEntityType(): void ], ]); - $this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields()); + $this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys()); $this->assertMatchesSnapshot($userType->config); } public function testCreatingEntityTypeWithCallable(): void { - $expectedKeyFields = ['id', 'email']; + $expectedKeys = [['fields' => 'id'], ['fields' => 'email']]; $userType = new EntityObjectType([ 'name' => 'User', - 'keyFields' => $expectedKeyFields, + 'keys' => $expectedKeys, 'fields' => function () { return [ 'id' => ['type' => Type::int()], @@ -50,7 +50,7 @@ public function testCreatingEntityTypeWithCallable(): void }, ]); - $this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields()); + $this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys()); $this->assertMatchesSnapshot($userType->config); } @@ -66,7 +66,7 @@ public function testResolvingEntityReference(): void $userType = new EntityObjectType([ 'name' => 'User', - 'keyFields' => ['id', 'email'], + 'keys' => [['fields' => 'id'], ['fields' => 'email']], 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], @@ -85,18 +85,18 @@ public function testResolvingEntityReference(): void public function testCreatingEntityRefType(): void { - $expectedKeyFields = ['id', 'email']; + $expectedKeys = [['fields' => 'id', 'resolvable' => false]]; $userType = new EntityRefObjectType([ 'name' => 'User', - 'keyFields' => $expectedKeyFields, + 'keys' => $expectedKeys, 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()], ], ]); - $this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields()); + $this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys()); $this->assertMatchesSnapshot($userType->config); } } diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index 6786d4b..5673e89 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -83,7 +83,7 @@ private static function getEpisodeType(): EntityObjectType 'provides' => 'name', ], ], - EntityObjectType::FIELD_KEY_FIELDS => ['id'], + EntityObjectType::FIELD_KEYS => ['fields' => 'id'], EntityObjectType::FIELD_REFERENCE_RESOLVER => static function (array $ref): array { $entity = StarWarsData::getEpisodeById($ref['id']); $entity['__typename'] = 'Episode'; @@ -113,7 +113,7 @@ private static function getCharacterType(): EntityRefObjectType 'requires' => 'name', ], ], - EntityObjectType::FIELD_KEY_FIELDS => ['id'], + EntityObjectType::FIELD_KEYS => ['fields' => 'id'], ]); } @@ -132,7 +132,7 @@ private static function getLocationType(): EntityRefObjectType 'isExternal' => true, ], ], - EntityObjectType::FIELD_KEY_FIELDS => ['id'], + EntityObjectType::FIELD_KEYS => ['fields' => 'id'], ]); } } diff --git a/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt b/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt index 6f55ba2..6444549 100644 --- a/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt +++ b/test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt @@ -2,7 +2,7 @@ directive @external on FIELD_DEFINITION directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @key(fields: String!) on OBJECT | INTERFACE +directive @key(fields: String!, resolvable: Boolean) on OBJECT | INTERFACE directive @link(url: String!, as: String, for: String, import: [link_Import!]) repeatable on SCHEMA diff --git a/test/__snapshots__/EntitiesTest__testCreatingEntityRefType__1.yml b/test/__snapshots__/EntitiesTest__testCreatingEntityRefType__1.yml index 3bc1453..036ee5e 100644 --- a/test/__snapshots__/EntitiesTest__testCreatingEntityRefType__1.yml +++ b/test/__snapshots__/EntitiesTest__testCreatingEntityRefType__1.yml @@ -1,7 +1,6 @@ name: User -keyFields: - - id - - email +keys: + - { fields: id, resolvable: false } fields: id: { type: { name: Int, description: "The `Int` scalar type represents non-fractional signed whole numeric\nvalues. Int can represent values between -(2^31) and 2^31 - 1. ", astNode: null, extensionASTNodes: null, config: { } } } email: { type: { name: String, description: "The `String` scalar type represents textual data, represented as UTF-8\ncharacter sequences. The String type is most often used by GraphQL to\nrepresent free-form human-readable text.", astNode: null, extensionASTNodes: null, config: { } } } diff --git a/test/__snapshots__/EntitiesTest__testCreatingEntityTypeWithCallable__1.yml b/test/__snapshots__/EntitiesTest__testCreatingEntityTypeWithCallable__1.yml index 550a52f..525bd68 100644 --- a/test/__snapshots__/EntitiesTest__testCreatingEntityTypeWithCallable__1.yml +++ b/test/__snapshots__/EntitiesTest__testCreatingEntityTypeWithCallable__1.yml @@ -1,5 +1,5 @@ name: User -keyFields: - - id - - email +keys: + - { fields: id } + - { fields: email } fields: { } diff --git a/test/__snapshots__/EntitiesTest__testCreatingEntityType__1.yml b/test/__snapshots__/EntitiesTest__testCreatingEntityType__1.yml index e073da4..2b18be2 100644 --- a/test/__snapshots__/EntitiesTest__testCreatingEntityType__1.yml +++ b/test/__snapshots__/EntitiesTest__testCreatingEntityType__1.yml @@ -1,7 +1,7 @@ name: User -keyFields: - - id - - email +keys: + - { fields: id } + - { fields: email } fields: id: { type: { name: Int, description: "The `Int` scalar type represents non-fractional signed whole numeric\nvalues. Int can represent values between -(2^31) and 2^31 - 1. ", astNode: null, extensionASTNodes: null, config: { } } } email: { type: { name: String, description: "The `String` scalar type represents textual data, represented as UTF-8\ncharacter sequences. The String type is most often used by GraphQL to\nrepresent free-form human-readable text.", astNode: null, extensionASTNodes: null, config: { } } } diff --git a/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt b/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt index 7718755..2af0a85 100644 --- a/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt +++ b/test/__snapshots__/SchemaTest__testSchemaSdl__1.txt @@ -2,7 +2,7 @@ directive @external on FIELD_DEFINITION directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -directive @key(fields: String!) on OBJECT | INTERFACE +directive @key(fields: String!, resolvable: Boolean) on OBJECT | INTERFACE directive @link(url: String!, as: String, for: String, import: [link_Import!]) repeatable on SCHEMA From 970ca0b286c3b157d37392874d0503a78856fa16 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 13:01:40 +0300 Subject: [PATCH 26/40] Extend validation of type reference --- src/Types/EntityObjectType.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index 5d694ec..f58cf47 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -143,7 +143,11 @@ private function validateReferenceResolver(): void */ private function validateReferenceKeys(array $ref): void { - Utils::invariant(isset($ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.'); + Utils::invariant( + isset($ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]) + && $ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME] === $this->config['name'], + 'Type name must be provided in the reference.' + ); } /** From 75f8b3ae00fcd40b81bd8214f4712d668a65e477 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 2 Aug 2022 19:29:26 +0300 Subject: [PATCH 27/40] Add validation if Referenced entity has only one @key directive. --- README.md | 2 +- src/Types/EntityRefObjectType.php | 23 ++++++++ test/EntitiesTest.php | 57 +++++++++++++++++++ test/StarWarsSchema.php | 6 +- .../SchemaTest__testServiceSdl__1.yml | 2 +- 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f03f5db..a477d60 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ use Apollo\Federation\Types\EntityRefObjectType; $userType = new EntityRefObjectType([ 'name' => 'User', - 'keys' => [['fields' => 'id'], ['fields' => 'email']], + 'keys' => [['fields' => 'id', 'resolvable' => false]], 'fields' => [ 'id' => ['type' => Type::int()], 'email' => ['type' => Type::string()] diff --git a/src/Types/EntityRefObjectType.php b/src/Types/EntityRefObjectType.php index 3789755..a6201d8 100644 --- a/src/Types/EntityRefObjectType.php +++ b/src/Types/EntityRefObjectType.php @@ -4,12 +4,35 @@ namespace Apollo\Federation\Types; +use GraphQL\Utils\Utils; + /** * An entity reference is a type referencing an entity owned by another service. Usually, * entity references are stub types containing only the key fields necessary for the * Apollo Gateway {@see https://www.apollographql.com/docs/intro/platform/#gateway } to * resolve the entity during query execution. + * + * @see https://www.apollographql.com/docs/federation/v1/entities#referencing-entities */ class EntityRefObjectType extends EntityObjectType { + public function __construct(array $config) + { + parent::__construct($config); + + $keys = $this->getKeys(); + Utils::invariant( + 1 === \count($keys), + 'There is invalid config of %s. Referenced entity must have exactly one directive @key.', + $this->name + ); + + /** @var array $key */ + $key = reset($keys); + Utils::invariant( + \array_key_exists('resolvable', $key) && false === $key['resolvable'], + 'There is invalid config of %s. Referenced entity directive @key must have argument "resolvable" with value `false`.', + $this->name + ); + } } diff --git a/test/EntitiesTest.php b/test/EntitiesTest.php index f0b4c42..1e480f6 100644 --- a/test/EntitiesTest.php +++ b/test/EntitiesTest.php @@ -6,6 +6,7 @@ use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; +use GraphQL\Error\InvariantViolation; use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; use Spatie\Snapshots\MatchesSnapshots; @@ -99,4 +100,60 @@ public function testCreatingEntityRefType(): void $this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys()); $this->assertMatchesSnapshot($userType->config); } + + /** + * @doesNotPerformAssertions + * @dataProvider getDataEntityRefObjectTypeWithSingleKeys + * + * @param array $config + */ + public function testEntityRefObjectTypeDoesNotThrowsErrorOnSingleKeys(array $config): void + { + new EntityRefObjectType($config); + } + + /** + * @dataProvider getDataEntityRefObjectTypeWithMultipleKeys + * + * @param array $config + */ + public function testEntityRefObjectTypeThrowsErrorOnMultipleKeys(array $config): void + { + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('There is invalid config of EntityRefObject. Referenced entity must have exactly one directive @key.'); + new EntityRefObjectType($config); + } + + /** + * @dataProvider getDataWithInvalidResolvableArgument + * + * @param array $config + */ + public function testEntityRefObjectTypeThrowsErrorOnInvalidResolvableArgument(array $config): void + { + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('There is invalid config of EntityRefObject. Referenced entity directive @key must have argument "resolvable" with value `false`.'); + new EntityRefObjectType($config); + } + + public function getDataEntityRefObjectTypeWithMultipleKeys(): \Generator + { + yield [['keys' => [['fields' => 'id'], ['fields' => 'id2']]]]; + yield [['keys' => [['fields' => 'id'], ['fields' => 'id2'], ['fields' => 'id2']]]]; + } + + public function getDataEntityRefObjectTypeWithSingleKeys(): \Generator + { + yield [['keys' => [['fields' => 'id', 'resolvable' => false]]]]; + yield [['keys' => [['fields' => ['obj' => 'id'], 'resolvable' => false]]]]; + } + + public function getDataWithInvalidResolvableArgument(): \Generator + { + yield [['keys' => [['fields' => 'id']]]]; + yield [['keys' => [['fields' => 'id', 'resolvable' => true]]]]; + yield [['keys' => [['fields' => 'id', 'resolvable' => 0]]]]; + yield [['keys' => [['fields' => 'id', 'resolvable' => 'no']]]]; + yield [['keys' => [['fields' => 'id', 'resolvable' => 'false']]]]; + } } diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index 5673e89..f6c1d71 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -83,7 +83,7 @@ private static function getEpisodeType(): EntityObjectType 'provides' => 'name', ], ], - EntityObjectType::FIELD_KEYS => ['fields' => 'id'], + EntityObjectType::FIELD_KEYS => [['fields' => 'id']], EntityObjectType::FIELD_REFERENCE_RESOLVER => static function (array $ref): array { $entity = StarWarsData::getEpisodeById($ref['id']); $entity['__typename'] = 'Episode'; @@ -113,7 +113,7 @@ private static function getCharacterType(): EntityRefObjectType 'requires' => 'name', ], ], - EntityObjectType::FIELD_KEYS => ['fields' => 'id'], + EntityObjectType::FIELD_KEYS => [['fields' => 'id', 'resolvable' => false]], ]); } @@ -132,7 +132,7 @@ private static function getLocationType(): EntityRefObjectType 'isExternal' => true, ], ], - EntityObjectType::FIELD_KEYS => ['fields' => 'id'], + EntityObjectType::FIELD_KEYS => [['fields' => 'id', 'resolvable' => false]], ]); } } diff --git a/test/__snapshots__/SchemaTest__testServiceSdl__1.yml b/test/__snapshots__/SchemaTest__testServiceSdl__1.yml index 799ec1c..588e21c 100644 --- a/test/__snapshots__/SchemaTest__testServiceSdl__1.yml +++ b/test/__snapshots__/SchemaTest__testServiceSdl__1.yml @@ -1,2 +1,2 @@ data: - _service: { sdl: "\"\"\"A character in the Star Wars Trilogy\"\"\"\nextend type Character @key(fields: \"id\") {\n id: Int! @external\n name: String! @external\n locations: [Location]! @requires(fields: \"name\")\n}\n\n\"\"\"A film in the Star Wars Trilogy\"\"\"\ntype Episode @key(fields: \"id\") {\n id: Int! \n title: String! \n characters: [Character!]! @provides(fields: \"name\")\n}\n\n\"\"\"A location in the Star Wars Trilogy\"\"\"\nextend type Location @key(fields: \"id\") {\n id: Int! @external\n name: String! @external\n}\n\nextend type Query {\n episodes: [Episode!]! \n deprecatedEpisodes: [Episode!]! @deprecated(reason: \"Because you should use the other one.\") \n}\n\nscalar link_Import\n" } + _service: { sdl: "\"\"\"A character in the Star Wars Trilogy\"\"\"\nextend type Character @key(fields: \"id\", resolvable: false) {\n id: Int! @external\n name: String! @external\n locations: [Location]! @requires(fields: \"name\")\n}\n\n\"\"\"A film in the Star Wars Trilogy\"\"\"\ntype Episode @key(fields: \"id\") {\n id: Int! \n title: String! \n characters: [Character!]! @provides(fields: \"name\")\n}\n\n\"\"\"A location in the Star Wars Trilogy\"\"\"\nextend type Location @key(fields: \"id\", resolvable: false) {\n id: Int! @external\n name: String! @external\n}\n\nextend type Query {\n episodes: [Episode!]! \n deprecatedEpisodes: [Episode!]! @deprecated(reason: \"Because you should use the other one.\") \n}\n\nscalar link_Import\n" } From 86ae658c894671cdd36485d028bf9ae9e365d8b5 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 17:24:55 +0300 Subject: [PATCH 28/40] Rename file "phpunit.xml" to "phpunit.xml.dist" to stick to standard testing configuration files pattern and permit safe overriding configs in local environment. --- .gitignore | 3 ++- phpunit.xml => phpunit.xml.dist | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename phpunit.xml => phpunit.xml.dist (100%) diff --git a/.gitignore b/.gitignore index f5aea2f..f05fec2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar /vendor/ /node_modules/ *.cache -cov.xml \ No newline at end of file +cov.xml +phpunit.xml \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 100% rename from phpunit.xml rename to phpunit.xml.dist From ae58a6ee263a0e02bfda4924fe5ad1a033516fad Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 5 Aug 2022 16:47:12 +0300 Subject: [PATCH 29/40] Extract getting of required federated directives to the schema builder --- src/FederatedSchema.php | 3 +-- src/FederatedSchemaTrait.php | 18 ------------------ src/SchemaBuilder.php | 34 ++++++++++++++++++++++++++++++++++ test/StarWarsSchema.php | 10 ++++++++-- 4 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 src/SchemaBuilder.php diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index bbb6ec1..7a7e8d1 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -81,10 +81,9 @@ public static function isReservedRootType(string $name): bool public function __construct(array $config) { $this->entityTypes = $this->extractEntityTypes($config); - $this->entityDirectives = Directives::getDirectives(); $this->schemaExtensionTypes = $this->extractSchemaExtensionTypes($config); - $config = array_merge($config, $this->getEntityDirectivesConfig($config), $this->getQueryTypeConfig($config)); + $config = array_merge($config, $this->getQueryTypeConfig($config)); parent::__construct($config); } diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php index 6942362..781a629 100644 --- a/src/FederatedSchemaTrait.php +++ b/src/FederatedSchemaTrait.php @@ -28,11 +28,6 @@ trait FederatedSchemaTrait */ protected array $schemaExtensionTypes = []; - /** - * @var Directive[] - */ - protected array $entityDirectives = []; - /** * Returns all the resolved entity types in the schema. * @@ -59,19 +54,6 @@ public function getSchemaExtensionTypes(): array return $this->schemaExtensionTypes; } - /** - * @param array $config - * - * @return array - */ - protected function getEntityDirectivesConfig(array $config): array - { - $directives = $config['directives'] ?? []; - $config['directives'] = array_merge($directives, $this->entityDirectives); - - return $config; - } - /** * @param array{ query: ObjectType } $config * diff --git a/src/SchemaBuilder.php b/src/SchemaBuilder.php new file mode 100644 index 0000000..255a8d4 --- /dev/null +++ b/src/SchemaBuilder.php @@ -0,0 +1,34 @@ + $schemaConfig + * @param array $builderConfig + */ + public function build(array $schemaConfig, array $builderConfig = []): FederatedSchema + { + $builderConfig += ['directives' => ['link']]; + $schemaConfig = array_merge($schemaConfig, $this->getEntityDirectivesConfig($schemaConfig, $builderConfig)); + + return new FederatedSchema($schemaConfig); + } + + /** + * @param array $schemaConfig + * @param array{ directives: array } $builderConfig + * + * @return array + */ + protected function getEntityDirectivesConfig(array $schemaConfig, array $builderConfig): array + { + $directives = array_intersect_key(Directives::getDirectives(), array_flip($builderConfig['directives'])); + $schemaConfig['directives'] = array_merge($schemaConfig['directives'] ?? [], $directives); + + return $schemaConfig; + } +} diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index f6c1d71..927747b 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -4,7 +4,9 @@ namespace Apollo\Federation\Tests; +use Apollo\Federation\Enum\DirectiveEnum; use Apollo\Federation\FederatedSchema; +use Apollo\Federation\SchemaBuilder; use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; use GraphQL\Type\Definition\ObjectType; @@ -18,8 +20,10 @@ class StarWarsSchema public static function getEpisodesSchema(): FederatedSchema { if (!self::$episodesSchema) { - self::$episodesSchema = new FederatedSchema([ + self::$episodesSchema = (new SchemaBuilder())->build([ 'query' => self::getQueryType(), + ], [ + 'directives' => DirectiveEnum::getAll(), ]); } @@ -29,7 +33,7 @@ public static function getEpisodesSchema(): FederatedSchema public static function getEpisodesSchemaCustomResolver(): FederatedSchema { if (!self::$overriddenEpisodesSchema) { - self::$overriddenEpisodesSchema = new FederatedSchema([ + self::$overriddenEpisodesSchema = (new SchemaBuilder())->build([ 'query' => self::getQueryType(), 'resolve' => function ($root, $args, $context, $info): array { return array_map(static function (array $ref) use ($info) { @@ -40,6 +44,8 @@ public static function getEpisodesSchemaCustomResolver(): FederatedSchema return $type->resolveReference($ref); }, $args[FederatedSchema::RESERVED_FIELD_REPRESENTATIONS]); }, + ], [ + 'directives' => DirectiveEnum::getAll(), ]); } From 78ba78c1054d1cec4f01435a2411897aee3a379e Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 5 Aug 2022 20:32:51 +0300 Subject: [PATCH 30/40] Fix extendability of schema --- src/FederatedSchemaTrait.php | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php index 781a629..c7e36ec 100644 --- a/src/FederatedSchemaTrait.php +++ b/src/FederatedSchemaTrait.php @@ -166,6 +166,24 @@ protected function resolve($root, $args, $context, $info): array }, $args[FederatedSchema::RESERVED_FIELD_REPRESENTATIONS]); } + /** + * @param array $config + * + * @return array + */ + protected function extractExtraTypes(array $config): array + { + $typeMap = []; + $configTypes = $config['types'] ?? []; + if (\is_array($configTypes)) { + $typeMap = $configTypes; + } elseif (\is_callable($configTypes)) { + $typeMap = $configTypes(); + } + + return $typeMap; + } + /** * @param array $config * @@ -192,17 +210,8 @@ protected function extractEntityTypes(array $config): array */ protected function extractSchemaExtensionTypes(array $config): array { - $typeMap = []; - $configTypes = $config['types'] ?? []; - if (\is_array($configTypes)) { - $typeMap = $configTypes; - } elseif (\is_callable($configTypes)) { - $typeMap = $configTypes(); - } - $types = []; - - foreach ($typeMap as $type) { + foreach ($this->extractExtraTypes($config) as $type) { if ($type instanceof SchemaExtensionType) { $types[$type->name] = $type; } From fe7456cc80fdd5c27217827fe4b635eb0fce72f8 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 29 Aug 2022 17:59:35 +0300 Subject: [PATCH 31/40] Update phpdoc --- src/Types/EntityObjectType.php | 2 ++ src/Types/EntityRefObjectType.php | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index f58cf47..e0e05a8 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -85,6 +85,8 @@ public function __construct(array $config) * @deprecated Use {@see getKeys()} * * @return array|array> + * + * @codeCoverageIgnore */ public function getKeyFields(): array { diff --git a/src/Types/EntityRefObjectType.php b/src/Types/EntityRefObjectType.php index a6201d8..98dcf0f 100644 --- a/src/Types/EntityRefObjectType.php +++ b/src/Types/EntityRefObjectType.php @@ -13,6 +13,7 @@ * resolve the entity during query execution. * * @see https://www.apollographql.com/docs/federation/v1/entities#referencing-entities + * @see https://www.apollographql.com/docs/federation/entities#referencing-an-entity-without-contributing-fields */ class EntityRefObjectType extends EntityObjectType { From 408c3e19f40dfc35cbf99b688e231c9f2c7118b2 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 29 Aug 2022 18:10:08 +0300 Subject: [PATCH 32/40] Remove deprecated excess classes imports --- src/FederatedSchema.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/FederatedSchema.php b/src/FederatedSchema.php index 7a7e8d1..d0ae2b3 100644 --- a/src/FederatedSchema.php +++ b/src/FederatedSchema.php @@ -4,17 +4,7 @@ namespace Apollo\Federation; -use Apollo\Federation\Types\EntityObjectType; -use Apollo\Federation\Utils\FederatedSchemaPrinter; -use GraphQL\Type\Definition\CustomScalarType; -use GraphQL\Type\Definition\Directive; -use GraphQL\Type\Definition\ListOfType; -use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; -use GraphQL\Utils\TypeInfo; -use GraphQL\Utils\Utils; /** * A federated GraphQL schema definition see related docs {@see https://www.apollographql.com/docs/apollo-server/federation/introduction }. From 07a0f9e80b0ef891ce68fa4d9f69d1eada721679 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 29 Aug 2022 18:18:02 +0300 Subject: [PATCH 33/40] Fix merge conflict: make updates equivalent to the commit 4f958ddb683c1fceb1d162f58a5dc2957e26fe5d --- src/SchemaBuilder.php | 5 +++++ test/StarWarsSchema.php | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/SchemaBuilder.php b/src/SchemaBuilder.php index 255a8d4..cda327a 100644 --- a/src/SchemaBuilder.php +++ b/src/SchemaBuilder.php @@ -4,6 +4,8 @@ namespace Apollo\Federation; +use GraphQL\Type\Definition\Directive; + class SchemaBuilder { /** @@ -27,6 +29,9 @@ public function build(array $schemaConfig, array $builderConfig = []): Federated protected function getEntityDirectivesConfig(array $schemaConfig, array $builderConfig): array { $directives = array_intersect_key(Directives::getDirectives(), array_flip($builderConfig['directives'])); + if (array_intersect_key($directives, Directive::getInternalDirectives())) { + throw new \LogicException('Some Apollo directives override internals.'); + } $schemaConfig['directives'] = array_merge($schemaConfig['directives'] ?? [], $directives); return $schemaConfig; diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index 927747b..f04e48f 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -9,6 +9,7 @@ use Apollo\Federation\SchemaBuilder; use Apollo\Federation\Types\EntityObjectType; use Apollo\Federation\Types\EntityRefObjectType; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -21,6 +22,7 @@ public static function getEpisodesSchema(): FederatedSchema { if (!self::$episodesSchema) { self::$episodesSchema = (new SchemaBuilder())->build([ + 'directives' => Directive::getInternalDirectives(), 'query' => self::getQueryType(), ], [ 'directives' => DirectiveEnum::getAll(), @@ -34,6 +36,7 @@ public static function getEpisodesSchemaCustomResolver(): FederatedSchema { if (!self::$overriddenEpisodesSchema) { self::$overriddenEpisodesSchema = (new SchemaBuilder())->build([ + 'directives' => Directive::getInternalDirectives(), 'query' => self::getQueryType(), 'resolve' => function ($root, $args, $context, $info): array { return array_map(static function (array $ref) use ($info) { From a34d3361d0c1e16cac99a40885cbb7cd4ff37162 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 30 Aug 2022 09:06:05 +0300 Subject: [PATCH 34/40] Fix documentation --- README.md | 8 ++++---- src/Types/EntityObjectType.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a477d60..7f6143a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 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; @@ -29,13 +29,13 @@ $userType = new EntityObjectType([ '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. } ]); ``` -* `keys` — defines the entity's primary key, which consists of one or more of the fields. 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. diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index e0e05a8..f20710a 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -15,7 +15,7 @@ * of the type, similar to the function of a primary key in a SQL table * see related docs {@see https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#entities-and-keys }. * - * The `keyFields` property is required in the configuration, indicating the fields that + * The `keys` property is required in the configuration, indicating the fields that * serve as the unique keys or identifiers of the entity. * * Sample usage: From edce7284cad70afb8acfb87e8d69c13240e6c8b5 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 7 Sep 2022 21:30:00 +0300 Subject: [PATCH 35/40] Implement tests --- phpunit.xml.dist | 5 ++++- test/EntityTest.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 test/EntityTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 75731ed..55389c4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,8 @@ - + ./src diff --git a/test/EntityTest.php b/test/EntityTest.php new file mode 100644 index 0000000..8b62068 --- /dev/null +++ b/test/EntityTest.php @@ -0,0 +1,34 @@ +expectException(InvariantViolation::class); + $config = ['keys' => [], 'keyFields' => [], 'name' => '*']; + new EntityObjectType($config); + } + + public function testMethodGetKeyFieldsTriggersDeprecation(): void + { + $isCaught = false; + set_error_handler(static function (int $errno, string $errstr, string $errfile = '', int $errline = 0, array $errcontext = []) use (&$isCaught): bool { + $isCaught = true; + return true; + }); + $config = ['name' => '*', 'keys' => [['fields' => ['id']]]]; + (new EntityObjectType($config))->getKeyFields(); + + self::assertTrue($isCaught, 'It does trigger deprecation error. But it should!'); + + restore_error_handler(); + } +} \ No newline at end of file From 3562fe3769b975771fe39f1d24f61e9d1bbd531c Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 7 Sep 2022 21:41:51 +0300 Subject: [PATCH 36/40] Implement tests --- src/Types/EntityObjectType.php | 2 +- test/EntityTest.php | 47 +++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index f20710a..f92ac6a 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -97,7 +97,7 @@ public function getKeyFields(): array \E_USER_DEPRECATED ); - return array_map(static fn(array $x) => $x['fields'], $this->keys); + return array_merge(...array_map(static fn(array $x): array => (array) $x['fields'], $this->keys)); } /** diff --git a/test/EntityTest.php b/test/EntityTest.php index 8b62068..e759ff2 100644 --- a/test/EntityTest.php +++ b/test/EntityTest.php @@ -17,11 +17,24 @@ public function testConstructorsThrowsExceptionOnDuplicatedKeysConfig(): void new EntityObjectType($config); } + /** + * @dataProvider getDataForTestMethodGetKeyFields + * + * @param string[] $expected + * @param array $config + */ + public function testMethodGetKeyFields(array $expected, array $config): void + { + self::assertSame($expected, (new EntityObjectType($config))->getKeyFields()); + } + public function testMethodGetKeyFieldsTriggersDeprecation(): void { $isCaught = false; - set_error_handler(static function (int $errno, string $errstr, string $errfile = '', int $errline = 0, array $errcontext = []) use (&$isCaught): bool { + set_error_handler(static function (int $n, string $s, string $f = '', int $l = 0, array $c = []) + use (&$isCaught): bool { $isCaught = true; + return true; }); $config = ['name' => '*', 'keys' => [['fields' => ['id']]]]; @@ -31,4 +44,36 @@ public function testMethodGetKeyFieldsTriggersDeprecation(): void restore_error_handler(); } + + public function getDataForTestMethodGetKeyFields(): \Generator + { + yield [ + ['id'], + ['name' => '*', 'keyFields' => ['id']], + ]; + yield [ + ['id', 'email'], + ['name' => '*', 'keyFields' => ['id', 'email']], + ]; + yield [ + ['id'], + ['name' => '*', 'keys' => [['fields' => 'id']]], + ]; + yield [ + ['id'], + ['name' => '*', 'keys' => [['fields' => ['id']]]], + ]; + yield [ + ['id', 'email'], + ['name' => '*', 'keys' => [['fields' => ['id', 'email']]]], + ]; + yield [ + ['id', 'email'], + ['name' => '*', 'keys' => [['fields' => ['id']], ['fields' => ['email']]]], + ]; + yield [ + ['id', 'email'], + ['name' => '*', 'keys' => [['fields' => 'id'], ['fields' => 'email']]], + ]; + } } \ No newline at end of file From 7002d92defa51d1076907315c67ee7b400929c29 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 7 Sep 2022 21:49:17 +0300 Subject: [PATCH 37/40] Fix test failed assertion message. --- test/EntityTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EntityTest.php b/test/EntityTest.php index e759ff2..1fd308c 100644 --- a/test/EntityTest.php +++ b/test/EntityTest.php @@ -40,7 +40,7 @@ public function testMethodGetKeyFieldsTriggersDeprecation(): void $config = ['name' => '*', 'keys' => [['fields' => ['id']]]]; (new EntityObjectType($config))->getKeyFields(); - self::assertTrue($isCaught, 'It does trigger deprecation error. But it should!'); + self::assertTrue($isCaught, 'It does not trigger deprecation error. But it should!'); restore_error_handler(); } From 1b663b5541c0c7df6e7dc6df2a7c835d4dbfed4f Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 28 Oct 2022 00:14:27 +0300 Subject: [PATCH 38/40] Update entity reference resolver validation --- src/Types/EntityObjectType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index f92ac6a..0b979ee 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -137,7 +137,7 @@ public function resolveReference($ref, $context = null, $info = null) private function validateReferenceResolver(): void { - Utils::invariant(isset($this->referenceResolver), 'No reference resolver was set in the configuration.'); + Utils::invariant(\is_callable($this->referenceResolver), 'Invalid reference resolver configuration.'); } /** From 60feec36a149837a27d144d2f95fd43e288ce270 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 28 Oct 2022 00:14:57 +0300 Subject: [PATCH 39/40] Remove excess class import --- src/FederatedSchemaTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php index c7e36ec..f2d04ed 100644 --- a/src/FederatedSchemaTrait.php +++ b/src/FederatedSchemaTrait.php @@ -8,7 +8,6 @@ use Apollo\Federation\Types\SchemaExtensionType; use Apollo\Federation\Utils\FederatedSchemaPrinter; use GraphQL\Type\Definition\CustomScalarType; -use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; From b81878710f0160f96c97ed1f6b8e5ecaa69acd52 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 9 Feb 2023 21:39:48 +0300 Subject: [PATCH 40/40] Downgrade to PHP 7.1 --- composer.json | 3 ++- composer.lock | 2 +- src/Directives.php | 2 +- src/Directives/LinkDirective.php | 9 ++++++++- src/Enum/DirectiveEnum.php | 2 +- src/FederatedSchemaTrait.php | 16 +++++++++++----- src/Types/EntityObjectType.php | 11 +++++++---- src/Utils/FederatedSchemaPrinter.php | 23 ++++++++++++++++++----- test/StarWarsData.php | 18 ++++++++++++------ test/StarWarsSchema.php | 23 ++++++++++++++++++----- 10 files changed, 79 insertions(+), 30 deletions(-) diff --git a/composer.json b/composer.json index 99a662c..c01542d 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "library", "license": "MIT", "require": { - "php": "^7.4||^8.0", + "php": "^7.1||^8.0", + "ext-json": "*", "webonyx/graphql-php": "^0.13.8 || ^14.0" }, "scripts": { diff --git a/composer.lock b/composer.lock index 0da2c98..ff1b0f0 100644 --- a/composer.lock +++ b/composer.lock @@ -3209,7 +3209,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.4||^8.0" + "php": "^7.1||^8.0" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/src/Directives.php b/src/Directives.php index 08a1b19..db87a81 100644 --- a/src/Directives.php +++ b/src/Directives.php @@ -31,7 +31,7 @@ class Directives * shareable: ShareableDirective, * }|null */ - private static ?array $directives = null; + private static $directives; /** * Gets the @key directive. diff --git a/src/Directives/LinkDirective.php b/src/Directives/LinkDirective.php index 87f3c27..93d2c4a 100644 --- a/src/Directives/LinkDirective.php +++ b/src/Directives/LinkDirective.php @@ -21,7 +21,14 @@ public function __construct() { $linkImport = new CustomScalarType([ 'name' => 'link_Import', - 'serialize' => static fn ($value) => json_encode($value, \JSON_THROW_ON_ERROR), + 'serialize' => static function ($value) { + $data = json_encode($value); + if (json_last_error()) { + throw new \RuntimeException(json_last_error_msg()); + } + + return $data; + }, 'parseValue' => static function ($value) { if (\is_string($value)) { return $value; diff --git a/src/Enum/DirectiveEnum.php b/src/Enum/DirectiveEnum.php index 06abf9a..30d466a 100644 --- a/src/Enum/DirectiveEnum.php +++ b/src/Enum/DirectiveEnum.php @@ -18,7 +18,7 @@ class DirectiveEnum /** * @var string[]|null */ - protected static ?array $constants = null; + protected static $constants; /** * @return string[] diff --git a/src/FederatedSchemaTrait.php b/src/FederatedSchemaTrait.php index f2d04ed..b41ce64 100644 --- a/src/FederatedSchemaTrait.php +++ b/src/FederatedSchemaTrait.php @@ -20,12 +20,12 @@ trait FederatedSchemaTrait /** * @var EntityObjectType[] */ - protected array $entityTypes = []; + protected $entityTypes = []; /** * @var SchemaExtensionType[] */ - protected array $schemaExtensionTypes = []; + protected $schemaExtensionTypes = []; /** * Returns all the resolved entity types in the schema. @@ -89,7 +89,9 @@ protected function getQueryTypeServiceFieldConfig(): array 'fields' => [ FederatedSchema::RESERVED_FIELD_SDL => [ 'type' => Type::string(), - 'resolve' => fn (): string => FederatedSchemaPrinter::doPrint($this), + 'resolve' => function (): string { + return FederatedSchemaPrinter::doPrint($this); + }, ], ], ]); @@ -97,7 +99,9 @@ protected function getQueryTypeServiceFieldConfig(): array return [ FederatedSchema::RESERVED_FIELD_SERVICE => [ 'type' => Type::nonNull($serviceType), - 'resolve' => static fn (): array => [], + 'resolve' => static function (): array { + return []; + }, ], ]; } @@ -120,7 +124,9 @@ protected function getQueryTypeEntitiesFieldConfig(?array $config): array $anyType = new CustomScalarType([ 'name' => FederatedSchema::RESERVED_TYPE_ANY, - 'serialize' => static fn ($value) => $value, + 'serialize' => static function ($value) { + return $value; + }, ]); return [ diff --git a/src/Types/EntityObjectType.php b/src/Types/EntityObjectType.php index 0b979ee..f675f25 100644 --- a/src/Types/EntityObjectType.php +++ b/src/Types/EntityObjectType.php @@ -56,7 +56,7 @@ class EntityObjectType extends ObjectType /** * @var array|array>, resolvable: bool }> */ - private array $keys; + private $keys; /** * @param array $config @@ -68,8 +68,9 @@ public function __construct(array $config) 'Use only one way to define directives @key.' ); - $this->keys = $config[self::FIELD_KEYS] - ?? array_map(static fn ($x): array => ['fields' => $x], $config['keyFields']); + $this->keys = $config[self::FIELD_KEYS] ?? array_map(static function ($x): array { + return ['fields' => $x]; + }, $config['keyFields']); if (isset($config[self::FIELD_REFERENCE_RESOLVER])) { self::validateResolveReference($config); @@ -97,7 +98,9 @@ public function getKeyFields(): array \E_USER_DEPRECATED ); - return array_merge(...array_map(static fn(array $x): array => (array) $x['fields'], $this->keys)); + return array_merge(...array_map(static function (array $x): array { + return (array) $x['fields']; + }, $this->keys)); } /** diff --git a/src/Utils/FederatedSchemaPrinter.php b/src/Utils/FederatedSchemaPrinter.php index d541e20..281584d 100644 --- a/src/Utils/FederatedSchemaPrinter.php +++ b/src/Utils/FederatedSchemaPrinter.php @@ -73,8 +73,12 @@ public static function doPrint(Schema $schema, array $options = []): string { return static::printFilteredSchema( $schema, - static fn (Directive $type): bool => !Directive::isSpecifiedDirective($type) && !static::isFederatedDirective($type), - static fn (Type $type): bool => !Type::isBuiltInType($type), + static function (Directive $type): bool { + return !Directive::isSpecifiedDirective($type) && !static::isFederatedDirective($type); + }, + static function (Type $type): bool { + return !Type::isBuiltInType($type); + }, $options ); } @@ -185,7 +189,9 @@ protected static function printImplementedInterfaces(ObjectType $type): string $interfaces = $type->getInterfaces(); return !empty($interfaces) - ? ' implements ' . implode(' & ', array_map(static fn (InterfaceType $i): string => $i->name, $interfaces)) + ? ' implements ' . implode(' & ', array_map(static function (InterfaceType $i): string { + return $i->name; + }, $interfaces)) : ''; } @@ -257,7 +263,12 @@ protected static function printLinkDirectiveArgumentValue($argument, string $nam throw new \InvalidArgumentException('Invalid type of "import" argument value'); } - return json_encode($argument, \JSON_THROW_ON_ERROR); + $data = json_encode($argument); + if (json_last_error()) { + throw new \RuntimeException(json_last_error_msg()); + } + + return $data; } /** @@ -301,7 +312,9 @@ protected static function printSchemaExtensionType(SchemaExtensionType $schemaEx return sprintf( 'extend schema %s\n', - implode("\n", array_map(static fn (array $x): string => static::printLinkDirectiveConfig($x), $links)), + implode("\n", array_map(static function (array $x): string { + return static::printLinkDirectiveConfig($x); + }, $links)) ); } } diff --git a/test/StarWarsData.php b/test/StarWarsData.php index 27bd28e..b2b9098 100644 --- a/test/StarWarsData.php +++ b/test/StarWarsData.php @@ -9,24 +9,26 @@ class StarWarsData /** * @var array>|null */ - private static ?array $episodes = null; + private static $episodes; /** * @var array>|null */ - private static ?array $characters = null; + private static $characters; /** * @var array>|null */ - private static ?array $locations = null; + private static $locations; /** * @return array|null */ public static function getEpisodeById(int $id): ?array { - $matches = array_filter(self::getEpisodes(), static fn (array $episode): bool => $episode['id'] === $id); + $matches = array_filter(self::getEpisodes(), static function (array $episode) use ($id): bool { + return $episode['id'] === $id; + }); return reset($matches) ?: null; } @@ -66,7 +68,9 @@ public static function getEpisodes(): array */ public static function getCharactersByIds(array $ids): array { - return array_filter(self::getCharacters(), static fn ($item): bool => \in_array($item['id'], $ids, true)); + return array_filter(self::getCharacters(), static function ($item) use ($ids): bool { + return \in_array($item['id'], $ids, true); + }); } /** @@ -104,7 +108,9 @@ public static function getCharacters(): array */ public static function getLocationsByIds(array $ids): array { - return array_filter(self::getLocations(), static fn ($item): bool => \in_array($item['id'], $ids, true)); + return array_filter(self::getLocations(), static function ($item) use ($ids): bool { + return \in_array($item['id'], $ids, true); + }); } /** diff --git a/test/StarWarsSchema.php b/test/StarWarsSchema.php index f04e48f..bec8a2b 100644 --- a/test/StarWarsSchema.php +++ b/test/StarWarsSchema.php @@ -15,8 +15,15 @@ class StarWarsSchema { - public static ?FederatedSchema $episodesSchema = null; - public static ?FederatedSchema $overriddenEpisodesSchema = null; + /** + * @var FederatedSchema|null + */ + public static $episodesSchema; + + /** + * @var FederatedSchema|null + */ + public static $overriddenEpisodesSchema; public static function getEpisodesSchema(): FederatedSchema { @@ -64,7 +71,9 @@ private static function getQueryType(): ObjectType 'fields' => [ 'episodes' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($episodeType))), - 'resolve' => static fn (): array => StarWarsData::getEpisodes(), + 'resolve' => static function (): array { + return StarWarsData::getEpisodes(); + }, ], 'deprecatedEpisodes' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull($episodeType))), @@ -88,7 +97,9 @@ private static function getEpisodeType(): EntityObjectType ], 'characters' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::getCharacterType()))), - 'resolve' => static fn ($root): array => StarWarsData::getCharactersByIds($root['characters']), + 'resolve' => static function ($root): array { + return StarWarsData::getCharactersByIds($root['characters']); + }, 'provides' => 'name', ], ], @@ -118,7 +129,9 @@ private static function getCharacterType(): EntityRefObjectType ], 'locations' => [ 'type' => Type::nonNull(Type::listOf(self::getLocationType())), - 'resolve' => static fn ($root): array => StarWarsData::getLocationsByIds($root['locations']), + 'resolve' => static function ($root): array { + return StarWarsData::getLocationsByIds($root['locations']); + }, 'requires' => 'name', ], ],