├── LICENCE ├── README.md ├── composer.json ├── renovate.json └── src ├── Builder ├── EnumBuilder.php ├── FieldBuilder.php ├── InputFieldBuilder.php ├── InputObjectBuilder.php ├── InterfaceBuilder.php ├── ObjectBuilder.php ├── TypeBuilder.php └── UnionBuilder.php ├── Error ├── Error.php └── FormattedError.php └── Exception └── InvalidArgument.php /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Šimon Podlipský 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP GraphQL Utils for graphql-php 2 | 3 | [![GitHub Actions][GA Image]][GA Link] 4 | [![Code Coverage][Coverage Image]][CodeCov Link] 5 | [![Downloads][Downloads Image]][Packagist Link] 6 | [![Packagist][Packagist Image]][Packagist Link] 7 | [![Infection MSI][Infection Image]][Infection Link] 8 | 9 | ## Installation 10 | 11 | Add as [Composer](https://getcomposer.org/) dependency: 12 | 13 | ```sh 14 | composer require simpod/graphql-utils 15 | ``` 16 | 17 | ## Features 18 | 19 | ### Schema Builders 20 | 21 | Instead of defining your schema as an array, use can use more objective-oriented approach. This library provides set of strictly typed builders that help you build your schema: 22 | 23 | - EnumBuilder 24 | - FieldBuilder 25 | - InputFieldBuilder 26 | - InputObjectBuilder 27 | - InterfaceBuilder 28 | - ObjectBuilder 29 | - TypeBuilder 30 | - UnionBuilder 31 | 32 | #### ObjectBuilder and FieldBuilder 33 | 34 | ✔️ Standard way with `webonyx/graphql-php` 35 | 36 | ```php 37 | 'User', 44 | 'description' => 'Our blog visitor', 45 | 'fields' => [ 46 | 'firstName' => [ 47 | 'type' => Type::string(), 48 | 'description' => 'User first name' 49 | ], 50 | 'email' => Type::string() 51 | ], 52 | 'resolveField' => static function(User $user, $args, $context, ResolveInfo $info) { 53 | switch ($info->fieldName) { 54 | case 'name': 55 | return $user->getName(); 56 | case 'email': 57 | return $user->getEmail(); 58 | default: 59 | return null; 60 | } 61 | } 62 | ]); 63 | ``` 64 | 65 | ✨ The same can be produced in objective way 66 | 67 | ```php 68 | setDescription('Our blog visitor') 77 | ->setFields([ 78 | FieldBuilder::create('firstName', Type::string()) 79 | ->setDescription('User first name') 80 | ->build(), 81 | FieldBuilder::create('email', Type::string())->build(), 82 | ]) 83 | ->setFieldResolver( 84 | static function(User $user, $args, $context, ResolveInfo $info) { 85 | switch ($info->fieldName) { 86 | case 'name': 87 | return $user->getName(); 88 | case 'email': 89 | return $user->getEmail(); 90 | default: 91 | return null; 92 | } 93 | } 94 | ) 95 | ->build() 96 | ); 97 | ``` 98 | 99 | #### EnumBuilder 100 | 101 | ✔️ Standard way with `webonyx/graphql-php` 102 | 103 | ```php 104 | 'Episode', 110 | 'description' => 'One of the films in the Star Wars Trilogy', 111 | 'values' => [ 112 | 'NEWHOPE' => [ 113 | 'value' => 4, 114 | 'description' => 'Released in 1977.' 115 | ], 116 | 'EMPIRE' => [ 117 | 'value' => 5, 118 | 'description' => 'Released in 1980.' 119 | ], 120 | 'JEDI' => [ 121 | 'value' => 6, 122 | 'description' => 'Released in 1983.' 123 | ], 124 | ] 125 | ]); 126 | ``` 127 | 128 | ✨ The same can be produced in objective way 129 | 130 | ```php 131 | setDescription('One of the films in the Star Wars Trilogy') 139 | ->addValue(4, 'NEWHOPE', 'Released in 1977.') 140 | ->addValue(5, 'EMPIRE', 'Released in 1980.') 141 | ->addValue(6, 'JEDI', 'Released in 1983.') 142 | ->build() 143 | ); 144 | ``` 145 | 146 | #### InterfaceBuilder 147 | 148 | ✔️ Standard way with `webonyx/graphql-php` 149 | 150 | ```php 151 | 'Character', 158 | 'description' => 'A character in the Star Wars Trilogy', 159 | 'fields' => [ 160 | 'id' => [ 161 | 'type' => Type::nonNull(Type::string()), 162 | 'description' => 'The id of the character.', 163 | ], 164 | 'name' => [ 165 | 'type' => Type::string(), 166 | 'description' => 'The name of the character.' 167 | ] 168 | ], 169 | 'resolveType' => static function ($value) : object { 170 | if ($value->type === 'human') { 171 | return MyTypes::human(); 172 | } 173 | 174 | return MyTypes::droid(); 175 | } 176 | ]); 177 | ``` 178 | 179 | ✨ The same can be produced in objective way 180 | 181 | ```php 182 | setDescription('A character in the Star Wars Trilogy') 192 | ->setFields([ 193 | FieldBuilder::create('id', Type::nonNull(Type::string())) 194 | ->setDescription('The id of the character.') 195 | ->build(), 196 | FieldBuilder::create('name', Type::string()) 197 | ->setDescription('The name of the character.') 198 | ->build() 199 | ]) 200 | ->setResolveType( 201 | static function ($value) : object { 202 | if ($value->type === 'human') { 203 | return MyTypes::human(); 204 | } 205 | 206 | return MyTypes::droid(); 207 | } 208 | ) 209 | ->build() 210 | ); 211 | ``` 212 | 213 | #### UnionBuilder 214 | 215 | ✔️ Standard way with `webonyx/graphql-php` 216 | 217 | ```php 218 | 'SearchResult', 224 | 'types' => [ 225 | MyTypes::story(), 226 | MyTypes::user() 227 | ], 228 | 'resolveType' => static function($value) { 229 | if ($value->type === 'story') { 230 | return MyTypes::story(); 231 | } 232 | 233 | return MyTypes::user(); 234 | } 235 | ]); 236 | ``` 237 | 238 | ✨ The same can be produced in objective way 239 | 240 | ```php 241 | setResolveType( 254 | static function($value) { 255 | if ($value->type === 'story') { 256 | return MyTypes::story(); 257 | } 258 | 259 | return MyTypes::user(); 260 | } 261 | ) 262 | ->build() 263 | ); 264 | ``` 265 | 266 | [GA Image]: https://github.com/simPod/GraphQL-Utils/workflows/CI/badge.svg 267 | 268 | [GA Link]: https://github.com/simPod/GraphQL-Utils/actions?query=workflow%3A%22CI%22+branch%3A0.7.x 269 | 270 | [Coverage Image]: https://codecov.io/gh/simPod/GraphQL-Utils/branch/0.7.x/graph/badge.svg 271 | 272 | [CodeCov Link]: https://codecov.io/gh/simPod/GraphQL-Utils/branch/0.7.x 273 | 274 | [Downloads Image]: https://poser.pugx.org/simpod/graphql-utils/d/total.svg 275 | 276 | [Packagist Image]: https://poser.pugx.org/simpod/graphql-utils/v/stable.svg 277 | 278 | [Packagist Link]: https://packagist.org/packages/simpod/graphql-utils 279 | 280 | [Infection Image]: https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FsimPod%2FGraphQL-Utils%2F0.7.x 281 | 282 | [Infection Link]: https://dashboard.stryker-mutator.io/reports/github.com/simPod/GraphQL-Utils/0.7.x 283 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simpod/graphql-utils", 3 | "description": "GraphQL Utils for graphql-php", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Simon Podlipsky", 9 | "email": "simon@podlipsky.net" 10 | } 11 | ], 12 | "config": { 13 | "sort-packages": true, 14 | "allow-plugins": { 15 | "dealerdirect/phpcodesniffer-composer-installer": true, 16 | "infection/extension-installer": true, 17 | "phpstan/extension-installer": true 18 | } 19 | }, 20 | "require": { 21 | "php": "^8.2", 22 | "webonyx/graphql-php": "^15.4" 23 | }, 24 | "require-dev": { 25 | "doctrine/coding-standard": "^12.0", 26 | "infection/infection": "^0.29.0", 27 | "phpstan/extension-installer": "^1.1", 28 | "phpstan/phpstan": "^2.0", 29 | "phpstan/phpstan-phpunit": "^2.0", 30 | "phpstan/phpstan-strict-rules": "^2.0", 31 | "phpunit/phpunit": "^11.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "SimPod\\GraphQLUtils\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "SimPod\\GraphQLUtils\\Tests\\": "tests/" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>simPod/renovatebot-presets:php-lib" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/Builder/EnumBuilder.php: -------------------------------------------------------------------------------- 1 | $value]; 50 | if ($description !== null) { 51 | $enumDefinition['description'] = $description; 52 | } 53 | 54 | if ($deprecationReason !== null) { 55 | $enumDefinition['deprecationReason'] = $deprecationReason; 56 | } 57 | 58 | $this->values[$name] = $enumDefinition; 59 | 60 | return $this; 61 | } 62 | 63 | /** @phpstan-return EnumTypeConfig */ 64 | public function build(): array 65 | { 66 | return [ 67 | 'name' => $this->name, 68 | 'description' => $this->description, 69 | 'values' => $this->values, 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Builder/FieldBuilder.php: -------------------------------------------------------------------------------- 1 | type = $type; 42 | } 43 | 44 | /** 45 | * @phpstan-param FieldType $type 46 | * 47 | * @return static 48 | */ 49 | public static function create(BackedEnum|string $name, $type): self 50 | { 51 | return new static($name, $type); 52 | } 53 | 54 | /** @return $this */ 55 | public function setDescription(string $description): self 56 | { 57 | $this->description = $description; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @phpstan-param ArgumentType $type 64 | * 65 | * @return $this 66 | */ 67 | public function addArgument( 68 | string $name, 69 | $type, 70 | string|null $description = null, 71 | mixed $defaultValue = null, 72 | string|null $deprecationReason = null, 73 | ): self { 74 | if ($this->args === null) { 75 | $this->args = []; 76 | } 77 | 78 | $value = ['type' => $type]; 79 | 80 | if ($description !== null) { 81 | $value['description'] = $description; 82 | } 83 | 84 | if ($defaultValue !== null) { 85 | $value['defaultValue'] = $defaultValue; 86 | } 87 | 88 | if ($deprecationReason !== null) { 89 | $value['deprecationReason'] = $deprecationReason; 90 | } 91 | 92 | $this->args[$name] = $value; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * @phpstan-param FieldResolver $resolver 99 | * 100 | * @return $this 101 | */ 102 | public function setResolver(callable $resolver): self 103 | { 104 | $this->resolve = $resolver; 105 | 106 | return $this; 107 | } 108 | 109 | /** @return $this */ 110 | public function setDeprecationReason(string $reason): self 111 | { 112 | $this->deprecationReason = $reason; 113 | 114 | return $this; 115 | } 116 | 117 | /** @phpstan-return FieldDefinitionConfig */ 118 | public function build(): array 119 | { 120 | return [ 121 | 'args' => $this->args, 122 | 'name' => $this->name instanceof BackedEnum ? (string) $this->name->value : $this->name, 123 | 'description' => $this->description, 124 | 'deprecationReason' => $this->deprecationReason, 125 | 'resolve' => $this->resolve, 126 | 'type' => $this->type, 127 | ]; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Builder/InputFieldBuilder.php: -------------------------------------------------------------------------------- 1 | type = $type; 37 | } 38 | 39 | /** 40 | * @phpstan-param ArgumentType $type 41 | * 42 | * @return static 43 | */ 44 | public static function create(string $name, $type): self 45 | { 46 | return new static($name, $type); 47 | } 48 | 49 | /** @return $this */ 50 | public function setDefaultValue(mixed $defaultValue): self 51 | { 52 | $this->defaultValue = $defaultValue; 53 | 54 | return $this; 55 | } 56 | 57 | /** @return $this */ 58 | public function setDescription(string $description): self 59 | { 60 | $this->description = $description; 61 | 62 | return $this; 63 | } 64 | 65 | /** @return $this */ 66 | public function setDeprecationReason(string|null $deprecationReason): self 67 | { 68 | $this->deprecationReason = $deprecationReason; 69 | 70 | return $this; 71 | } 72 | 73 | /** @phpstan-return InputObjectFieldConfig */ 74 | public function build(): array 75 | { 76 | $config = [ 77 | 'name' => $this->name, 78 | 'deprecationReason' => $this->deprecationReason, 79 | 'description' => $this->description, 80 | 'type' => $this->type, 81 | ]; 82 | 83 | $property = new ReflectionProperty($this, 'defaultValue'); 84 | if ($property->isInitialized($this)) { 85 | $config['defaultValue'] = $this->defaultValue; 86 | } 87 | 88 | return $config; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Builder/InputObjectBuilder.php: -------------------------------------------------------------------------------- 1 | |array */ 18 | private $fields = []; 19 | 20 | final private function __construct(private string|null $name) 21 | { 22 | } 23 | 24 | /** @return static */ 25 | public static function create(string $name): self 26 | { 27 | return new static($name); 28 | } 29 | 30 | /** 31 | * @param callable():array|array $fields 32 | * 33 | * @return $this 34 | */ 35 | public function setFields(callable|array $fields): self 36 | { 37 | $this->fields = $fields; 38 | 39 | return $this; 40 | } 41 | 42 | /** @phpstan-return InputObjectConfig */ 43 | public function build(): array 44 | { 45 | return [ 46 | 'name' => $this->name, 47 | 'description' => $this->description, 48 | 'fields' => $this->fields, 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Builder/InterfaceBuilder.php: -------------------------------------------------------------------------------- 1 | >|callable():array> */ 24 | private $fields = []; 25 | 26 | final private function __construct(private string|null $name) 27 | { 28 | } 29 | 30 | /** @return static */ 31 | public static function create(string|null $name = null): self 32 | { 33 | return new static($name); 34 | } 35 | 36 | /** @return $this */ 37 | public function addInterface(InterfaceType $interfaceType): self 38 | { 39 | $this->interfaces[] = $interfaceType; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @param callable():array>|array> $fields 46 | * 47 | * @return $this 48 | */ 49 | public function setFields(callable|array $fields): self 50 | { 51 | $this->fields = $fields; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * @param ResolveType $resolveType 58 | * 59 | * @return $this 60 | */ 61 | public function setResolveType(callable $resolveType): self 62 | { 63 | $this->resolveType = $resolveType; 64 | 65 | return $this; 66 | } 67 | 68 | /** @phpstan-return InterfaceConfig */ 69 | public function build(): array 70 | { 71 | return [ 72 | 'name' => $this->name, 73 | 'description' => $this->description, 74 | 'interfaces' => $this->interfaces, 75 | 'fields' => $this->fields, 76 | 'resolveType' => $this->resolveType, 77 | ]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Builder/ObjectBuilder.php: -------------------------------------------------------------------------------- 1 | |callable():array */ 27 | private $fields = []; 28 | 29 | /** @var callable(mixed, array, mixed, ResolveInfo) : mixed|null */ 30 | private $fieldResolver; 31 | 32 | final private function __construct(private string|null $name) 33 | { 34 | } 35 | 36 | /** @return static */ 37 | public static function create(string $name): self 38 | { 39 | return new static($name); 40 | } 41 | 42 | /** @return $this */ 43 | public function addInterface(InterfaceType $interfaceType): self 44 | { 45 | $this->interfaces[] = $interfaceType; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @param array|callable():array $fields 52 | * 53 | * @return $this 54 | */ 55 | public function setFields(callable|array $fields): self 56 | { 57 | $this->fields = $fields; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @param FieldDefinition|FieldDefinitionConfig $field 64 | * 65 | * @return $this 66 | */ 67 | public function addField(FieldDefinition|array $field): self 68 | { 69 | if (is_callable($this->fields)) { 70 | $originalFields = $this->fields; 71 | $closure = static function () use ($field, $originalFields): array { 72 | $originalFields = $originalFields(); 73 | $originalFields[] = $field; 74 | 75 | return $originalFields; 76 | }; 77 | $this->fields = $closure; 78 | } else { 79 | $this->fields[] = $field; 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @param callable(mixed, array, mixed, ResolveInfo) : mixed $fieldResolver 87 | * 88 | * @return $this 89 | */ 90 | public function setFieldResolver(callable $fieldResolver): self 91 | { 92 | $this->fieldResolver = $fieldResolver; 93 | 94 | return $this; 95 | } 96 | 97 | /** @phpstan-return ObjectConfig */ 98 | public function build(): array 99 | { 100 | return [ 101 | 'name' => $this->name, 102 | 'description' => $this->description, 103 | 'interfaces' => $this->interfaces, 104 | 'fields' => $this->fields, 105 | 'resolveField' => $this->fieldResolver, 106 | ]; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Builder/TypeBuilder.php: -------------------------------------------------------------------------------- 1 | description = $description; 17 | 18 | return $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Builder/UnionBuilder.php: -------------------------------------------------------------------------------- 1 | |callable(): iterable 20 | */ 21 | class UnionBuilder extends TypeBuilder 22 | { 23 | /** @phpstan-var ResolveType|null */ 24 | private $resolveType = null; 25 | 26 | /** @phpstan-var Types */ 27 | private $types; 28 | 29 | /** @phpstan-param Types $types */ 30 | final private function __construct(iterable|callable $types, private string|null $name = null) 31 | { 32 | $this->types = $types; 33 | } 34 | 35 | /** 36 | * @phpstan-param Types $types 37 | * 38 | * @return static 39 | */ 40 | public static function create(string|null $name, iterable|callable $types): self 41 | { 42 | return new static($types, $name); 43 | } 44 | 45 | /** 46 | * @phpstan-param ResolveType $resolveType 47 | * 48 | * @return $this 49 | */ 50 | public function setResolveType(callable $resolveType): self 51 | { 52 | $this->resolveType = $resolveType; 53 | 54 | return $this; 55 | } 56 | 57 | /** @phpstan-return UnionConfig */ 58 | public function build(): array 59 | { 60 | return [ 61 | 'name' => $this->name, 62 | 'description' => $this->description, 63 | 'types' => $this->types, 64 | 'resolveType' => $this->resolveType, 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Error/Error.php: -------------------------------------------------------------------------------- 1 | getPrevious() instanceof Error) { 18 | $arrayError['extensions']['type'] = $exception->getPrevious()->getType(); 19 | } 20 | 21 | return $arrayError; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgument.php: -------------------------------------------------------------------------------- 1 |