├── .editorconfig ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Executor │ ├── ExecutionContext.php │ ├── Executor.php │ └── Values.php ├── GraphQL.php ├── Language │ ├── Lexer.php │ ├── Location.php │ ├── Node.php │ ├── Node │ │ ├── Argument.php │ │ ├── ArrayValue.php │ │ ├── BooleanValue.php │ │ ├── DefinitionInterface.php │ │ ├── Directive.php │ │ ├── Document.php │ │ ├── EnumValue.php │ │ ├── Field.php │ │ ├── FloatValue.php │ │ ├── FragmentDefinition.php │ │ ├── FragmentSpread.php │ │ ├── InlineFragment.php │ │ ├── IntValue.php │ │ ├── ListType.php │ │ ├── Name.php │ │ ├── NamedType.php │ │ ├── NonNullType.php │ │ ├── ObjectField.php │ │ ├── ObjectValue.php │ │ ├── OperationDefinition.php │ │ ├── SelectionInterface.php │ │ ├── SelectionSet.php │ │ ├── StringValue.php │ │ ├── TypeInterface.php │ │ ├── ValueInterface.php │ │ ├── Variable.php │ │ └── VariableDefinition.php │ ├── Parser.php │ ├── Source.php │ ├── SourceLocation.php │ └── Token.php ├── Schema.php ├── Type │ ├── Definition │ │ ├── EnumValueDefinition.php │ │ ├── FieldArgument.php │ │ ├── FieldDefinition.php │ │ ├── InputObjectField.php │ │ └── Types │ │ │ ├── AbstractType.php │ │ │ ├── AbstractTypeInterface.php │ │ │ ├── CompositeTypeInterface.php │ │ │ ├── EnumType.php │ │ │ ├── InputObjectType.php │ │ │ ├── InputTypeInterface.php │ │ │ ├── InterfaceType.php │ │ │ ├── LeafTypeInterface.php │ │ │ ├── ListModifier.php │ │ │ ├── ModifierInterface.php │ │ │ ├── NonNullModifier.php │ │ │ ├── NullableTypeInterface.php │ │ │ ├── ObjectType.php │ │ │ ├── OutputTypeInterface.php │ │ │ ├── ScalarType.php │ │ │ ├── ScalarTypeInterface.php │ │ │ ├── Scalars │ │ │ ├── BooleanType.php │ │ │ ├── FloatType.php │ │ │ ├── IdType.php │ │ │ ├── IntType.php │ │ │ └── StringType.php │ │ │ ├── Type.php │ │ │ ├── TypeInterface.php │ │ │ ├── UnionType.php │ │ │ └── UnmodifiedTypeInterface.php │ ├── Directives │ │ ├── Directive.php │ │ ├── DirectiveInterface.php │ │ ├── IncludeDirective.php │ │ └── SkipDirective.php │ └── Introspection.php └── Utility │ └── TypeInfo.php └── tests ├── Executor ├── DirectivesTest.php ├── ExecutorSchemaTest.php ├── ExecutorTest.php ├── InputObjectTest.php ├── ListsTest.php ├── NonNullTest.php └── UnionInterfaceTest.php ├── Language ├── LexerTest.php ├── ParserTest.php └── kitchen-sink.graphql ├── StarWarsData.php ├── StarWarsIntrospectionTest.php ├── StarWarsQueryTest.php ├── StarWarsSchema.php └── Type ├── CoercionTest.php ├── DefinitionTest.php └── IntrospectionTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = space 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | build -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | 4 | checks: 5 | php: 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | 20 | tools: 21 | external_code_coverage: 22 | timeout: 600 23 | runs: 3 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - hhvm 9 | 10 | matrix: 11 | allow_failures: 12 | - php: 7.0 13 | include: 14 | - php: 5.4 15 | env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' 16 | 17 | before_script: 18 | - travis_retry composer self-update 19 | - travis_retry composer install --no-interaction --prefer-source --dev 20 | 21 | script: 22 | - phpunit --coverage-text --coverage-clover=coverage.clover 23 | 24 | after_script: 25 | - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover; fi -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sebastian Siemssen 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL PHP 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 7 | [![Quality Score][ico-code-quality]][link-code-quality] 8 | [![Total Downloads][ico-downloads]][link-downloads] 9 | 10 | PSR-4 compliant implementation of Facebook's GraphQL parser reference implementation in PHP. 11 | 12 | ## Install 13 | 14 | Via Composer 15 | 16 | ``` bash 17 | $ composer require fubhy/graphql-php 18 | ``` 19 | 20 | ## Usage 21 | 22 | To be done ... 23 | 24 | ## Testing 25 | 26 | ``` bash 27 | $ composer test 28 | ``` 29 | 30 | ## Contributing 31 | 32 | To be done ... 33 | 34 | ## Security 35 | 36 | If you discover any security related issues, please email fubhy@fubhy.com instead of using the issue tracker. 37 | 38 | ## License 39 | 40 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 41 | 42 | [ico-version]: https://img.shields.io/packagist/v/fubhy/graphql-php.svg?style=flat-square 43 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 44 | [ico-travis]: https://img.shields.io/travis/fubhy/graphql-php/master.svg?style=flat-square 45 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/fubhy/graphql-php.svg?style=flat-square 46 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/fubhy/graphql-php.svg?style=flat-square 47 | [ico-downloads]: https://img.shields.io/packagist/dt/fubhy/graphql-php.svg?style=flat-square 48 | 49 | [link-packagist]: https://packagist.org/packages/fubhy/graphql-php 50 | [link-travis]: https://travis-ci.org/fubhy/graphql-php 51 | [link-scrutinizer]: https://scrutinizer-ci.com/g/fubhy/graphql-php/code-structure 52 | [link-code-quality]: https://scrutinizer-ci.com/g/fubhy/graphql-php 53 | [link-downloads]: https://packagist.org/packages/fubhy/graphql-php -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fubhy/graphql-php", 3 | "description": "A GraphQL parser for PHP.", 4 | "keywords": [ 5 | "graphql", 6 | "parser", 7 | "relay", 8 | "facebook" 9 | ], 10 | "homepage": "https://github.com/fubhy/graphql-php", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Sebastian Siemssen", 15 | "email": "fubhy@fubhy.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.3.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "~4.7", 23 | "scrutinizer/ocular": "~1.1" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Fubhy\\GraphQL\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Fubhy\\GraphQL\\Tests\\": "tests/" 33 | } 34 | }, 35 | "minimum-stability": "dev", 36 | "scripts": { 37 | "test": "phpunit" 38 | }, 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "1.0-dev" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Executor/ExecutionContext.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 59 | $this->fragments = $fragments; 60 | $this->root = $root; 61 | $this->operation = $operation; 62 | $this->variables = $variables; 63 | $this->errors = $errors ?: []; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Executor/Values.php: -------------------------------------------------------------------------------- 1 | get('variable')->get('name')->get('value'); 38 | $values[$variable] = self::getvariableValue($schema, $ast, isset($inputs[$variable]) ? $inputs[$variable] : NULL); 39 | } 40 | return $values; 41 | } 42 | 43 | /** 44 | * Prepares an object map of argument values given a list of argument 45 | * definitions and list of argument AST nodes. 46 | * 47 | * @param $arguments 48 | * @param $asts 49 | * @param $variables 50 | * 51 | * @return array|null 52 | */ 53 | public static function getArgumentValues($arguments, $asts, $variables) 54 | { 55 | if (!$arguments || count($arguments) === 0) { 56 | return NULL; 57 | } 58 | 59 | $map = array_reduce($asts, function ($carry, $argument) { 60 | $carry[$argument->get('name')->get('value')] = $argument; 61 | return $carry; 62 | }, []); 63 | 64 | $result = []; 65 | foreach ($arguments as $argument) { 66 | $name = $argument->getName(); 67 | $value = isset($map[$name]) ? $map[$name]->get('value') : NULL; 68 | $result[$name] = self::coerceValueAST($argument->getType(), $value, $variables); 69 | } 70 | return $result; 71 | } 72 | 73 | /** 74 | * @param DirectiveInterface $definition 75 | * @param $directives 76 | * @param $variables 77 | * 78 | * @return array|mixed|null|string 79 | */ 80 | public static function getDirectiveValue(DirectiveInterface $definition, $directives, $variables) 81 | { 82 | $ast = NULL; 83 | if ($directives) { 84 | foreach ($directives as $directive) { 85 | if ($directive->get('name')->get('value') === $definition->getName()) { 86 | $ast = $directive; 87 | break; 88 | } 89 | } 90 | } 91 | if ($ast) { 92 | if (!$definition->getType()) { 93 | return NULL; 94 | } 95 | 96 | return self::coerceValueAST($definition->getType(), $ast->get('value'), $variables); 97 | } 98 | } 99 | 100 | /** 101 | * Given a variable definition, and any value of input, return a value which 102 | * adheres to the variable definition, or throw an error. 103 | * 104 | * @param Schema $schema 105 | * @param VariableDefinition $definition 106 | * @param $input 107 | * 108 | * @return array|mixed|null|string 109 | * 110 | * @throws \Exception 111 | */ 112 | protected static function getVariableValue(Schema $schema, VariableDefinition $definition, $input) 113 | { 114 | $type = TypeInfo::typeFromAST($schema, $definition->get('type')); 115 | if (!$type) { 116 | return NULL; 117 | } 118 | 119 | if (self::isValidValue($type, $input)) { 120 | if (!isset($input)) { 121 | $default = $definition->get('defaultValue'); 122 | 123 | if ($default) { 124 | return self::coerceValueAST($type, $default); 125 | } 126 | } 127 | 128 | return self::coerceValue($type, $input); 129 | } 130 | 131 | // @todo Fix exception message once printer is ported. 132 | throw new \Exception(sprintf('Variable $%s expected value of different type.', $definition->get('variable')->get('name')->get('value'))); 133 | } 134 | 135 | /** 136 | * Given a type and any value, return true if that value is valid. 137 | * 138 | * @param TypeInterface $type 139 | * @param $value 140 | * 141 | * @return bool 142 | */ 143 | protected static function isValidValue(TypeInterface $type, $value) 144 | { 145 | if ($type instanceof NonNullModifier) { 146 | if (NULL === $value) { 147 | return FALSE; 148 | } 149 | 150 | return self::isValidValue($type->getWrappedType(), $value); 151 | } 152 | 153 | if ($value === NULL) { 154 | return TRUE; 155 | } 156 | 157 | if ($type instanceof ListModifier) { 158 | $itemType = $type->getWrappedType(); 159 | if (is_array($value)) { 160 | foreach ($value as $item) { 161 | if (!self::isValidValue($itemType, $item)) { 162 | return FALSE; 163 | } 164 | } 165 | 166 | return TRUE; 167 | } else { 168 | return self::isValidValue($itemType, $value); 169 | } 170 | } 171 | 172 | if ($type instanceof InputObjectType) { 173 | $fields = $type->getFields(); 174 | foreach ($fields as $fieldName => $field) { 175 | if (!self::isValidValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : NULL)) { 176 | return FALSE; 177 | } 178 | } 179 | 180 | return TRUE; 181 | } 182 | 183 | if ($type instanceof ScalarType || $type instanceof EnumType) { 184 | return NULL !== $type->coerce($value); 185 | } 186 | 187 | return FALSE; 188 | } 189 | 190 | /** 191 | * Given a type and any value, return a runtime value coerced to match the 192 | * type. 193 | * 194 | * @param TypeInterface $type 195 | * @param $value 196 | * 197 | * @return array|mixed|null|string 198 | */ 199 | protected static function coerceValue(TypeInterface $type, $value) 200 | { 201 | if ($type instanceof NonNullModifier) { 202 | // Note: we're not checking that the result of coerceValue is non-null. 203 | // We only call this function after calling isValidValue. 204 | return self::coerceValue($type->getWrappedType(), $value); 205 | } 206 | 207 | if (!isset($value)) { 208 | return NULL; 209 | } 210 | 211 | if ($type instanceof ListModifier) { 212 | $itemType = $type->getWrappedType(); 213 | if (is_array($value)) { 214 | return array_map(function ($item) use ($itemType) { 215 | return Values::coerceValue($itemType, $item); 216 | }, $value); 217 | } else { 218 | return [self::coerceValue($itemType, $value)]; 219 | } 220 | } 221 | 222 | if ($type instanceof InputObjectType) { 223 | $fields = $type->getFields(); 224 | $object = []; 225 | foreach ($fields as $fieldName => $field) { 226 | $fieldValue = self::coerceValue($field->getType(), $value[$fieldName]); 227 | $object[$fieldName] = $fieldValue === NULL ? $field->getDefaultValue() : $fieldValue; 228 | } 229 | 230 | return $object; 231 | } 232 | 233 | if ($type instanceof ScalarType || $type instanceof EnumType) { 234 | $coerced = $type->coerce($value); 235 | if (NULL !== $coerced) { 236 | return $coerced; 237 | } 238 | } 239 | 240 | return NULL; 241 | } 242 | 243 | /** 244 | * Given a type and a value AST node known to match this type, build a 245 | * runtime value. 246 | * 247 | * @param TypeInterface $type 248 | * @param $ast 249 | * @param null $variables 250 | * 251 | * @return array|mixed|null|string 252 | */ 253 | protected static function coerceValueAST(TypeInterface $type, $ast, $variables = NULL) 254 | { 255 | if ($type instanceof NonNullModifier) { 256 | // Note: we're not checking that the result of coerceValueAST is non-null. 257 | // We're assuming that this query has been validated and the value used 258 | // here is of the correct type. 259 | return self::coerceValueAST($type->getWrappedType(), $ast, $variables); 260 | } 261 | 262 | if (!$ast) { 263 | return NULL; 264 | } 265 | 266 | if ($ast::KIND === Node::KIND_VARIABLE) { 267 | $variableName = $ast->get('name')->get('value'); 268 | 269 | if (!isset($variables, $variables[$variableName])) { 270 | return NULL; 271 | } 272 | 273 | // Note: we're not doing any checking that this variable is correct. We're 274 | // assuming that this query has been validated and the variable usage here 275 | // is of the correct type. 276 | return $variables[$variableName]; 277 | } 278 | 279 | if ($type instanceof ListModifier) { 280 | $itemType = $type->getWrappedType(); 281 | 282 | if ($ast::KIND === Node::KIND_ARRAY_VALUE) { 283 | $tmp = []; 284 | foreach ($ast->get('values') as $itemAST) { 285 | $tmp[] = self::coerceValueAST($itemType, $itemAST, $variables); 286 | } 287 | 288 | return $tmp; 289 | } else { 290 | return [self::coerceValueAST($itemType, $ast, $variables)]; 291 | } 292 | } 293 | 294 | if ($type instanceof InputObjectType) { 295 | $fields = $type->getFields(); 296 | 297 | if ($ast::KIND !== Node::KIND_OBJECT_VALUE) { 298 | return NULL; 299 | } 300 | 301 | $asts = array_reduce($ast->get('fields'), function ($carry, $field) { 302 | $carry[$field->get('name')->get('value')] = $field; 303 | return $carry; 304 | }, []); 305 | 306 | $object = []; 307 | foreach ($fields as $name => $item) { 308 | $field = $asts[$name]; 309 | $fieldValue = self::coerceValueAST($item->getType(), $field ? $field->get('value') : NULL, $variables); 310 | $object[$name] = $fieldValue === NULL ? $item->getDefaultValue() : $fieldValue; 311 | } 312 | 313 | return $object; 314 | } 315 | 316 | if ($type instanceof ScalarType || $type instanceof EnumType) { 317 | $coerced = $type->coerceLiteral($ast); 318 | 319 | if (isset($coerced)) { 320 | return $coerced; 321 | } 322 | } 323 | 324 | return NULL; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/GraphQL.php: -------------------------------------------------------------------------------- 1 | parse($source); 27 | return Executor::execute($schema, $root, $ast, $operation, $variables); 28 | } catch (\Exception $exception) { 29 | return ['errors' => $exception->getMessage()]; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Language/Location.php: -------------------------------------------------------------------------------- 1 | start = $start; 36 | $this->end = $end; 37 | $this->source = $source; 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getStart() 44 | { 45 | return $this->start; 46 | } 47 | 48 | /** 49 | * @return int 50 | */ 51 | public function getEnd() 52 | { 53 | return $this->end; 54 | } 55 | 56 | /** 57 | * @return Source|null 58 | */ 59 | public function getSource() 60 | { 61 | return $this->source; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Language/Node.php: -------------------------------------------------------------------------------- 1 | location = $location; 53 | $this->attributes = $attributes; 54 | } 55 | 56 | /** 57 | * @return Location|null 58 | */ 59 | public function getLocation() 60 | { 61 | return $this->location; 62 | } 63 | 64 | /** 65 | * @param string $key 66 | * 67 | * @return bool 68 | */ 69 | public function has($key) 70 | { 71 | return array_key_exists($key, $this->attributes); 72 | } 73 | 74 | /** 75 | * @param string $key 76 | * @param null $default 77 | * 78 | * @return mixed 79 | */ 80 | public function get($key, $default = NULL) 81 | { 82 | if (array_key_exists($key, $this->attributes)) { 83 | return $this->attributes[$key]; 84 | } 85 | 86 | return $default; 87 | } 88 | 89 | /** 90 | * @param string $key 91 | * @param mixed $value 92 | * 93 | * @return $this 94 | */ 95 | public function set($key, $value) 96 | { 97 | $this->attributes[$key] = $value; 98 | return $this; 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function __toString() 105 | { 106 | return json_encode($this); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Language/Node/Argument.php: -------------------------------------------------------------------------------- 1 | $name, 23 | 'value' => $value, 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/Node/ArrayValue.php: -------------------------------------------------------------------------------- 1 | $values]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/BooleanValue.php: -------------------------------------------------------------------------------- 1 | $value]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/DefinitionInterface.php: -------------------------------------------------------------------------------- 1 | $name, 23 | 'arguments' => $arguments, 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/Node/Document.php: -------------------------------------------------------------------------------- 1 | $definitions]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Language/Node/EnumValue.php: -------------------------------------------------------------------------------- 1 | $value]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/Field.php: -------------------------------------------------------------------------------- 1 | $name, 32 | 'alias' => $alias, 33 | 'arguments' => $arguments, 34 | 'directives' => $directives, 35 | 'selectionSet' => $selectionSet, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Language/Node/FloatValue.php: -------------------------------------------------------------------------------- 1 | $value]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/FragmentDefinition.php: -------------------------------------------------------------------------------- 1 | $name, 30 | 'typeCondition' => $typeCondition, 31 | 'directives' => $directives, 32 | 'selectionSet' => $selectionSet, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Language/Node/FragmentSpread.php: -------------------------------------------------------------------------------- 1 | $name, 23 | 'directives' => $directives, 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/Node/InlineFragment.php: -------------------------------------------------------------------------------- 1 | $typeCondition, 28 | 'directives' => $directives, 29 | 'selectionSet' => $selectionSet, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Language/Node/IntValue.php: -------------------------------------------------------------------------------- 1 | $value]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/ListType.php: -------------------------------------------------------------------------------- 1 | $type]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/Name.php: -------------------------------------------------------------------------------- 1 | $value]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/NamedType.php: -------------------------------------------------------------------------------- 1 | $name]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/NonNullType.php: -------------------------------------------------------------------------------- 1 | $type]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/Node/ObjectField.php: -------------------------------------------------------------------------------- 1 | $name, 23 | 'value' => $value, 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Language/Node/ObjectValue.php: -------------------------------------------------------------------------------- 1 | $fields]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/OperationDefinition.php: -------------------------------------------------------------------------------- 1 | $operation, 32 | 'name' => $name, 33 | 'variableDefinitions' => $variableDefinitions, 34 | 'directives' => $directives, 35 | 'selectionSet' => $selectionSet, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Language/Node/SelectionInterface.php: -------------------------------------------------------------------------------- 1 | $selections]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/StringValue.php: -------------------------------------------------------------------------------- 1 | $value]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/TypeInterface.php: -------------------------------------------------------------------------------- 1 | $name]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Language/Node/VariableDefinition.php: -------------------------------------------------------------------------------- 1 | $variable, 28 | 'type' => $type, 29 | 'defaultValue' => $defaultValue, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Language/Source.php: -------------------------------------------------------------------------------- 1 | body = $body; 38 | $this->name = $name; 39 | $this->length = mb_strlen($body); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getBody() 46 | { 47 | return $this->body; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getName() 54 | { 55 | return $this->name; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getLength() 62 | { 63 | return $this->length; 64 | } 65 | 66 | /** 67 | * Takes a Source and a UTF-8 character offset, and returns the 68 | * corresponding line and column as a SourceLocation. 69 | * 70 | * @param $position 71 | * 72 | * @return \Fubhy\GraphQL\Language\SourceLocation 73 | */ 74 | public function getLocation($position) 75 | { 76 | $pattern = '/\r\n|[\n\r\u2028\u2029]/g'; 77 | $subject = mb_substr($this->body, 0, $position, 'UTF-8'); 78 | 79 | preg_match_all($pattern, $subject, $matches, PREG_OFFSET_CAPTURE); 80 | $location = array_reduce($matches[0], function ($carry, $match) use ($position) { 81 | return [ 82 | $carry[0] + 1, 83 | $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8')) 84 | ]; 85 | }, [1, $position + 1]); 86 | 87 | return new SourceLocation($location[0], $location[1]); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Language/SourceLocation.php: -------------------------------------------------------------------------------- 1 | line = $line; 29 | $this->column = $column; 30 | } 31 | 32 | /** 33 | * @return int 34 | */ 35 | public function getLine() 36 | { 37 | return $this->line; 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getColumn() 44 | { 45 | return $this->column; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Language/Token.php: -------------------------------------------------------------------------------- 1 | type = $type; 73 | $this->start = $start; 74 | $this->end = $end; 75 | $this->value = $value; 76 | } 77 | 78 | /** 79 | * Gets the token type. 80 | * 81 | * @return int 82 | * The token type 83 | */ 84 | public function getType() 85 | { 86 | return $this->type; 87 | } 88 | 89 | /** 90 | * Gets the start position. 91 | * 92 | * @return int 93 | * The start position. 94 | */ 95 | public function getStart() 96 | { 97 | return $this->start; 98 | } 99 | 100 | /** 101 | * Gets the end position. 102 | * 103 | * @return int 104 | * The end position. 105 | */ 106 | public function getEnd() 107 | { 108 | return $this->end; 109 | } 110 | 111 | /** 112 | * Gets the token value. 113 | * 114 | * @return string|null 115 | * The token value 116 | */ 117 | public function getValue() 118 | { 119 | return $this->value; 120 | } 121 | 122 | /** 123 | * Gets the token value. 124 | * 125 | * @return string|null 126 | * The token value 127 | */ 128 | public function getDescription() 129 | { 130 | return self::typeToString($this->type) . ($this->value ? ' "' . $this->value . '"' : ''); 131 | } 132 | 133 | /** 134 | * Returns a string representation of the token. 135 | * 136 | * @return string 137 | * A string representation of the token 138 | */ 139 | public function __toString() 140 | { 141 | $description = self::typeToString($this->type); 142 | return $this->value ? sprintf('%s "%s"', $description, $this->value) : $description; 143 | } 144 | 145 | /** 146 | * Returns the constant representation (internal) of a given type. 147 | * 148 | * @param int $type 149 | * The type as an integer 150 | * 151 | * @return string 152 | * The string representation 153 | */ 154 | public static function typeToString($type) 155 | { 156 | switch ($type) { 157 | case self::EOF_TYPE: 158 | return 'EOF'; 159 | case self::BANG_TYPE: 160 | return '!'; 161 | case self::DOLLAR_TYPE: 162 | return '$'; 163 | case self::PAREN_L_TYPE: 164 | return '('; 165 | case self::PAREN_R_TYPE: 166 | return ')'; 167 | case self::SPREAD_TYPE: 168 | return '...'; 169 | case self::COLON_TYPE: 170 | return ':'; 171 | case self::EQUALS_TYPE: 172 | return '='; 173 | case self::AT_TYPE: 174 | return '@'; 175 | case self::BRACKET_L_TYPE: 176 | return '['; 177 | case self::BRACKET_R_TYPE: 178 | return ']'; 179 | case self::BRACE_L_TYPE: 180 | return '{'; 181 | case self::PIPE_TYPE: 182 | return '|'; 183 | case self::BRACE_R_TYPE: 184 | return '}'; 185 | case self::NAME_TYPE: 186 | return 'Name'; 187 | case self::VARIABLE_TYPE: 188 | return 'Variable'; 189 | case self::INT_TYPE: 190 | return 'Int'; 191 | case self::FLOAT_TYPE: 192 | return 'Float'; 193 | case self::STRING_TYPE: 194 | return 'String'; 195 | default: 196 | throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Schema.php: -------------------------------------------------------------------------------- 1 | queryType = $queryType; 53 | $this->mutationType = $mutationType; 54 | } 55 | 56 | /** 57 | * @return \Fubhy\GraphQL\Type\Definition\Types\ObjectType 58 | */ 59 | public function getQueryType() 60 | { 61 | return $this->queryType; 62 | } 63 | 64 | /** 65 | * @return \Fubhy\GraphQL\Type\Definition\Types\ObjectType|null 66 | */ 67 | public function getMutationType() 68 | { 69 | return $this->mutationType; 70 | } 71 | 72 | /** 73 | * @param string $name 74 | * 75 | * @return \Fubhy\GraphQL\Type\Directives\DirectiveInterface|null 76 | */ 77 | public function getDirective($name) 78 | { 79 | foreach ($this->getDirectives() as $directive) { 80 | if ($directive->getName() === $name) { 81 | return $directive; 82 | } 83 | } 84 | return NULL; 85 | } 86 | 87 | /** 88 | * @return \Fubhy\GraphQL\Type\Directives\DirectiveInterface[] 89 | */ 90 | public function getDirectives() 91 | { 92 | if (!isset($this->directives)) { 93 | $include = Directive::includeDirective(); 94 | $skip = Directive::skipDirective(); 95 | 96 | $this->directives = [ 97 | $include->getName() => $include, 98 | $skip->getName() => $skip, 99 | ]; 100 | } 101 | 102 | return $this->directives; 103 | } 104 | 105 | /** 106 | * @return \Fubhy\GraphQL\Type\Definition\Types\TypeInterface[] 107 | */ 108 | public function getTypeMap() 109 | { 110 | if (!isset($this->typeMap)) { 111 | $input = [$this->getQueryType(), $this->getMutationType(), Introspection::schema()]; 112 | $this->typeMap = array_reduce($input, [$this, 'typeMapReducer'], []); 113 | 114 | $this->typeMap['Boolean'] = Type::booleanType(); 115 | $this->typeMap['Float'] = Type::floatType(); 116 | $this->typeMap['Id'] = Type::idType(); 117 | $this->typeMap['Integer'] = Type::intType(); 118 | $this->typeMap['String'] = Type::stringType(); 119 | } 120 | 121 | return $this->typeMap; 122 | } 123 | 124 | /** 125 | * @param string $name 126 | * 127 | * @return \Fubhy\GraphQL\Type\Definition\Types\TypeInterface 128 | */ 129 | public function getType($name) 130 | { 131 | $map = $this->getTypeMap(); 132 | return isset($map[$name]) ? $map[$name] : NULL; 133 | } 134 | 135 | /** 136 | * @param \Fubhy\GraphQL\Type\Definition\Types\TypeInterface[] $map 137 | * @param mixed $type 138 | * 139 | * @return \Fubhy\GraphQL\Type\Definition\Types\TypeInterface[] 140 | */ 141 | protected function typeMapReducer(array $map, $type) 142 | { 143 | if ($type instanceof ModifierInterface) { 144 | return $this->typeMapReducer($map, $type->getWrappedType()); 145 | } 146 | 147 | if (!$type instanceof TypeInterface) { 148 | return $map; 149 | } 150 | 151 | if (!empty($map[$type->getName()])) { 152 | if ($type instanceof ObjectType || $type instanceof InterfaceType) { 153 | $type->setFields($map[$type->getName()]->getFields()); 154 | } 155 | 156 | return $map; 157 | } 158 | 159 | $reducedMap = array_merge($map, [$type->getName() => $type]); 160 | if ($type instanceof InterfaceType || $type instanceof UnionType) { 161 | $reducedMap = array_reduce( 162 | $type->getPossibleTypes(), [$this, 'typeMapReducer'], $reducedMap 163 | ); 164 | } 165 | 166 | if ($type instanceof ObjectType) { 167 | $reducedMap = array_reduce( 168 | $type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap 169 | ); 170 | } 171 | 172 | if ($type instanceof ObjectType || $type instanceof InterfaceType) { 173 | foreach ($type->getFields() as $fieldName => $field) { 174 | $args = $field->getArguments(); 175 | $reducedMap = array_reduce(array_map(function (FieldArgument $arg) { 176 | return $arg->getType(); 177 | }, $args), [$this, 'typeMapReducer'], $reducedMap); 178 | 179 | $reducedMap = $this->typeMapReducer($reducedMap, $field->getType()); 180 | } 181 | } 182 | 183 | return $reducedMap; 184 | } 185 | 186 | /** 187 | * Re-populate static properties when de-serializing. 188 | */ 189 | public function __wakeup() { 190 | Type::intType(); 191 | Type::booleanType(); 192 | Type::floatType(); 193 | Type::idType(); 194 | Type::stringType(); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Type/Definition/EnumValueDefinition.php: -------------------------------------------------------------------------------- 1 | name = $config['name']; 35 | $this->value = $config['value']; 36 | $this->description = isset($config['description']) ? $config['description'] : NULL; 37 | 38 | if (isset($config['deprecationReason'])) { 39 | $this->deprecationReason = $config['deprecationReason']; 40 | } 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getName() 47 | { 48 | return $this->name; 49 | } 50 | 51 | /** 52 | * @return string|null 53 | */ 54 | public function getDescription() 55 | { 56 | return $this->description; 57 | } 58 | 59 | /** 60 | * @return mixed 61 | */ 62 | public function getValue() 63 | { 64 | return $this->value; 65 | } 66 | 67 | /** 68 | * @return string|null 69 | */ 70 | public function getDeprecationReason() 71 | { 72 | return $this->deprecationReason; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Type/Definition/FieldArgument.php: -------------------------------------------------------------------------------- 1 | name = $config['name']; 35 | $this->type = $config['type']; 36 | $this->description = isset($config['description']) ? $config['description'] : NULL; 37 | 38 | if (isset($config['defaultValue'])) { 39 | $this->defaultValue = $config['defaultValue']; 40 | } 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getName() 47 | { 48 | return $this->name; 49 | } 50 | 51 | /** 52 | * @return string|null 53 | */ 54 | public function getDescription() 55 | { 56 | return $this->description; 57 | } 58 | 59 | /** 60 | * @return \Fubhy\GraphQL\Type\Definition\Types\InputTypeInterface 61 | */ 62 | public function getType() 63 | { 64 | if (is_callable($this->type)) { 65 | return call_user_func($this->type); 66 | } 67 | 68 | return $this->type; 69 | } 70 | 71 | /** 72 | * @return mixed|null 73 | */ 74 | public function getDefaultValue() 75 | { 76 | return $this->defaultValue; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Type/Definition/FieldDefinition.php: -------------------------------------------------------------------------------- 1 | name = $config['name']; 55 | $this->type = $config['type']; 56 | $this->description = isset($config['description']) ? $config['description'] : NULL; 57 | $this->resolve = isset($config['resolve']) ? $config['resolve'] : NULL; 58 | 59 | if (isset($config['args'])) { 60 | $this->args = $config['args']; 61 | } 62 | 63 | if (isset($config['deprecationReason'])) { 64 | $this->deprecationReason = $config['deprecationReason']; 65 | } 66 | 67 | if (isset($config['resolveData'])) { 68 | $this->resolveData = $config['resolveData']; 69 | } 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getName() 76 | { 77 | return $this->name; 78 | } 79 | 80 | /** 81 | * @return string|null 82 | */ 83 | public function getDescription() 84 | { 85 | return $this->description; 86 | } 87 | 88 | /** 89 | * @return \Fubhy\GraphQL\Type\Definition\Types\OutputTypeInterface 90 | */ 91 | public function getType() 92 | { 93 | if (is_callable($this->type)) { 94 | $this->type = call_user_func($this->type); 95 | } 96 | 97 | return $this->type; 98 | } 99 | 100 | /** 101 | * @return callable|null 102 | */ 103 | public function getResolveCallback() 104 | { 105 | return $this->resolve; 106 | } 107 | 108 | /** 109 | * @return mixed 110 | */ 111 | public function getResolveData() 112 | { 113 | return $this->resolveData; 114 | } 115 | 116 | /** 117 | * @return string|null 118 | */ 119 | public function getDeprecationReason() 120 | { 121 | return $this->deprecationReason; 122 | } 123 | 124 | /** 125 | * @return \Fubhy\GraphQL\Type\Definition\FieldArgument[] 126 | */ 127 | public function getArguments() 128 | { 129 | if (!isset($this->argMap)) { 130 | $this->argMap = []; 131 | foreach ($this->args as $name => $arg) { 132 | if (!isset($arg['name'])) { 133 | $arg['name'] = $name; 134 | } 135 | 136 | $this->argMap[$name] = new FieldArgument($arg); 137 | } 138 | } 139 | 140 | return $this->argMap; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Type/Definition/InputObjectField.php: -------------------------------------------------------------------------------- 1 | name = $config['name']; 35 | $this->type = $config['type']; 36 | $this->description = isset($config['description']) ? $config['description'] : NULL; 37 | $this->defaultValue = isset($config['defaultValue']) ? $config['defaultValue'] : NULL; 38 | } 39 | 40 | /** 41 | * @return \Fubhy\GraphQL\Type\Definition\Types\InputTypeInterface 42 | */ 43 | public function getType() 44 | { 45 | if (is_callable($this->type)) { 46 | return call_user_func($this->type); 47 | } 48 | 49 | return $this->type; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getName() 56 | { 57 | return $this->name; 58 | } 59 | 60 | /** 61 | * @return string|null 62 | */ 63 | public function getDescription() 64 | { 65 | return $this->description; 66 | } 67 | 68 | /** 69 | * @return mixed|null 70 | */ 71 | public function getDefaultValue() 72 | { 73 | return $this->defaultValue; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/AbstractType.php: -------------------------------------------------------------------------------- 1 | getPossibleTypes() as $type) { 17 | if ($isTypeOf = $type->isTypeOf($value)) { 18 | return $type; 19 | } 20 | 21 | if (!isset($isTypeOf)) { 22 | throw new \Exception(sprintf( 23 | 'Non-Object Type %s does not implement resolveType and Object ' . 24 | 'Type %s does not implement isTypeOf. There is no way to ' . 25 | 'determine if a value is of this type.', $this->getName(), $type->getName() 26 | )); 27 | } 28 | } 29 | 30 | return NULL; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/AbstractTypeInterface.php: -------------------------------------------------------------------------------- 1 | values = $values; 53 | } 54 | 55 | /** 56 | * @return \Fubhy\GraphQL\Type\Definition\EnumValueDefinition[] 57 | */ 58 | public function getValues() 59 | { 60 | if (!isset($this->valueMap)) { 61 | $this->valueMap = []; 62 | foreach ($this->values as $name => $value) { 63 | $value['name'] = $name; 64 | 65 | if (!array_key_exists('value', $value)) { 66 | $value['value'] = $name; 67 | } 68 | 69 | $this->valueMap[$name] = new EnumValueDefinition($value); 70 | } 71 | } 72 | 73 | return $this->valueMap; 74 | } 75 | 76 | /** 77 | * @param mixed $value 78 | * 79 | * @return string|null 80 | */ 81 | public function coerce($value) 82 | { 83 | $enumValue = $this->getValueLookup()[$value]; 84 | return $enumValue ? $enumValue->getName() : NULL; 85 | } 86 | 87 | /** 88 | * @param \Fubhy\GraphQL\Language\Node $value 89 | * 90 | * @return string|null 91 | */ 92 | public function coerceLiteral(Node $value) 93 | { 94 | if ($value instanceof EnumValue) { 95 | $key = $value->get('value'); 96 | if (($lookup = $this->getNameLookup()) && isset($lookup[$key])) { 97 | return $lookup[$key] ? $lookup[$key]->getName() : NULL; 98 | } 99 | } 100 | 101 | return NULL; 102 | } 103 | 104 | /** 105 | * @return \Fubhy\GraphQL\Type\Definition\EnumValueDefinition[] 106 | */ 107 | protected function getValueLookup() 108 | { 109 | if (!isset($this->valueLookup)) { 110 | $this->valueLookup = []; 111 | foreach ($this->getValues() as $value) { 112 | $this->valueLookup[$value->getValue()] = $value; 113 | } 114 | } 115 | 116 | return $this->valueLookup; 117 | } 118 | 119 | /** 120 | * @return \Fubhy\GraphQL\Type\Definition\EnumValueDefinition[] 121 | */ 122 | protected function getNameLookup() 123 | { 124 | if (!isset($this->nameLookup)) { 125 | $this->nameLookup = []; 126 | foreach ($this->getValues() as $value) { 127 | $this->nameLookup[$value->getName()] = $value; 128 | } 129 | } 130 | 131 | return $this->nameLookup; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/InputObjectType.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 39 | } 40 | 41 | /** 42 | * @return \Fubhy\GraphQL\Type\Definition\InputObjectField[] 43 | */ 44 | public function getFields() 45 | { 46 | if (!isset($this->fieldMap)) { 47 | $this->fieldMap = []; 48 | foreach ($this->fields as $name => $field) { 49 | if (!isset($field['name'])) { 50 | $field['name'] = $name; 51 | } 52 | 53 | $this->fieldMap[$name] = new InputObjectField($field); 54 | } 55 | } 56 | 57 | return $this->fieldMap; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/InputTypeInterface.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 50 | $this->typeResolver = $typeResolver; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getName() 57 | { 58 | return $this->name; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getDescription() 65 | { 66 | return $this->description; 67 | } 68 | 69 | /** 70 | * @return \Fubhy\GraphQL\Type\Definition\FieldDefinition[] 71 | */ 72 | public function getFields() 73 | { 74 | if (!isset($this->fieldMap)) { 75 | $this->fieldMap = []; 76 | foreach ($this->fields as $name => $field) { 77 | if (!isset($field['name'])) { 78 | $field['name'] = $name; 79 | } 80 | 81 | $this->fieldMap[$name] = new FieldDefinition($field); 82 | } 83 | 84 | unset($this->fields); 85 | } 86 | 87 | return $this->fieldMap; 88 | } 89 | 90 | /** 91 | * @param array $fields 92 | */ 93 | public function setFields(array $fields) { 94 | unset($this->fields); 95 | $this->fieldMap = $fields; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function getPossibleTypes() 102 | { 103 | return $this->types; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function addPossibleType(ObjectType $type) 110 | { 111 | $this->types = array_unique(array_merge($this->types, [$type])); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function isPossibleType(ObjectType $type) 118 | { 119 | return in_array($type, $this->types, TRUE); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function resolveType($value) 126 | { 127 | if (isset($this->typeResolver)) { 128 | return call_user_func($this->typeResolver, $value); 129 | } 130 | 131 | return $this->getTypeOf($value); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/LeafTypeInterface.php: -------------------------------------------------------------------------------- 1 | ofType = $ofType; 29 | } 30 | 31 | /** 32 | * @return \Fubhy\GraphQL\Type\Definition\Types\TypeInterface 33 | */ 34 | public function getWrappedType() 35 | { 36 | if (is_callable($this->ofType)) { 37 | $this->ofType = call_user_func($this->ofType); 38 | } 39 | 40 | return $this->ofType; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getName() 47 | { 48 | return '[' . (string) $this->getWrappedType() . ']'; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getDescription() 55 | { 56 | return NULL; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function __toString() 63 | { 64 | return $this->getName(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/ModifierInterface.php: -------------------------------------------------------------------------------- 1 | ofType = $ofType; 35 | } 36 | 37 | /** 38 | * @return \Fubhy\GraphQL\Type\Definition\Types\TypeInterface 39 | */ 40 | public function getWrappedType() 41 | { 42 | if (is_callable($this->ofType)) { 43 | $this->ofType = call_user_func($this->ofType); 44 | } 45 | 46 | return $this->ofType; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getName() 53 | { 54 | return (string) $this->getWrappedType() . '!'; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getDescription() 61 | { 62 | return NULL; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function __toString() 69 | { 70 | return $this->getName(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/NullableTypeInterface.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 53 | $this->interfaces = $interfaces; 54 | $this->isTypeOf = $isTypeOf; 55 | 56 | foreach ($this->interfaces as $interface) { 57 | $interface->addPossibleType($this); 58 | } 59 | } 60 | 61 | /** 62 | * @return \Fubhy\GraphQL\Type\Definition\FieldDefinition[] 63 | */ 64 | public function getFields() 65 | { 66 | if (!isset($this->fieldMap)) { 67 | $this->fieldMap = []; 68 | foreach ($this->fields as $name => $field) { 69 | if (!isset($field['name'])) { 70 | $field['name'] = $name; 71 | } 72 | 73 | $this->fieldMap[$name] = new FieldDefinition($field); 74 | } 75 | 76 | unset($this->fields); 77 | } 78 | 79 | return $this->fieldMap; 80 | } 81 | 82 | /** 83 | * @param array $fields 84 | */ 85 | public function setFields(array $fields) { 86 | unset($this->fields); 87 | $this->fieldMap = $fields; 88 | } 89 | 90 | /** 91 | * @param string $field 92 | * 93 | * @return \Fubhy\GraphQL\Type\Definition\FieldDefinition 94 | */ 95 | public function getField($field) 96 | { 97 | $fields = $this->getFields(); 98 | if (!isset($fields[$field])) { 99 | throw new \LogicException(sprintf('Undefined field %s.', $field)); 100 | } 101 | 102 | return $fields[$field]; 103 | } 104 | 105 | /** 106 | * @return \Fubhy\GraphQL\Type\Definition\Types\InterfaceType[] 107 | */ 108 | public function getInterfaces() 109 | { 110 | return isset($this->interfaces) ? $this->interfaces : []; 111 | } 112 | 113 | /** 114 | * @param mixed $value 115 | * 116 | * @return bool|null 117 | */ 118 | public function isTypeOf($value) 119 | { 120 | return isset($this->isTypeOf) ? call_user_func($this->isTypeOf, $value) : NULL; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/OutputTypeInterface.php: -------------------------------------------------------------------------------- 1 | get('value') : NULL; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/Scalars/FloatType.php: -------------------------------------------------------------------------------- 1 | get('value'); 32 | 33 | if ($num <= PHP_INT_MAX && $num >= -PHP_INT_MAX) { 34 | return $num; 35 | } 36 | } 37 | 38 | return NULL; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/Scalars/IdType.php: -------------------------------------------------------------------------------- 1 | get('value'); 32 | } 33 | 34 | return NULL; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/Scalars/IntType.php: -------------------------------------------------------------------------------- 1 | = -PHP_INT_MAX) || is_bool($value)) { 22 | return (int) $value; 23 | } 24 | 25 | return NULL; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function coerceLiteral(Node $node) 32 | { 33 | if ($node instanceof IntValue) { 34 | $num = $node->get('value'); 35 | 36 | if ($num <= PHP_INT_MAX && $num >= -PHP_INT_MAX) { 37 | return intval($num); 38 | } 39 | } 40 | 41 | return NULL; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/Scalars/StringType.php: -------------------------------------------------------------------------------- 1 | get('value') : NULL; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/Type.php: -------------------------------------------------------------------------------- 1 | name = $name; 120 | $this->description = $description; 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function getName() 127 | { 128 | return $this->name; 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | public function getDescription() 135 | { 136 | return $this->description; 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | public function __toString() 143 | { 144 | return $this->getName(); 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function isInputType() 151 | { 152 | return $this->getUnmodifiedType() instanceof InputTypeInterface; 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function isOutputType() 159 | { 160 | return $this->getUnmodifiedType() instanceof OutputTypeInterface; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function isLeafType() 167 | { 168 | return $this->getUnmodifiedType() instanceof LeafTypeInterface; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function isCompositeType() 175 | { 176 | return $this->getUnmodifiedType() instanceof CompositeTypeInterface; 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | */ 182 | public function isAbstractType() 183 | { 184 | return $this->getUnmodifiedType() instanceof AbstractTypeInterface; 185 | } 186 | 187 | /** 188 | * {@inheritdoc} 189 | */ 190 | public function getNullableType() 191 | { 192 | if ($this instanceof NonNullModifier) { 193 | return $this->getWrappedType(); 194 | } 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | */ 202 | public function getUnmodifiedType() 203 | { 204 | $return = $this; 205 | while ($return instanceof ListModifier || $return instanceof NonNullModifier) { 206 | $return = $return->getWrappedType(); 207 | } 208 | 209 | return $return; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/TypeInterface.php: -------------------------------------------------------------------------------- 1 | types = $types; 49 | $this->typeResolver = $typeResolver; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getPossibleTypes() 56 | { 57 | return $this->types; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function addPossibleType(ObjectType $type) 64 | { 65 | $this->types = array_unique($this->types + [$type]); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function isPossibleType(ObjectType $type) 72 | { 73 | return in_array($type, $this->types, TRUE); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function resolveType($value) 80 | { 81 | if (isset($this->typeResolver)) { 82 | return call_user_func($this->typeResolver, $value); 83 | } 84 | 85 | return $this->getTypeOf($value); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Type/Definition/Types/UnmodifiedTypeInterface.php: -------------------------------------------------------------------------------- 1 | name; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getDescription() 92 | { 93 | return $this->description; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function getArguments() 100 | { 101 | return $this->arguments; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function getType() 108 | { 109 | return $this->type; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function onOperation() 116 | { 117 | return $this->onOperation; 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function onFragment() 124 | { 125 | return $this->onFragment; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function onField() 132 | { 133 | return $this->onField; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Type/Directives/DirectiveInterface.php: -------------------------------------------------------------------------------- 1 | arguments = [ 45 | new FieldArgument([ 46 | 'name' => 'if', 47 | 'type' => new NonNullModifier(Type::booleanType()), 48 | 'description' => 'Included if true.' 49 | ]), 50 | ]; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getType() 57 | { 58 | if (!isset($this->type)) { 59 | $this->type = new NonNullModifier(Type::booleanType()); 60 | } 61 | 62 | return $this->type; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Type/Directives/SkipDirective.php: -------------------------------------------------------------------------------- 1 | arguments = [ 45 | new FieldArgument([ 46 | 'name' => 'if', 47 | 'type' => new NonNullModifier(Type::booleanType()), 48 | 'description' => 'Skipped if true.' 49 | ]), 50 | ]; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getType() 57 | { 58 | if (!isset($this->type)) { 59 | $this->type = new NonNullModifier(Type::booleanType()); 60 | } 61 | 62 | return $this->type; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Utility/TypeInfo.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected $typeStack; 37 | 38 | /** 39 | * @var \SplStack 40 | */ 41 | protected $parentTypeStack; 42 | 43 | /** 44 | * @var \SplStack 45 | */ 46 | protected $inputTypeStack; 47 | 48 | /** 49 | * @var \SplStack 50 | */ 51 | protected $fieldDefinitionStack; 52 | 53 | /** 54 | * @param Schema $schema 55 | * @param $inputType 56 | * 57 | * @return ListModifier|NonNullModifier|\Fubhy\GraphQL\Type\Definition\Types\TypeInterface|null 58 | */ 59 | public static function typeFromAST(Schema $schema, $inputType) 60 | { 61 | if ($inputType instanceof ListType) { 62 | $innerType = self::typeFromAST($schema, $inputType->get('type')); 63 | return $innerType ? new ListModifier($innerType) : NULL; 64 | } 65 | 66 | if ($inputType instanceof NonNullType) { 67 | $innerType = self::typeFromAST($schema, $inputType->get('type')); 68 | return $innerType ? new NonNullModifier($innerType) : NULL; 69 | } 70 | 71 | if (!($inputType instanceof NamedType)) { 72 | throw new \LogicException('Must be a type name.'); 73 | } 74 | 75 | return $schema->getType($inputType->get('name')->get('value')); 76 | } 77 | 78 | /** 79 | * Not exactly the same as the executor's definition of getFieldDef, in this 80 | * statically evaluated environment we do not always have an Object type, 81 | * and need to handle Interface and Union types. 82 | * 83 | * @param Schema $schema 84 | * @param Type $parentType 85 | * @param Field $fieldAST 86 | * 87 | * @return FieldDefinition 88 | */ 89 | protected static function getFieldDefinition(Schema $schema, Type $parentType, Field $fieldAST) 90 | { 91 | $name = $fieldAST->get('name')->get('value'); 92 | $schemaMeta = Introspection::schemaMetaFieldDefinition(); 93 | if ($name === $schemaMeta->getName() && $schema->getQueryType() === $parentType) { 94 | return $schemaMeta; 95 | } 96 | 97 | $typeMeta = Introspection::typeMetaFieldDefinition(); 98 | if ($name === $typeMeta->getName() && $schema->getQueryType() === $parentType) { 99 | return $typeMeta; 100 | } 101 | 102 | $typeNameMeta = Introspection::typeNameMetaFieldDefinition(); 103 | if ($name === $typeNameMeta->getName() && ($parentType instanceof ObjectType || $parentType instanceof InterfaceType || $parentType instanceof UnionType)) { 104 | return $typeNameMeta; 105 | } 106 | 107 | if ($parentType instanceof ObjectType || $parentType instanceof InterfaceType) { 108 | $fields = $parentType->getFields(); 109 | return isset($fields[$name]) ? $fields[$name] : NULL; 110 | } 111 | 112 | return NULL; 113 | } 114 | 115 | /** 116 | * Constructor. 117 | * 118 | * @param Schema $schema 119 | */ 120 | public function __construct(Schema $schema) 121 | { 122 | $this->_schema = $schema; 123 | $this->typeStack = []; 124 | $this->parentTypeStack = []; 125 | $this->inputTypeStack = []; 126 | $this->fieldDefinitionStack = []; 127 | } 128 | 129 | /** 130 | * @return Type 131 | */ 132 | protected function getType() 133 | { 134 | if (!empty($this->typeStack)) { 135 | return $this->typeStack[count($this->typeStack) - 1]; 136 | } 137 | 138 | return NULL; 139 | } 140 | 141 | /** 142 | * @return Type 143 | */ 144 | protected function getParentType() 145 | { 146 | if (!empty($this->parentTypeStack)) { 147 | return $this->parentTypeStack[count($this->parentTypeStack) - 1]; 148 | } 149 | return NULL; 150 | } 151 | 152 | /** 153 | * @return mixed|null 154 | */ 155 | protected function getInputType() 156 | { 157 | if (!empty($this->inputTypeStack)) { 158 | return $this->inputTypeStack[count($this->inputTypeStack) - 1]; 159 | } 160 | return NULL; 161 | } 162 | 163 | /** 164 | * @param Node $node 165 | */ 166 | protected function enter(Node $node) 167 | { 168 | $schema = $this->_schema; 169 | 170 | switch ($node::KIND) { 171 | case Node::KIND_SELECTION_SET: 172 | $rawType = $this->getType(); 173 | $rawType = isset($rawType) ? $rawType->getUnmodifiedType() : NULL; 174 | $compositeType = NULL; 175 | if (isset($rawType) && $rawType->isCompositeType()) { 176 | $compositeType = $rawType; 177 | } 178 | array_push($this->parentTypeStack, $compositeType); 179 | break; 180 | 181 | case Node::KIND_DIRECTIVE: 182 | $this->directive = $schema->getDirective($node->get('name')->get('value')); 183 | break; 184 | 185 | case Node::KIND_FIELD: 186 | $parentType = $this->getParentType(); 187 | $fieldDefinition = NULL; 188 | if (isset($parentType)) { 189 | $fieldDefinition = self::getFieldDefinition($schema, $parentType, $node); 190 | } 191 | array_push($this->fieldDefinitionStack, $fieldDefinition); 192 | array_push($this->typeStack, $fieldDefinition ? $fieldDefinition->getType() : NULL); 193 | break; 194 | 195 | case Node::KIND_OPERATION_DEFINITION: 196 | $type = NULL; 197 | if ($node->get('operation') === 'query') { 198 | $type = $schema->getQueryType(); 199 | } else if ($node->get('operation') === 'mutation') { 200 | $type = $schema->getMutationType(); 201 | } 202 | array_push($this->typeStack, $type); 203 | break; 204 | 205 | case Node::KIND_INLINE_FRAGMENT: 206 | case Node::KIND_FRAGMENT_DEFINITION: 207 | $type = $schema->getType($node->get('typeCondition')->get('value')); 208 | array_push($this->typeStack, $type); 209 | break; 210 | 211 | case Node::KIND_VARIABLE_DEFINITION: 212 | array_push($this->inputTypeStack, self::typeFromAST($schema, $node->get('type'))); 213 | break; 214 | 215 | case Node::KIND_ARGUMENT: 216 | if (!empty($this->fieldDefinitionStack)) { 217 | $field = $this->fieldDefinitionStack[count($this->fieldDefinitionStack) - 1]; 218 | } 219 | else { 220 | $field = NULL; 221 | } 222 | 223 | $argType = NULL; 224 | if ($field) { 225 | $argDefinition = NULL; 226 | foreach ($field->getArguments() as $arg) { 227 | if ($arg->getName() === $node->get('name')->get('value')) { 228 | $argDefinition = $arg; 229 | } 230 | break; 231 | } 232 | 233 | if ($argDefinition) { 234 | $argType = $argDefinition->getType(); 235 | } 236 | } 237 | array_push($this->inputTypeStack, $argType); 238 | break; 239 | 240 | case Node::KIND_ARRAY_VALUE: 241 | $arrayType = $this->getInputType(); 242 | $arrayType = isset($arrayType) ? $arrayType->getNullableType() : NULL; 243 | array_push( 244 | $this->inputTypeStack, 245 | $arrayType instanceof ListModifier ? $arrayType->getWrappedType() : NULL 246 | ); 247 | break; 248 | 249 | case Node::KIND_OBJECT_FIELD: 250 | $objectType = $this->getInputType(); 251 | $objectType = isset($objectType) ? $objectType->getUnmodifiedType() : NULL; 252 | $fieldType = NULL; 253 | if ($objectType instanceof InputObjectType) { 254 | $tmp = $objectType->getFields(); 255 | $inputField = isset($tmp[$node->get('name')->get('value')]) ? $tmp[$node->get('name')->get('value')] : NULL; 256 | $fieldType = $inputField ? $inputField->getType() : NULL; 257 | } 258 | array_push($this->inputTypeStack, $fieldType); 259 | break; 260 | } 261 | } 262 | 263 | /** 264 | * @param Node $node 265 | */ 266 | protected function leave(Node $node) 267 | { 268 | switch ($node::KIND) { 269 | case Node::KIND_SELECTION_SET: 270 | array_pop($this->parentTypeStack); 271 | break; 272 | case Node::KIND_FIELD: 273 | array_pop($this->fieldDefinitionStack); 274 | array_pop($this->typeStack); 275 | break; 276 | case Node::KIND_DIRECTIVE: 277 | $this->directive = NULL; 278 | break; 279 | case Node::KIND_OPERATION_DEFINITION: 280 | case Node::KIND_INLINE_FRAGMENT: 281 | case Node::KIND_FRAGMENT_DEFINITION: 282 | array_pop($this->typeStack); 283 | break; 284 | case Node::KIND_VARIABLE_DEFINITION: 285 | array_pop($this->inputTypeStack); 286 | break; 287 | case Node::KIND_ARGUMENT: 288 | array_pop($this->inputTypeStack); 289 | break; 290 | case Node::KIND_ARRAY_VALUE: 291 | case Node::KIND_OBJECT_FIELD: 292 | array_pop($this->inputTypeStack); 293 | break; 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /tests/Executor/DirectivesTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b }')); 17 | } 18 | 19 | public function testWorksOnScalars() 20 | { 21 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @include(if: true) }')); 22 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @include(if: false) }')); 23 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @skip(if: false) }')); 24 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @skip(if: true) }')); 25 | } 26 | 27 | public function testWorksOnFragmentSpreads() 28 | { 29 | $query = ' 30 | query Q { 31 | a 32 | ...Frag @include(if: false) 33 | } 34 | 35 | fragment Frag on TestType { 36 | b 37 | } 38 | '; 39 | 40 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($query)); 41 | 42 | $query = ' 43 | query Q { 44 | a 45 | ...Frag @include(if: true) 46 | } 47 | 48 | fragment Frag on TestType { 49 | b 50 | } 51 | '; 52 | 53 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($query)); 54 | 55 | $query = ' 56 | query Q { 57 | a 58 | ...Frag @skip(if: false) 59 | } 60 | 61 | fragment Frag on TestType { 62 | b 63 | } 64 | '; 65 | 66 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($query)); 67 | 68 | $query = ' 69 | query Q { 70 | a 71 | ...Frag @skip(if: true) 72 | } 73 | 74 | fragment Frag on TestType { 75 | b 76 | } 77 | '; 78 | 79 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($query)); 80 | } 81 | 82 | public function testWorksOnInlineFragment() 83 | { 84 | $query = ' 85 | query Q { 86 | a 87 | ... on TestType @include(if: false) { 88 | b 89 | } 90 | } 91 | 92 | fragment Frag on TestType { 93 | b 94 | } 95 | '; 96 | 97 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($query)); 98 | 99 | $query = ' 100 | query Q { 101 | a 102 | ... on TestType @include(if: true) { 103 | b 104 | } 105 | } 106 | 107 | fragment Frag on TestType { 108 | b 109 | } 110 | '; 111 | 112 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($query)); 113 | 114 | $query = ' 115 | query Q { 116 | a 117 | ... on TestType @skip(if: false) { 118 | b 119 | } 120 | } 121 | 122 | fragment Frag on TestType { 123 | b 124 | } 125 | '; 126 | 127 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($query)); 128 | 129 | $query = ' 130 | query Q { 131 | a 132 | ... on TestType @skip(if: true) { 133 | b 134 | } 135 | } 136 | 137 | fragment Frag on TestType { 138 | b 139 | } 140 | '; 141 | 142 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($query)); 143 | } 144 | 145 | public function testWorksOnFragment() 146 | { 147 | $query = ' 148 | query Q { 149 | a 150 | ...Frag 151 | } 152 | 153 | fragment Frag on TestType @include(if: false) { 154 | b 155 | } 156 | '; 157 | 158 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($query)); 159 | 160 | $query = ' 161 | query Q { 162 | a 163 | ...Frag 164 | } 165 | 166 | fragment Frag on TestType @include(if: true) { 167 | b 168 | } 169 | '; 170 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($query)); 171 | 172 | $query = ' 173 | query Q { 174 | a 175 | ...Frag 176 | } 177 | 178 | fragment Frag on TestType @skip(if: false) { 179 | b 180 | } 181 | '; 182 | 183 | $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($query)); 184 | 185 | $query = ' 186 | query Q { 187 | a 188 | ...Frag 189 | } 190 | 191 | fragment Frag on TestType @skip(if: true) { 192 | b 193 | } 194 | '; 195 | 196 | $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($query)); 197 | } 198 | 199 | protected function executeTestQuery($document) 200 | { 201 | $data = [ 202 | 'a' => function () { 203 | return 'a'; 204 | }, 205 | 'b' => function () { 206 | return 'b'; 207 | } 208 | ]; 209 | 210 | $schema = new Schema(new ObjectType('TestType', [ 211 | 'a' => ['type' => Type::stringType()], 212 | 'b' => ['type' => Type::stringType()], 213 | ])); 214 | 215 | $parser = new Parser(); 216 | return Executor::execute($schema, $data, $parser->parse(new Source($document))); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/Executor/ExecutorSchemaTest.php: -------------------------------------------------------------------------------- 1 | ['type' => Type::stringType()], 21 | 'width' => ['type' => Type::intType()], 22 | 'height' => ['type' => Type::intType()], 23 | ]); 24 | 25 | $blogAuthor = new ObjectType('Author', [ 26 | 'id' => ['type' => Type::stringType()], 27 | 'name' => ['type' => Type::stringType()], 28 | 'pic' => [ 29 | 'args' => ['width' => ['type' => Type::intType()], 'height' => ['type' => Type::intType()]], 30 | 'type' => $blogImage, 31 | 'resolve' => function ($obj, $args) { 32 | return $obj['pic']($args['width'], $args['height']); 33 | } 34 | ], 35 | 'recentArticle' => [ 36 | 'type' => function () use (&$blogArticle) { 37 | return $blogArticle; 38 | } 39 | ], 40 | ]); 41 | 42 | $blogArticle = new ObjectType('Article', [ 43 | 'id' => ['type' => new NonNullModifier(Type::stringType())], 44 | 'isPublished' => ['type' => Type::booleanType()], 45 | 'author' => ['type' => $blogAuthor], 46 | 'title' => ['type' => Type::stringType()], 47 | 'body' => ['type' => Type::stringType()], 48 | 'keywords' => ['type' => new ListModifier(Type::stringType())] 49 | ]); 50 | 51 | $blogQuery = new ObjectType('Query', [ 52 | 'article' => [ 53 | 'type' => $blogArticle, 54 | 'args' => ['id' => ['type' => Type::idType()]], 55 | 'resolve' => function ($_, $args) { 56 | return $this->article($args['id']); 57 | } 58 | ], 59 | 'feed' => [ 60 | 'type' => new ListModifier($blogArticle), 61 | 'resolve' => function () { 62 | return [ 63 | $this->article(1), 64 | $this->article(2), 65 | $this->article(3), 66 | $this->article(4), 67 | $this->article(5), 68 | $this->article(6), 69 | $this->article(7), 70 | $this->article(8), 71 | $this->article(9), 72 | $this->article(10), 73 | ]; 74 | } 75 | ], 76 | ]); 77 | 78 | $blogSchema = new Schema($blogQuery); 79 | 80 | $request = ' 81 | { 82 | feed { 83 | id, 84 | title 85 | }, 86 | article(id: "1") { 87 | ...articleFields, 88 | author { 89 | id, 90 | name, 91 | pic(width: 640, height: 480) { 92 | url, 93 | width, 94 | height 95 | }, 96 | recentArticle { 97 | ...articleFields, 98 | keywords 99 | } 100 | } 101 | } 102 | } 103 | 104 | fragment articleFields on Article { 105 | id, 106 | isPublished, 107 | title, 108 | body, 109 | hidden, 110 | notdefined 111 | } 112 | '; 113 | 114 | $expected = [ 115 | 'data' => [ 116 | 'feed' => [ 117 | ['id' => '1', 'title' => 'My Article 1'], 118 | ['id' => '2', 'title' => 'My Article 2'], 119 | ['id' => '3', 'title' => 'My Article 3'], 120 | ['id' => '4', 'title' => 'My Article 4'], 121 | ['id' => '5', 'title' => 'My Article 5'], 122 | ['id' => '6', 'title' => 'My Article 6'], 123 | ['id' => '7', 'title' => 'My Article 7'], 124 | ['id' => '8', 'title' => 'My Article 8'], 125 | ['id' => '9', 'title' => 'My Article 9'], 126 | ['id' => '10', 'title' => 'My Article 10'], 127 | ], 128 | 'article' => [ 129 | 'id' => '1', 130 | 'isPublished' => TRUE, 131 | 'title' => 'My Article 1', 132 | 'body' => 'This is a post', 133 | 'author' => [ 134 | 'id' => '123', 135 | 'name' => 'John Smith', 136 | 'pic' => [ 137 | 'url' => 'cdn://123', 138 | 'width' => 640, 139 | 'height' => 480, 140 | ], 141 | 'recentArticle' => [ 142 | 'id' => '1', 143 | 'isPublished' => TRUE, 144 | 'title' => 'My Article 1', 145 | 'body' => 'This is a post', 146 | 'keywords' => ['foo', 'bar', '1', 'true', NULL], 147 | ], 148 | ], 149 | ], 150 | ], 151 | ]; 152 | 153 | $parser = new Parser(); 154 | $this->assertEquals($expected, Executor::execute($blogSchema, NULL, $parser->parse(new Source($request)), '', [])); 155 | } 156 | 157 | protected function article($id) 158 | { 159 | $johnSmith = NULL; 160 | $article = function ($id) use (&$johnSmith) { 161 | return [ 162 | 'id' => $id, 163 | 'isPublished' => 'true', 164 | 'author' => $johnSmith, 165 | 'title' => 'My Article ' . $id, 166 | 'body' => 'This is a post', 167 | 'hidden' => 'This data is not exposed in the schema', 168 | 'keywords' => ['foo', 'bar', 1, TRUE, NULL], 169 | ]; 170 | }; 171 | 172 | $johnSmith = [ 173 | 'id' => 123, 174 | 'name' => 'John Smith', 175 | 'pic' => function ($width, $height) { 176 | return [ 177 | 'url' => "cdn://123", 178 | 'width' => $width, 179 | 'height' => $height 180 | ]; 181 | }, 182 | 'recentArticle' => $article(1), 183 | ]; 184 | 185 | return $article($id); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tests/Executor/ListsTest.php: -------------------------------------------------------------------------------- 1 | parse(new Source($document)); 28 | $expected = ['data' => ['nest' => ['list' => [1,2]]]]; 29 | 30 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 31 | } 32 | 33 | public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues() 34 | { 35 | $document = ' 36 | query Q { 37 | nest { 38 | listOfNonNull, 39 | } 40 | } 41 | '; 42 | 43 | $parser = new Parser(); 44 | $ast = $parser->parse(new Source($document)); 45 | $expected = ['data' => ['nest' => ['listOfNonNull' => [1, 2]]]]; 46 | 47 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 48 | } 49 | 50 | public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues() 51 | { 52 | $document = ' 53 | query Q { 54 | nest { 55 | nonNullList, 56 | } 57 | } 58 | '; 59 | 60 | $parser = new Parser(); 61 | $ast = $parser->parse(new Source($document)); 62 | $expected = ['data' => ['nest' => ['nonNullList' => [1, 2]]]]; 63 | 64 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 65 | } 66 | 67 | public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues() 68 | { 69 | $document = ' 70 | query Q { 71 | nest { 72 | nonNullListOfNonNull, 73 | } 74 | } 75 | '; 76 | 77 | $parser = new Parser(); 78 | $ast = $parser->parse(new Source($document)); 79 | $expected = ['data' => ['nest' => ['nonNullListOfNonNull' => [1, 2]]]]; 80 | 81 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 82 | } 83 | 84 | public function testHandlesListsWhenTheyReturnNullAsAValue() 85 | { 86 | $document = ' 87 | query Q { 88 | nest { 89 | listContainsNull, 90 | } 91 | } 92 | '; 93 | 94 | $parser = new Parser(); 95 | $ast = $parser->parse(new Source($document)); 96 | $expected = ['data' => ['nest' => ['listContainsNull' => [1, NULL, 2]]]]; 97 | 98 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 99 | } 100 | 101 | public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue() 102 | { 103 | $document = ' 104 | query Q { 105 | nest { 106 | listOfNonNullContainsNull, 107 | } 108 | } 109 | '; 110 | 111 | $parser = new Parser(); 112 | $ast = $parser->parse(new Source($document)); 113 | 114 | $expected = [ 115 | 'data' => ['nest' => ['listOfNonNullContainsNull' => NULL]], 116 | 'errors' => [new \Exception('Cannot return null for non-nullable type.')], 117 | ]; 118 | 119 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 120 | } 121 | 122 | public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue() 123 | { 124 | $document = ' 125 | query Q { 126 | nest { 127 | nonNullListContainsNull, 128 | } 129 | } 130 | '; 131 | 132 | $parser = new Parser(); 133 | $ast = $parser->parse(new Source($document)); 134 | $expected = ['data' => ['nest' => ['nonNullListContainsNull' => [1, NULL, 2]]]]; 135 | 136 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 137 | } 138 | 139 | public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue() 140 | { 141 | $document = ' 142 | query Q { 143 | nest { 144 | nonNullListOfNonNullContainsNull, 145 | } 146 | } 147 | '; 148 | 149 | $parser = new Parser(); 150 | $ast = $parser->parse(new Source($document)); 151 | 152 | $expected = [ 153 | 'data' => ['nest' => NULL], 154 | 'errors' => [new \Exception('Cannot return null for non-nullable type.')], 155 | ]; 156 | 157 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 158 | } 159 | 160 | public function testHandlesListsWhenTheyReturnNull() 161 | { 162 | $document = ' 163 | query Q { 164 | nest { 165 | listReturnsNull, 166 | } 167 | } 168 | '; 169 | 170 | $parser = new Parser(); 171 | $ast = $parser->parse(new Source($document)); 172 | $expected = ['data' => ['nest' => ['listReturnsNull' => NULL]]]; 173 | 174 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 175 | } 176 | 177 | public function testHandlesListsOfNonNullsWhenTheyReturnNull() 178 | { 179 | $document = ' 180 | query Q { 181 | nest { 182 | listOfNonNullReturnsNull, 183 | } 184 | } 185 | '; 186 | 187 | $parser = new Parser(); 188 | $ast = $parser->parse(new Source($document)); 189 | $expected = ['data' => ['nest' => ['listOfNonNullReturnsNull' => NULL]]]; 190 | 191 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 192 | } 193 | 194 | public function testHandlesNonNullListsOfWhenTheyReturnNull() 195 | { 196 | $document = ' 197 | query Q { 198 | nest { 199 | nonNullListReturnsNull, 200 | } 201 | } 202 | '; 203 | 204 | $parser = new Parser(); 205 | $ast = $parser->parse(new Source($document)); 206 | 207 | $expected = [ 208 | 'data' => ['nest' => NULL], 209 | 'errors' => [new \Exception('Cannot return null for non-nullable type.')], 210 | ]; 211 | 212 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 213 | } 214 | 215 | public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull() 216 | { 217 | $document = ' 218 | query Q { 219 | nest { 220 | nonNullListOfNonNullReturnsNull, 221 | } 222 | } 223 | '; 224 | 225 | $parser = new Parser(); 226 | $ast = $parser->parse(new Source($document)); 227 | 228 | $expected = [ 229 | 'data' => ['nest' => NULL], 230 | 'errors' => [new \Exception('Cannot return null for non-nullable type.')], 231 | ]; 232 | 233 | $this->assertEquals($expected, Executor::execute($this->getSchema(), $this->getData(), $ast, 'Q', [])); 234 | } 235 | 236 | protected function getSchema() 237 | { 238 | $dataType = new ObjectType('DataType', [ 239 | 'list' => [ 240 | 'type' => new ListModifier(Type::intType()), 241 | ], 242 | 'listOfNonNull' => [ 243 | 'type' => new ListModifier(new NonNullModifier(Type::intType())), 244 | ], 245 | 'nonNullList' => [ 246 | 'type' => new NonNullModifier(new ListModifier(Type::intType())), 247 | ], 248 | 'nonNullListOfNonNull' => [ 249 | 'type' => new NonNullModifier(new ListModifier(new NonNullModifier(Type::intType()))), 250 | ], 251 | 'listContainsNull' => [ 252 | 'type' => new ListModifier(Type::intType()), 253 | ], 254 | 'listOfNonNullContainsNull' => [ 255 | 'type' => new ListModifier(new NonNullModifier(Type::intType())), 256 | ], 257 | 'nonNullListContainsNull' => [ 258 | 'type' => new NonNullModifier(new ListModifier(Type::intType())), 259 | ], 260 | 'nonNullListOfNonNullContainsNull' => [ 261 | 'type' => new NonNullModifier(new ListModifier(new NonNullModifier(Type::intType()))), 262 | ], 263 | 'listReturnsNull' => [ 264 | 'type' => new ListModifier(Type::intType()), 265 | ], 266 | 'listOfNonNullReturnsNull' => [ 267 | 'type' => new ListModifier(new NonNullModifier(Type::intType())), 268 | ], 269 | 'nonNullListReturnsNull' => [ 270 | 'type' => new NonNullModifier(new ListModifier(Type::intType())), 271 | ], 272 | 'nonNullListOfNonNullReturnsNull' => [ 273 | 'type' => new NonNullModifier(new ListModifier(new NonNullModifier(Type::intType()))), 274 | ], 275 | 'nest' => ['type' => function () use (&$dataType) { 276 | return $dataType; 277 | }], 278 | ]); 279 | 280 | $schema = new Schema($dataType); 281 | return $schema; 282 | } 283 | 284 | protected function getData() 285 | { 286 | return [ 287 | 'list' => function () { 288 | return [1, 2]; 289 | }, 290 | 'listOfNonNull' => function () { 291 | return [1, 2]; 292 | }, 293 | 'nonNullList' => function () { 294 | return [1, 2]; 295 | }, 296 | 'nonNullListOfNonNull' => function () { 297 | return [1, 2]; 298 | }, 299 | 'listContainsNull' => function () { 300 | return [1, NULL, 2]; 301 | }, 302 | 'listOfNonNullContainsNull' => function () { 303 | return [1, NULL, 2]; 304 | }, 305 | 'nonNullListContainsNull' => function () { 306 | return [1, NULL, 2]; 307 | }, 308 | 'nonNullListOfNonNullContainsNull' => function () { 309 | return [1, NULL, 2]; 310 | }, 311 | 'listReturnsNull' => function () { 312 | return NULL; 313 | }, 314 | 'listOfNonNullReturnsNull' => function () { 315 | return NULL; 316 | }, 317 | 'nonNullListReturnsNull' => function () { 318 | return NULL; 319 | }, 320 | 'nonNullListOfNonNullReturnsNull' => function () { 321 | return NULL; 322 | }, 323 | 'nest' => function () { 324 | return self::getData(); 325 | } 326 | ]; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /tests/Executor/NonNullTest.php: -------------------------------------------------------------------------------- 1 | syncError = new \Exception('sync'); 24 | $this->nonNullSyncError = new \Exception('nonNullSync'); 25 | 26 | $this->throwingData = [ 27 | 'sync' => function () { 28 | throw $this->syncError; 29 | }, 30 | 'nonNullSync' => function () { 31 | throw $this->nonNullSyncError; 32 | }, 33 | 'nest' => function () { 34 | return $this->throwingData; 35 | }, 36 | 'nonNullNest' => function () { 37 | return $this->throwingData; 38 | }, 39 | ]; 40 | 41 | $this->NULLingData = [ 42 | 'sync' => function () { 43 | return NULL; 44 | }, 45 | 'nonNullSync' => function () { 46 | return NULL; 47 | }, 48 | 'nest' => function () { 49 | return $this->NULLingData; 50 | }, 51 | 'nonNullNest' => function () { 52 | return $this->NULLingData; 53 | }, 54 | ]; 55 | 56 | $dataType = new ObjectType('DataType', [ 57 | 'sync' => ['type' => Type::stringType()], 58 | 'nonNullSync' => ['type' => new NonNullModifier(Type::stringType())], 59 | 'nest' => ['type' => function () use (&$dataType) { 60 | return $dataType; 61 | }], 62 | 'nonNullNest' => ['type' => function () use (&$dataType) { 63 | return new NonNullModifier($dataType); 64 | }] 65 | ]); 66 | 67 | $this->schema = new Schema($dataType); 68 | } 69 | 70 | public function testNullsANullableFieldThatThrowsSynchronously() 71 | { 72 | $document = ' 73 | query Q { 74 | sync 75 | } 76 | '; 77 | 78 | $parser = new Parser(); 79 | $ast = $parser->parse(new Source($document)); 80 | $expected = [ 81 | 'data' => ['sync' => NULL], 82 | 'errors' => [new \Exception($this->syncError->getMessage())], 83 | ]; 84 | 85 | $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); 86 | } 87 | 88 | public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously() 89 | { 90 | $document = ' 91 | query Q { 92 | nest { 93 | nonNullSync, 94 | } 95 | } 96 | '; 97 | 98 | $parser = new Parser(); 99 | $ast = $parser->parse(new Source($document)); 100 | $expected = [ 101 | 'data' => ['nest' => NULL], 102 | 'errors' => [new \Exception($this->nonNullSyncError->getMessage())], 103 | ]; 104 | 105 | $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); 106 | } 107 | 108 | public function testNullsAComplexTreeOfNullableFieldsThatThrow() 109 | { 110 | $document = ' 111 | query Q { 112 | nest { 113 | sync 114 | nest { 115 | sync 116 | } 117 | } 118 | } 119 | '; 120 | 121 | $parser = new Parser(); 122 | $ast = $parser->parse(new Source($document)); 123 | $expected = [ 124 | 'data' => [ 125 | 'nest' => [ 126 | 'sync' => NULL, 127 | 'nest' => ['sync' => NULL], 128 | ], 129 | ], 130 | 'errors' => [ 131 | new \Exception($this->syncError->getMessage()), 132 | new \Exception($this->syncError->getMessage()), 133 | ], 134 | ]; 135 | 136 | $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast, 'Q', [])); 137 | } 138 | 139 | public function testNullsANullableFieldThatSynchronouslyReturnsNull() 140 | { 141 | $document = ' 142 | query Q { 143 | sync 144 | } 145 | '; 146 | 147 | $parser = new Parser(); 148 | $ast = $parser->parse(new Source($document)); 149 | $expected = ['data' => ['sync' => NULL]]; 150 | 151 | $this->assertEquals($expected, Executor::execute($this->schema, $this->NULLingData, $ast, 'Q', [])); 152 | } 153 | 154 | public function test4() 155 | { 156 | $document = ' 157 | query Q { 158 | nest { 159 | nonNullSync, 160 | } 161 | } 162 | '; 163 | 164 | $parser = new Parser(); 165 | $ast = $parser->parse(new Source($document)); 166 | $expected = [ 167 | 'data' => ['nest' => NULL], 168 | 'errors' => [new \Exception('Cannot return null for non-nullable type.')], 169 | ]; 170 | 171 | $this->assertEquals($expected, Executor::execute($this->schema, $this->NULLingData, $ast, 'Q', [])); 172 | } 173 | 174 | public function test5() 175 | { 176 | $document = ' 177 | query Q { 178 | nest { 179 | sync 180 | nest { 181 | sync 182 | nest { 183 | sync 184 | } 185 | } 186 | } 187 | } 188 | '; 189 | 190 | $parser = new Parser(); 191 | $ast = $parser->parse(new Source($document)); 192 | $expected = [ 193 | 'data' => [ 194 | 'nest' => [ 195 | 'sync' => NULL, 196 | 'nest' => [ 197 | 'sync' => NULL, 198 | 'nest' => ['sync' => NULL], 199 | ], 200 | ], 201 | ], 202 | ]; 203 | 204 | $this->assertEquals($expected, Executor::execute($this->schema, $this->NULLingData, $ast, 'Q', [])); 205 | } 206 | 207 | public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() 208 | { 209 | $document = 'query Q { nonNullSync }'; 210 | 211 | $expected = [ 212 | 'data' => NULL, 213 | 'errors' => [new \Exception($this->nonNullSyncError->getMessage())], 214 | ]; 215 | 216 | $parser = new Parser(); 217 | $ast = $parser->parse(new Source($document)); 218 | $this->assertEquals($expected, Executor::execute($this->schema, $this->throwingData, $ast)); 219 | } 220 | 221 | public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull() 222 | { 223 | $document = 'query Q { nonNullSync }'; 224 | $expected = [ 225 | 'data' => NULL, 226 | 'errors' => [new \Exception('Cannot return null for non-nullable type.')], 227 | ]; 228 | 229 | $parser = new Parser(); 230 | $ast = $parser->parse(new Source($document)); 231 | $this->assertEquals($expected, Executor::execute($this->schema, $this->NULLingData, $ast)); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /tests/Executor/UnionInterfaceTest.php: -------------------------------------------------------------------------------- 1 | ['type' => Type::stringType()], 27 | ]); 28 | 29 | $dogType = new ObjectType('Dog', [ 30 | 'name' => ['type' => Type::stringType()], 31 | 'barks' => ['type' => Type::booleanType()], 32 | ], [$namedType], function ($value) { 33 | return $value instanceof Dog; 34 | }); 35 | 36 | $catType = new ObjectType('Cat', [ 37 | 'name' => ['type' => Type::stringType()], 38 | 'meows' => ['type' => Type::booleanType()], 39 | ], [$namedType], function ($value) { 40 | return $value instanceof Cat; 41 | }); 42 | 43 | $petType = new UnionType('Pet', [$dogType, $catType], function ($value) use ($dogType, $catType) { 44 | if ($value instanceof Dog) { 45 | return $dogType; 46 | } 47 | 48 | if ($value instanceof Cat) { 49 | return $catType; 50 | } 51 | 52 | return NULL; 53 | }); 54 | 55 | $personType = new ObjectType('Person', [ 56 | 'name' => ['type' => Type::stringType()], 57 | 'pets' => ['type' => new ListModifier($petType)], 58 | 'friends' => ['type' => new ListModifier($namedType)], 59 | ], [$namedType], function ($value) { 60 | return $value instanceof Person; 61 | }); 62 | 63 | $this->schema = new Schema($personType); 64 | $this->garfield = new Cat('Garfield', FALSE); 65 | $this->odie = new Dog('Odie', TRUE); 66 | $this->liz = new Person('Liz'); 67 | $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); 68 | 69 | } 70 | 71 | public function testCanIntrospectOnUnionAndIntersectionTypes() 72 | { 73 | $parser = new Parser(); 74 | $ast = $parser->parse(new Source(' 75 | { 76 | Named: __type(name: "Named") { 77 | kind 78 | name 79 | fields { name } 80 | interfaces { name } 81 | possibleTypes { name } 82 | enumValues { name } 83 | inputFields { name } 84 | } 85 | Pet: __type(name: "Pet") { 86 | kind 87 | name 88 | fields { name } 89 | interfaces { name } 90 | possibleTypes { name } 91 | enumValues { name } 92 | inputFields { name } 93 | } 94 | } 95 | ')); 96 | 97 | $expected = [ 98 | 'data' => [ 99 | 'Named' => [ 100 | 'kind' => 'INTERFACE', 101 | 'name' => 'Named', 102 | 'fields' => [ 103 | ['name' => 'name'], 104 | ], 105 | 'interfaces' => NULL, 106 | 'possibleTypes' => [ 107 | ['name' => 'Dog'], 108 | ['name' => 'Cat'], 109 | ['name' => 'Person'], 110 | ], 111 | 'enumValues' => NULL, 112 | 'inputFields' => NULL, 113 | ], 114 | 'Pet' => [ 115 | 'kind' => 'UNION', 116 | 'name' => 'Pet', 117 | 'fields' => NULL, 118 | 'interfaces' => NULL, 119 | 'possibleTypes' => [ 120 | ['name' => 'Dog'], 121 | ['name' => 'Cat'], 122 | ], 123 | 'enumValues' => NULL, 124 | 'inputFields' => NULL, 125 | ], 126 | ], 127 | ]; 128 | 129 | $this->assertEquals($expected, Executor::execute($this->schema, NULL, $ast)); 130 | } 131 | 132 | public function testExecutesUsingUnionTypes() 133 | { 134 | $parser = new Parser(); 135 | $ast = $parser->parse(new Source(' 136 | { 137 | __typename 138 | name 139 | pets { 140 | __typename 141 | name 142 | barks 143 | meows 144 | } 145 | } 146 | ')); 147 | 148 | $expected = [ 149 | 'data' => [ 150 | '__typename' => 'Person', 151 | 'name' => 'John', 152 | 'pets' => [ 153 | ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => FALSE], 154 | ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => TRUE], 155 | ], 156 | ], 157 | ]; 158 | 159 | $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); 160 | } 161 | 162 | public function testExecutesUnionTypesWithInlineFragments() 163 | { 164 | $parser = new Parser(); 165 | $ast = $parser->parse(new Source(' 166 | { 167 | __typename 168 | name 169 | pets { 170 | __typename 171 | ... on Dog { 172 | name 173 | barks 174 | } 175 | ... on Cat { 176 | name 177 | meows 178 | } 179 | } 180 | } 181 | ')); 182 | 183 | $expected = [ 184 | 'data' => [ 185 | '__typename' => 'Person', 186 | 'name' => 'John', 187 | 'pets' => [ 188 | ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => FALSE], 189 | ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => TRUE], 190 | ], 191 | ], 192 | ]; 193 | 194 | $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); 195 | } 196 | 197 | public function testExecutesUsingInterfaceTypes() 198 | { 199 | $parser = new Parser(); 200 | $ast = $parser->parse(new Source(' 201 | { 202 | __typename 203 | name 204 | friends { 205 | __typename 206 | name 207 | barks 208 | meows 209 | } 210 | } 211 | ')); 212 | 213 | $expected = [ 214 | 'data' => [ 215 | '__typename' => 'Person', 216 | 'name' => 'John', 217 | 'friends' => [ 218 | ['__typename' => 'Person', 'name' => 'Liz'], 219 | ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => TRUE] 220 | ], 221 | ], 222 | ]; 223 | 224 | $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); 225 | } 226 | 227 | public function testExecutesInterfaceTypesWithInlineFragments() 228 | { 229 | $parser = new Parser(); 230 | $ast = $parser->parse(new Source(' 231 | { 232 | __typename 233 | name 234 | friends { 235 | __typename 236 | name 237 | ... on Dog { 238 | barks 239 | } 240 | ... on Cat { 241 | meows 242 | } 243 | } 244 | } 245 | ')); 246 | 247 | $expected = [ 248 | 'data' => [ 249 | '__typename' => 'Person', 250 | 'name' => 'John', 251 | 'friends' => [ 252 | ['__typename' => 'Person', 'name' => 'Liz'], 253 | ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => TRUE], 254 | ], 255 | ], 256 | ]; 257 | 258 | $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); 259 | } 260 | 261 | public function testAllowsFragmentConditionsToBeAbstractTypes() 262 | { 263 | $parser = new Parser(); 264 | $ast = $parser->parse(new Source(' 265 | { 266 | __typename 267 | name 268 | pets { ...PetFields } 269 | friends { ...FriendFields } 270 | } 271 | 272 | fragment PetFields on Pet { 273 | __typename 274 | ... on Dog { 275 | name 276 | barks 277 | } 278 | ... on Cat { 279 | name 280 | meows 281 | } 282 | } 283 | 284 | fragment FriendFields on Named { 285 | __typename 286 | name 287 | ... on Dog { 288 | barks 289 | } 290 | ... on Cat { 291 | meows 292 | } 293 | } 294 | ')); 295 | 296 | $expected = [ 297 | 'data' => [ 298 | '__typename' => 'Person', 299 | 'name' => 'John', 300 | 'pets' => [ 301 | ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => FALSE], 302 | ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => TRUE], 303 | ], 304 | 'friends' => [ 305 | ['__typename' => 'Person', 'name' => 'Liz'], 306 | ['__typename' => 'Dog', 'name' => 'Odie', 'barks' => TRUE], 307 | ], 308 | ], 309 | ]; 310 | 311 | $this->assertEquals($expected, Executor::execute($this->schema, $this->john, $ast)); 312 | } 313 | } 314 | 315 | class Dog 316 | { 317 | public $name; 318 | public $barks; 319 | 320 | function __construct($name, $barks) 321 | { 322 | $this->name = $name; 323 | $this->barks = $barks; 324 | } 325 | } 326 | 327 | class Cat 328 | { 329 | public $name; 330 | public $meows; 331 | 332 | function __construct($name, $meows) 333 | { 334 | $this->name = $name; 335 | $this->meows = $meows; 336 | } 337 | } 338 | 339 | class Person 340 | { 341 | public $name; 342 | public $pets; 343 | public $friends; 344 | 345 | function __construct($name, $pets = NULL, $friends = NULL) 346 | { 347 | $this->name = $name; 348 | $this->pets = $pets; 349 | $this->friends = $friends; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /tests/Language/LexerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $this->lexOne($body)); 17 | } 18 | 19 | public function skipsWhitespacesProvider() 20 | { 21 | $body1 = ' 22 | 23 | foo 24 | 25 | 26 | '; 27 | 28 | $body2 = ' 29 | #comment 30 | foo#comment 31 | '; 32 | 33 | $body3 = ',,,foo,,,'; 34 | 35 | return [ 36 | [$body1, new Token(Token::NAME_TYPE, 6, 9, 'foo')], 37 | [$body2, new Token(Token::NAME_TYPE, 18, 21, 'foo')], 38 | [$body3, new Token(Token::NAME_TYPE, 3, 6, 'foo')], 39 | ]; 40 | } 41 | 42 | public function testErrorsRespectWhitespace() 43 | { 44 | // @todo Implement this after porting exceptions. 45 | } 46 | 47 | /** 48 | * @dataProvider lexesStringsProvider() 49 | */ 50 | public function testLexesStrings($body, $expected) 51 | { 52 | $this->assertEquals($expected, $this->lexOne($body)); 53 | } 54 | 55 | public function lexesStringsProvider() 56 | { 57 | return [ 58 | ['"simple"', new Token(Token::STRING_TYPE, 0, 8, 'simple')], 59 | ['"quote \\""', new Token(Token::STRING_TYPE, 0, 10, 'quote "')], 60 | ['" white space "', new Token(Token::STRING_TYPE, 0, 15, ' white space ')], 61 | ['"escaped \\n\\r\\b\\t\\f"', new Token(Token::STRING_TYPE, 0, 20, 'escaped \n\r\b\t\f')], 62 | ['"slashes \\\\ \\/"', new Token(Token::STRING_TYPE, 0, 15, 'slashes \\ \/')], 63 | ['"unicode яуц"', new Token(Token::STRING_TYPE, 0, 13, 'unicode яуц')], 64 | ['"unicode \u1234\u5678\u90AB\uCDEF"', new Token(Token::STRING_TYPE, 0, 34, 'unicode ሴ噸邫췯')], 65 | ]; 66 | } 67 | 68 | public function testLexReportsUsefulStringErrors() 69 | { 70 | // @todo Implement this after porting exceptions. 71 | } 72 | 73 | /** 74 | * @dataProvider lexesNumbersProvider() 75 | */ 76 | public function testLexesNumbers($body, $expected) 77 | { 78 | $this->assertEquals($expected, $this->lexOne($body)); 79 | } 80 | 81 | public function lexesNumbersProvider() 82 | { 83 | return [ 84 | ['"simple"', new Token(Token::STRING_TYPE, 0, 8, 'simple')], 85 | ['" white space "', new Token(Token::STRING_TYPE, 0, 15, ' white space ')], 86 | ['"escaped \\n\\r\\b\\t\\f"', new Token(Token::STRING_TYPE, 0, 20, 'escaped \n\r\b\t\f')], 87 | ['"slashes \\\\ \\/"', new Token(Token::STRING_TYPE, 0, 15, 'slashes \\ \/')], 88 | ['"unicode \\u1234\\u5678\\u90AB\\uCDEF"', new Token(Token::STRING_TYPE, 0, 34, 'unicode ሴ噸邫췯')], 89 | ['4', new Token(Token::INT_TYPE, 0, 1, '4')], 90 | ['4.123', new Token(Token::FLOAT_TYPE, 0, 5, '4.123')], 91 | ['-4', new Token(Token::INT_TYPE, 0, 2, '-4')], 92 | ['9', new Token(Token::INT_TYPE, 0, 1, '9')], 93 | ['0', new Token(Token::INT_TYPE, 0, 1, '0')], 94 | ['00', new Token(Token::INT_TYPE, 0, 1, '0')], 95 | ['-4.123', new Token(Token::FLOAT_TYPE, 0, 6, '-4.123')], 96 | ['0.123', new Token(Token::FLOAT_TYPE, 0, 5, '0.123')], 97 | ['-1.123e4', new Token(Token::FLOAT_TYPE, 0, 8, '-1.123e4')], 98 | ['-1.123e-4', new Token(Token::FLOAT_TYPE, 0, 9, '-1.123e-4')], 99 | ['-1.123e4567', new Token(Token::FLOAT_TYPE, 0, 11, '-1.123e4567')], 100 | ]; 101 | } 102 | 103 | public function testReportsUsefulNumberErrors() 104 | { 105 | // @todo Implement this after porting exceptions. 106 | } 107 | 108 | /** 109 | * @dataProvider lexesPunctuationProvider() 110 | */ 111 | public function testLexesPunctuation($body, $expected) 112 | { 113 | $this->assertEquals($expected, $this->lexOne($body)); 114 | } 115 | 116 | public function lexesPunctuationProvider() 117 | { 118 | return [ 119 | ['!', new Token(Token::BANG_TYPE, 0, 1, NULL)], 120 | ['$', new Token(Token::DOLLAR_TYPE, 0, 1, NULL)], 121 | ['(', new Token(Token::PAREN_L_TYPE, 0, 1, NULL)], 122 | [')', new Token(Token::PAREN_R_TYPE, 0, 1, NULL)], 123 | ['...', new Token(Token::SPREAD_TYPE, 0, 3, NULL)], 124 | [':', new Token(Token::COLON_TYPE, 0, 1, NULL)], 125 | ['=', new Token(Token::EQUALS_TYPE, 0, 1, NULL)], 126 | ['@', new Token(Token::AT_TYPE, 0, 1, NULL)], 127 | ['[', new Token(Token::BRACKET_L_TYPE, 0, 1, NULL)], 128 | [']', new Token(Token::BRACKET_R_TYPE, 0, 1, NULL)], 129 | ['{', new Token(Token::BRACE_L_TYPE, 0, 1, NULL)], 130 | ['}', new Token(Token::BRACE_R_TYPE, 0, 1, NULL)], 131 | ['|', new Token(Token::PIPE_TYPE, 0, 1, NULL)], 132 | ]; 133 | } 134 | 135 | public function testReportsUsefulUnknownCharErrors() 136 | { 137 | // @todo Implement this after porting exceptions. 138 | } 139 | 140 | /** 141 | * @param string $body 142 | * 143 | * @return \Fubhy\GraphQL\Language\Token 144 | */ 145 | protected function lexOne($body) 146 | { 147 | $lexer = new Lexer(new Source($body)); 148 | return $lexer->readToken(0); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/Language/ParserTest.php: -------------------------------------------------------------------------------- 1 | parse(new Source($source)); 33 | } 34 | 35 | public function testParsesConstantDefaultValues() 36 | { 37 | // @todo Implement this after porting exceptions. 38 | } 39 | 40 | public function testDuplicateKeysInInputObjectIsSyntaxError() 41 | { 42 | // @todo Implement this after porting exceptions. 43 | } 44 | 45 | public function testParsesKitchenSink() 46 | { 47 | $source = file_get_contents(__DIR__ . '/kitchen-sink.graphql'); 48 | $parser = new Parser(); 49 | $parser->parse(new Source($source)); 50 | } 51 | 52 | public function testParseCreatesAst() 53 | { 54 | $source = new Source(' 55 | { 56 | node(id: 4) { 57 | id, 58 | name 59 | } 60 | } 61 | '); 62 | 63 | $parser = new Parser(); 64 | $result = $parser->parse($source); 65 | 66 | $expected = new Document([ 67 | new OperationDefinition('query', NULL, [], [], 68 | new SelectionSet([ 69 | new Field( 70 | new Name('node', new Location(31, 35, $source)), NULL, [ 71 | new Argument( 72 | new Name('id', new Location(36, 38, $source)), 73 | new IntValue('4', new Location(40, 41, $source)), 74 | new Location(36, 41, $source) 75 | ) 76 | ], [], 77 | new SelectionSet( 78 | [ 79 | new Field( 80 | new Name('id', new Location(65, 67, $source)), NULL, [], [], NULL, 81 | new Location(65, 67, $source) 82 | ), 83 | new Field( 84 | new Name('name', new Location(89, 93, $source)), NULL, [], [], NULL, 85 | new Location(89, 93, $source) 86 | ), 87 | ], new Location(43, 111, $source)), 88 | new Location(31, 111, $source)) 89 | ], new Location(13, 125, $source) 90 | ), new Location(13, 125, $source)) 91 | ], new Location(13, 134, $source) 92 | ); 93 | 94 | $this->assertEquals($expected, $result); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Language/kitchen-sink.graphql: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Facebook, Inc. 2 | # All rights reserved. 3 | # 4 | # This source code is licensed under the BSD-style license found in the 5 | # LICENSE file in the root directory of this source tree. An additional grant 6 | # of patent rights can be found in the PATENTS file in the same directory. 7 | 8 | query queryName($foo: ComplexType, $site: Site = MOBILE) { 9 | whoever123is: node(id: [123, 456]) { 10 | id , 11 | ... on User @defer { 12 | field2 { 13 | id , 14 | alias: field1(first:10, after:$foo,) @include(if: $foo) { 15 | id, 16 | ...frag 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | mutation likeStory { 24 | like(story: 123) @defer { 25 | story { 26 | id 27 | } 28 | } 29 | } 30 | 31 | fragment frag on Friend { 32 | foo(size: $size, bar: $b, obj: {key: "value"}) 33 | } 34 | 35 | { 36 | unnamed(truthy: TRUE, FALSEy: FALSE), 37 | query 38 | } 39 | -------------------------------------------------------------------------------- /tests/StarWarsData.php: -------------------------------------------------------------------------------- 1 | '1000', 18 | 'name' => 'Luke Skywalker', 19 | 'friends' => ['1002', '1003', '2000', '2001'], 20 | 'appearsIn' => [4, 5, 6], 21 | 'homePlanet' => 'Tatooine', 22 | ]; 23 | } 24 | 25 | protected function getDarthVader() 26 | { 27 | return [ 28 | 'id' => '1001', 29 | 'name' => 'Darth Vader', 30 | 'friends' => ['1004'], 31 | 'appearsIn' => [4, 5, 6], 32 | 'homePlanet' => 'Tatooine', 33 | ]; 34 | } 35 | 36 | protected function getHanSolo() 37 | { 38 | return [ 39 | 'id' => '1002', 40 | 'name' => 'Han Solo', 41 | 'friends' => ['1000', '1003', '2001'], 42 | 'appearsIn' => [4, 5, 6], 43 | ]; 44 | } 45 | 46 | protected function getLeiaOrgana() 47 | { 48 | return [ 49 | 'id' => '1003', 50 | 'name' => 'Leia Organa', 51 | 'friends' => ['1000', '1002', '2000', '2001'], 52 | 'appearsIn' => [4, 5, 6], 53 | 'homePlanet' => 'Alderaan', 54 | ]; 55 | } 56 | 57 | protected function getWilhuffTarkin() 58 | { 59 | return [ 60 | 'id' => '1004', 61 | 'name' => 'Wilhuff Tarkin', 62 | 'friends' => ['1001'], 63 | 'appearsIn' => [4], 64 | ]; 65 | } 66 | 67 | 68 | protected function getThreePiO() 69 | { 70 | return [ 71 | 'id' => '2000', 72 | 'name' => 'C-3PO', 73 | 'friends' => ['1000', '1002', '1003', '2001'], 74 | 'appearsIn' => [4, 5, 6], 75 | 'primaryFunction' => 'Protocol', 76 | ]; 77 | } 78 | 79 | protected function getArtoo() 80 | { 81 | return [ 82 | 'id' => '2001', 83 | 'name' => 'R2-D2', 84 | 'friends' => ['1000', '1002', '1003'], 85 | 'appearsIn' => [4, 5, 6], 86 | 'primaryFunction' => 'Astromech', 87 | ]; 88 | } 89 | 90 | protected function getHumans() 91 | { 92 | return [ 93 | '1000' => $this->getLukeSkyWalker(), 94 | '1001' => $this->getDarthVader(), 95 | '1002' => $this->getHanSolo(), 96 | '1003' => $this->getLeiaOrgana(), 97 | '1004' => $this->getWilhuffTarkin(), 98 | ]; 99 | } 100 | 101 | protected function getDroids() 102 | { 103 | return [ 104 | '2000' => $this->getThreePiO(), 105 | '2001' => $this->getArtoo(), 106 | ]; 107 | } 108 | 109 | /** 110 | * Helper function to get a character by ID. 111 | * 112 | * @param string $id 113 | * 114 | * @return array|null 115 | */ 116 | protected function getStarWarsCharacter($id) 117 | { 118 | // The original implementation returns a promise here. That obviously 119 | // doesn't make sense for PHP. 120 | $humans = $this->getHumans(); 121 | if (isset($humans[$id])) { 122 | return $humans[$id]; 123 | } 124 | 125 | $droids = $this->getDroids(); 126 | if (isset($droids[$id])) { 127 | return $droids[$id]; 128 | } 129 | 130 | return NULL; 131 | } 132 | 133 | /** 134 | * Allows us to query for a character's friends. 135 | * 136 | * @param string $character 137 | * 138 | * @return array 139 | */ 140 | protected function getStarWarsFriends($character) 141 | { 142 | return array_map([$this, 'getStarWarsCharacter'], $character['friends']); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/StarWarsQueryTest.php: -------------------------------------------------------------------------------- 1 | getStarWarsSchema(), $query); 21 | $this->assertEquals(['data' => $expected], $result); 22 | } 23 | 24 | /** 25 | * Helper function to test a query with params and the expected response. 26 | * 27 | * @param $query 28 | * @param $params 29 | * @param $expected 30 | */ 31 | protected function assertQueryWithParams($query, $params, $expected) 32 | { 33 | $result = GraphQL::execute($this->getStarWarsSchema(), $query, NULL, $params); 34 | $this->assertEquals(['data' => $expected], $result); 35 | } 36 | 37 | public function testCorrectlyIdentifiesR2D2AsTheHeroOfTheStarWarsSaga() 38 | { 39 | $query = ' 40 | query HeroNameQuery { 41 | hero { 42 | name 43 | } 44 | } 45 | '; 46 | 47 | $expected = [ 48 | 'hero' => [ 49 | 'name' => 'R2-D2' 50 | ] 51 | ]; 52 | 53 | $this->assertQuery($query, $expected); 54 | } 55 | 56 | public function testAllowsUsToQueryForTheIDAndFriendsOfR2D2() 57 | { 58 | $query = ' 59 | query HeroNameAndFriendsQuery { 60 | hero { 61 | id 62 | name 63 | friends { 64 | name 65 | } 66 | } 67 | } 68 | '; 69 | 70 | $expected = [ 71 | 'hero' => [ 72 | 'id' => '2001', 73 | 'name' => 'R2-D2', 74 | 'friends' => [ 75 | ['name' => 'Luke Skywalker'], 76 | ['name' => 'Han Solo'], 77 | ['name' => 'Leia Organa'], 78 | ], 79 | ], 80 | ]; 81 | 82 | $this->assertQuery($query, $expected); 83 | } 84 | 85 | public function testAllowsUsToQueryForTheFriendsOfFriendsOfR2D2() 86 | { 87 | $query = ' 88 | query NestedQuery { 89 | hero { 90 | name 91 | friends { 92 | name 93 | appearsIn 94 | friends { 95 | name 96 | } 97 | } 98 | } 99 | } 100 | '; 101 | 102 | $expected = [ 103 | 'hero' => [ 104 | 'name' => 'R2-D2', 105 | 'friends' => [ 106 | [ 107 | 'name' => 'Luke Skywalker', 108 | 'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'], 109 | 'friends' => [ 110 | ['name' => 'Han Solo'], 111 | ['name' => 'Leia Organa'], 112 | ['name' => 'C-3PO'], 113 | ['name' => 'R2-D2'], 114 | ], 115 | ], 116 | [ 117 | 'name' => 'Han Solo', 118 | 'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'], 119 | 'friends' => [ 120 | ['name' => 'Luke Skywalker'], 121 | ['name' => 'Leia Organa'], 122 | ['name' => 'R2-D2'], 123 | ], 124 | ], 125 | [ 126 | 'name' => 'Leia Organa', 127 | 'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'], 128 | 'friends' => [ 129 | ['name' => 'Luke Skywalker'], 130 | ['name' => 'Han Solo'], 131 | ['name' => 'C-3PO'], 132 | ['name' => 'R2-D2'], 133 | ], 134 | ], 135 | ], 136 | ], 137 | ]; 138 | 139 | $this->assertQuery($query, $expected); 140 | } 141 | 142 | public function testAllowsUsToQueryForLukeSkywalkerDirectlyUsingHisId() 143 | { 144 | $query = ' 145 | query FetchLukeQuery { 146 | human(id: "1000") { 147 | name 148 | } 149 | } 150 | '; 151 | 152 | $expected = [ 153 | 'human' => [ 154 | 'name' => 'Luke Skywalker', 155 | ] 156 | ]; 157 | 158 | $this->assertQuery($query, $expected); 159 | } 160 | 161 | public function testAllowsUsToCreateAGenericQueryThenUseItToFetchLukeSkywalkerUsingHisId() 162 | { 163 | $query = ' 164 | query FetchSomeIDQuery($someId: String!) { 165 | human(id: $someId) { 166 | name 167 | } 168 | } 169 | '; 170 | 171 | $params = [ 172 | 'someId' => '1000', 173 | ]; 174 | 175 | $expected = [ 176 | 'human' => [ 177 | 'name' => 'Luke Skywalker', 178 | ], 179 | ]; 180 | 181 | $this->assertQueryWithParams($query, $params, $expected); 182 | } 183 | 184 | public function testAllowsUsToCreateAGenericQueryThenUseItToFetchHanSoloUsingHisId() 185 | { 186 | $query = ' 187 | query FetchSomeIDQuery($someId: String!) { 188 | human(id: $someId) { 189 | name 190 | } 191 | } 192 | '; 193 | 194 | $params = [ 195 | 'someId' => '1002', 196 | ]; 197 | 198 | $expected = [ 199 | 'human' => [ 200 | 'name' => 'Han Solo', 201 | ], 202 | ]; 203 | 204 | $this->assertQueryWithParams($query, $params, $expected); 205 | } 206 | 207 | public function testAllowsUsToCreateAGenericQueryThenPassAnInvalidIdToGetNullBack() 208 | { 209 | $query = ' 210 | query humanQuery($id: String!) { 211 | human(id: $id) { 212 | name 213 | } 214 | } 215 | '; 216 | 217 | $params = [ 218 | 'id' => 'not a valid id', 219 | ]; 220 | 221 | $expected = [ 222 | 'human' => NULL 223 | ]; 224 | 225 | $this->assertQueryWithParams($query, $params, $expected); 226 | } 227 | 228 | public function testAllowsUsToQueryForLukeChangingHisKeyWithAnAlias() 229 | { 230 | $query = ' 231 | query FetchLukeAliased { 232 | luke: human(id: "1000") { 233 | name 234 | } 235 | } 236 | '; 237 | 238 | $expected = [ 239 | 'luke' => [ 240 | 'name' => 'Luke Skywalker', 241 | ], 242 | ]; 243 | 244 | $this->assertQuery($query, $expected); 245 | } 246 | 247 | public function testAllowsUsToQueryForBothLukeAndLeiaUsingTwoRootFieldsAndAnAlias() 248 | { 249 | $query = ' 250 | query FetchLukeAndLeiaAliased { 251 | luke: human(id: "1000") { 252 | name 253 | } 254 | leia: human(id: "1003") { 255 | name 256 | } 257 | } 258 | '; 259 | 260 | $expected = [ 261 | 'luke' => [ 262 | 'name' => 'Luke Skywalker', 263 | ], 264 | 'leia' => [ 265 | 'name' => 'Leia Organa', 266 | ], 267 | ]; 268 | 269 | $this->assertQuery($query, $expected); 270 | } 271 | 272 | public function testAllowsUsToQueryUsingDuplicatedContent() 273 | { 274 | $query = ' 275 | query DuplicateFields { 276 | luke: human(id: "1000") { 277 | name 278 | homePlanet 279 | } 280 | leia: human(id: "1003") { 281 | name 282 | homePlanet 283 | } 284 | } 285 | '; 286 | 287 | $expected = [ 288 | 'luke' => [ 289 | 'name' => 'Luke Skywalker', 290 | 'homePlanet' => 'Tatooine', 291 | ], 292 | 'leia' => [ 293 | 'name' => 'Leia Organa', 294 | 'homePlanet' => 'Alderaan', 295 | ], 296 | ]; 297 | 298 | $this->assertQuery($query, $expected); 299 | } 300 | 301 | public function testAllowsUsToUseAFragmentToAvoidDuplicatingContent() 302 | { 303 | $query = ' 304 | query UseFragment { 305 | luke: human(id: "1000") { 306 | ...HumanFragment 307 | } 308 | leia: human(id: "1003") { 309 | ...HumanFragment 310 | } 311 | } 312 | 313 | fragment HumanFragment on Human { 314 | name 315 | homePlanet 316 | } 317 | '; 318 | 319 | $expected = [ 320 | 'luke' => [ 321 | 'name' => 'Luke Skywalker', 322 | 'homePlanet' => 'Tatooine', 323 | ], 324 | 'leia' => [ 325 | 'name' => 'Leia Organa', 326 | 'homePlanet' => 'Alderaan', 327 | ], 328 | ]; 329 | 330 | $this->assertQuery($query, $expected); 331 | } 332 | 333 | public function testAllowsUsToVerifyThatR2D2IsADroid() 334 | { 335 | $query = ' 336 | query CheckTypeOfR2 { 337 | hero { 338 | __typename 339 | name 340 | } 341 | } 342 | '; 343 | 344 | $expected = [ 345 | 'hero' => [ 346 | '__typename' => 'Droid', 347 | 'name' => 'R2-D2' 348 | ], 349 | ]; 350 | 351 | $this->assertQuery($query, $expected); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /tests/StarWarsSchema.php: -------------------------------------------------------------------------------- 1 | [ 75 | 'value' => 4, 76 | 'description' => 'Released in 1977.', 77 | ], 78 | 'EMPIRE' => [ 79 | 'value' => 5, 80 | 'description' => 'Released in 1980.', 81 | ], 82 | 'JEDI' => [ 83 | 'value' => 6, 84 | 'description' => 'Released in 1983.', 85 | ], 86 | ], 'One of the films in the Star Wars Trilogy'); 87 | 88 | $humanType = NULL; 89 | $droidType = NULL; 90 | 91 | /** 92 | * Characters in the Star Wars trilogy are either humans or droids. 93 | * 94 | * This implements the following type system shorthand: 95 | * interface Character { 96 | * id: String! 97 | * name: String 98 | * friends: [Character] 99 | * appearsIn: [Episode] 100 | * } 101 | */ 102 | $characterInterface = new InterfaceType('Character', [ 103 | 'id' => [ 104 | 'type' => new NonNullModifier(Type::stringType()), 105 | 'description' => 'The id of the character.', 106 | ], 107 | 'name' => [ 108 | 'type' => Type::stringType(), 109 | 'description' => 'The name of the character.', 110 | ], 111 | 'friends' => [ 112 | 'type' => function () use (&$characterInterface) { 113 | return new ListModifier($characterInterface); 114 | }, 115 | 'description' => 'The friends of the character, or an empty list if they have none.', 116 | ], 117 | 'appearsIn' => [ 118 | 'type' => new ListModifier($episodeEnum), 119 | 'description' => 'Which movies they appear in.', 120 | ], 121 | ], function ($obj) use (&$humanType, &$droidType) { 122 | $humans = $this->getHumans(); 123 | if (isset($humans[$obj['id']])) { 124 | return $humanType; 125 | } 126 | 127 | $droids = $this->getDroids(); 128 | if (isset($droids[$obj['id']])) { 129 | return $droidType; 130 | } 131 | 132 | return NULL; 133 | }, 'A character in the Star Wars Trilogy'); 134 | 135 | /** 136 | * We define our human type, which implements the character interface. 137 | * 138 | * This implements the following type system shorthand: 139 | * type Human : Character { 140 | * id: String! 141 | * name: String 142 | * friends: [Character] 143 | * appearsIn: [Episode] 144 | * } 145 | */ 146 | $humanType = new ObjectType('Human', [ 147 | 'id' => [ 148 | 'type' => new NonNullModifier(Type::stringType()), 149 | 'description' => 'The id of the human.', 150 | ], 151 | 'name' => [ 152 | 'type' => Type::stringType(), 153 | 'description' => 'The name of the human.', 154 | ], 155 | 'friends' => [ 156 | 'type' => new ListModifier($characterInterface), 157 | 'description' => 'The friends of the human, or an empty list if they have none.', 158 | 'resolve' => function ($human) { 159 | return $this->getStarWarsFriends($human); 160 | }, 161 | ], 162 | 'appearsIn' => [ 163 | 'type' => new ListModifier($episodeEnum), 164 | 'description' => 'Which movies they appear in.', 165 | ], 166 | 'homePlanet' => [ 167 | 'type' => Type::stringType(), 168 | 'description' => 'The home planet of the human, or null if unknown.', 169 | ], 170 | ], [$characterInterface], NULL, 'A humanoid creature in the Star Wars universe.'); 171 | 172 | /** 173 | * The other type of character in Star Wars is a droid. 174 | * 175 | * This implements the following type system shorthand: 176 | * type Droid : Character { 177 | * id: String! 178 | * name: String 179 | * friends: [Character] 180 | * appearsIn: [Episode] 181 | * primaryFunction: String 182 | * } 183 | */ 184 | $droidType = new ObjectType('Droid', [ 185 | 'id' => [ 186 | 'type' => new NonNullModifier(Type::stringType()), 187 | 'description' => 'The id of the droid.', 188 | ], 189 | 'name' => [ 190 | 'type' => Type::stringType(), 191 | 'description' => 'The name of the droid.', 192 | ], 193 | 'friends' => [ 194 | 'type' => new ListModifier($characterInterface), 195 | 'description' => 'The friends of the droid, or an empty list if they have none.', 196 | 'resolve' => function ($droid) { 197 | return $this->getStarWarsFriends($droid); 198 | }, 199 | ], 200 | 'appearsIn' => [ 201 | 'type' => new ListModifier($episodeEnum), 202 | 'description' => 'Which movies they appear in.', 203 | ], 204 | 'primaryFunction' => [ 205 | 'type' => Type::stringType(), 206 | 'description' => 'The primary function of the droid.', 207 | ] 208 | ], [$characterInterface], NULL, 'A mechanical creature in the Star Wars universe.'); 209 | 210 | /** 211 | * This is the type that will be the root of our query, and the 212 | * entry point into our schema. It gives us the ability to fetch 213 | * objects by their IDs, as well as to fetch the undisputed hero 214 | * of the Star Wars trilogy, R2-D2, directly. 215 | * 216 | * This implements the following type system shorthand: 217 | * type Query { 218 | * hero: Character 219 | * human(id: String!): Human 220 | * droid(id: String!): Droid 221 | * } 222 | * 223 | */ 224 | $queryType = new ObjectType('Query', [ 225 | 'hero' => [ 226 | 'type' => $characterInterface, 227 | 'args' => [ 228 | 'episode' => [ 229 | 'description' => 'If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.', 230 | 'type' => $episodeEnum 231 | ], 232 | ], 233 | 'resolve' => function () { 234 | return $this->getArtoo(); 235 | }, 236 | ], 237 | 'human' => [ 238 | 'type' => $humanType, 239 | 'args' => [ 240 | 'id' => [ 241 | 'name' => 'id', 242 | 'description' => 'The id of the human.', 243 | 'type' => new NonNullModifier(Type::stringType()), 244 | ], 245 | ], 246 | 'resolve' => function ($root, array $args) { 247 | $humans = $this->getHumans(); 248 | return isset($humans[$args['id']]) ? $humans[$args['id']] : NULL; 249 | } 250 | ], 251 | 'droid' => [ 252 | 'type' => $droidType, 253 | 'args' => [ 254 | 'id' => [ 255 | 'name' => 'id', 256 | 'description' => 'The id of the droid.', 257 | 'type' => new NonNullModifier(Type::stringType()), 258 | ], 259 | ], 260 | 'resolve' => function ($root, array $args) { 261 | $droids = $this->getDroids(); 262 | return isset($droids[$args['id']]) ? $droids[$args['id']] : NULL; 263 | } 264 | ] 265 | ]); 266 | 267 | return new Schema($queryType); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /tests/Type/CoercionTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, Type::intType()->coerce($input)); 18 | } 19 | 20 | public function coercesOutputIntProvider() 21 | { 22 | return [ 23 | [1, 1], 24 | [0, 0], 25 | [-1, -1], 26 | [0.1, 0], 27 | [1.1, 1], 28 | [-1.1, -1], 29 | [1e5, 100000], 30 | [9876504321, 9876504321], 31 | [-9876504321, -9876504321], 32 | [1e100, NULL], 33 | [-1e100, NULL], 34 | ['-1.1', -1], 35 | ['one', NULL], 36 | [FALSE, 0], 37 | [TRUE, 1], 38 | ]; 39 | } 40 | 41 | /** 42 | * @dataProvider coercesOutputFloatProvider 43 | * 44 | * @param mixed $input 45 | * @param float|null $expected 46 | */ 47 | public function testCoercesOutputFloat($input, $expected) 48 | { 49 | $this->assertSame($expected, Type::floatType()->coerce($input)); 50 | } 51 | 52 | public function coercesOutputFloatProvider() 53 | { 54 | return [ 55 | [1, 1.0], 56 | [0, 0.0], 57 | [-1, -1.0], 58 | [0.1, 0.1], 59 | [1.1, 1.1], 60 | [-1.1, -1.1], 61 | ['-1.1', -1.1], 62 | [FALSE, 0.0], 63 | [TRUE, 1.0], 64 | ]; 65 | } 66 | 67 | /** 68 | * @dataProvider coercesOutputStringProvider 69 | * 70 | * @param mixed $input 71 | * @param float|null $expected 72 | */ 73 | public function testCoercesOutputString($input, $expected) 74 | { 75 | $this->assertSame($expected, Type::stringType()->coerce($input)); 76 | } 77 | 78 | public function coercesOutputStringProvider() { 79 | return [ 80 | ['string', 'string'], 81 | [1, '1'], 82 | [-1.1, '-1.1'], 83 | [TRUE, 'true'], 84 | [FALSE, 'false'], 85 | ]; 86 | } 87 | 88 | /** 89 | * @dataProvider coercesOutputBooleanProvider 90 | * 91 | * @param mixed $input 92 | * @param float|null $expected 93 | */ 94 | public function testCoercesOutputBoolean($input, $expected) 95 | { 96 | $this->assertSame($expected, Type::booleanType()->coerce($input)); 97 | } 98 | 99 | public function coercesOutputBooleanProvider() 100 | { 101 | return [ 102 | ['string', TRUE], 103 | ['', FALSE], 104 | [1, TRUE], 105 | [0, FALSE], 106 | [TRUE, TRUE], 107 | [FALSE, FALSE], 108 | ]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Type/DefinitionTest.php: -------------------------------------------------------------------------------- 1 | blogImage = new ObjectType('Image', [ 46 | 'url' => ['type' => Type::stringType()], 47 | 'width' => ['type' => Type::intType()], 48 | 'height' => ['type' => Type::intType()], 49 | ]); 50 | 51 | $this->blogAuthor = new ObjectType('Author', [ 52 | 'id' => ['type' => Type::stringType()], 53 | 'name' => ['type' => Type::stringType()], 54 | 'pic' => [ 55 | 'args' => [ 56 | 'width' => ['type' => Type::intType()], 57 | 'height' => ['type' => Type::intType()], 58 | ], 59 | 'type' => $this->blogImage, 60 | ], 61 | 'recentArticle' => ['type' => function () { 62 | return $this->blogArticle; 63 | }], 64 | ]); 65 | 66 | $this->blogArticle = new ObjectType('Article', [ 67 | 'id' => ['type' => Type::stringType()], 68 | 'isPublished' => ['type' => Type::booleanType()], 69 | 'author' => ['type' => $this->blogAuthor], 70 | 'title' => ['type' => Type::stringType()], 71 | 'body' => ['type' => Type::stringType()], 72 | ]); 73 | 74 | $this->blogQuery = new ObjectType('Query', [ 75 | 'article' => [ 76 | 'args' => [ 77 | 'id' => ['type' => Type::stringType()], 78 | ], 79 | 'type' => $this->blogArticle, 80 | ], 81 | 'feed' => ['type' => new ListModifier($this->blogArticle)], 82 | ]); 83 | 84 | $this->blogMutation = new ObjectType('Mutation', [ 85 | 'writeArticle' => ['type' => $this->blogArticle], 86 | ]); 87 | } 88 | 89 | public function testDefinesAQueryOnlySchema() 90 | { 91 | $blogSchema = new Schema($this->blogQuery); 92 | $this->assertSame($this->blogQuery, $blogSchema->getQueryType()); 93 | 94 | /** @var \Fubhy\GraphQL\Type\Definition\FieldDefinition $articleField */ 95 | /** @var \Fubhy\GraphQL\Type\Definition\Types\ObjectType $articleFieldType */ 96 | $articleField = $this->blogQuery->getFields()['article']; 97 | $articleFieldType = $articleField->getType(); 98 | $this->assertEquals('article', $articleField->getName()); 99 | $this->assertSame($this->blogArticle, $articleFieldType); 100 | $this->assertEquals('Article', $articleFieldType->getName()); 101 | 102 | /** @var \Fubhy\GraphQL\Type\Definition\FieldDefinition $titleField */ 103 | /** @var \Fubhy\GraphQL\Type\Definition\Types\ObjectType $titleFieldType */ 104 | $titleField = $articleFieldType->getFields()['title']; 105 | $titleFieldType = $titleField->getType(); 106 | $this->assertEquals('title', $titleField->getName()); 107 | $this->assertEquals(Type::stringType(), $titleFieldType); 108 | $this->assertEquals('String', $titleFieldType->getName()); 109 | 110 | /** @var \Fubhy\GraphQL\Type\Definition\FieldDefinition $authorField */ 111 | /** @var \Fubhy\GraphQL\Type\Definition\Types\ObjectType $authorFieldType */ 112 | /** @var \Fubhy\GraphQL\Type\Definition\FieldDefinition $recentArticleField */ 113 | $authorField = $articleFieldType->getFields()['author']; 114 | $authorFieldType = $authorField->getType(); 115 | $recentArticleField = $authorFieldType->getFields()['recentArticle']; 116 | $this->assertSame($this->blogArticle, $recentArticleField->getType()); 117 | 118 | /** @var \Fubhy\GraphQL\Type\Definition\FieldDefinition $feedField */ 119 | /** @var \Fubhy\GraphQL\Type\Definition\Types\ListModifier $feedFieldType */ 120 | $feedField = $this->blogQuery->getFields()['feed']; 121 | $feedFieldType = $feedField->getType(); 122 | $this->assertEquals('feed', $feedField->getName()); 123 | $this->assertSame($this->blogArticle, $feedFieldType->getWrappedType()); 124 | } 125 | 126 | public function testDefinesAMutationSchema() 127 | { 128 | $blogSchema = new Schema($this->blogQuery, $this->blogMutation); 129 | $this->assertSame($this->blogMutation, $blogSchema->getMutationType()); 130 | 131 | /** @var \Fubhy\GraphQL\Type\Definition\FieldDefinition $writeMutation */ 132 | /** @var \Fubhy\GraphQL\Type\Definition\Types\ObjectType $writeMutationType */ 133 | $writeMutation = $this->blogMutation->getFields()['writeArticle']; 134 | $writeMutationType = $writeMutation->getType(); 135 | $this->assertEquals('writeArticle', $writeMutation->getName()); 136 | $this->assertSame($this->blogArticle, $writeMutationType); 137 | $this->assertEquals('Article', $writeMutationType->getName()); 138 | } 139 | 140 | public function testIncludesInterfacesSubtypesInTheTypeMap() 141 | { 142 | $someInterface = new InterfaceType('SomeInterface'); 143 | $someSubType = new ObjectType('SomeSubType', [], [$someInterface]); 144 | $schema = new Schema($someInterface); 145 | 146 | $this->assertSame($someSubType, $schema->getTypeMap()['SomeSubType']); 147 | } 148 | 149 | public function testStringifiesSimpleTypes() { 150 | $this->assertEquals('Int', (string) Type::intType()); 151 | $this->assertEquals('Article', (string) $this->blogArticle); 152 | $this->assertEquals('Interface', (string) new InterfaceType('Interface')); 153 | $this->assertEquals('Union', (string) new UnionType('Union')); 154 | $this->assertEquals('Enum', (string) new EnumType('Enum')); 155 | $this->assertEquals('InputObject', (string) new InputObjectType('InputObject')); 156 | $this->assertEquals('Int!', (string) new NonNullModifier(Type::intType())); 157 | $this->assertEquals('[Int]', (string) new ListModifier(Type::intType())); 158 | $this->assertEquals('[Int]!', (string) new NonNullModifier(new ListModifier(Type::intType()))); 159 | $this->assertEquals('[Int!]', (string) new ListModifier(new NonNullModifier(Type::intType()))); 160 | $this->assertEquals('[[Int]]', (string) new ListModifier(new ListModifier(Type::intType()))); 161 | } 162 | 163 | /** 164 | * @dataProvider identifiesInputTypesProvider 165 | * 166 | * @param \Fubhy\GraphQL\Type\Definition\Types\TypeInterface $type 167 | * @param $answer 168 | */ 169 | public function testIdentifiesInputTypes(TypeInterface $type, $answer) { 170 | $this->assertEquals($answer, $type->isInputType()); 171 | $this->assertEquals($answer, (new ListModifier($type))->isInputType()); 172 | $this->assertEquals($answer, (new NonNullModifier($type))->isInputType()); 173 | } 174 | 175 | public function identifiesInputTypesProvider() { 176 | return [ 177 | [Type::intType(), TRUE], 178 | [new ObjectType('Object'), FALSE], 179 | [new InterfaceType('Interface'), FALSE], 180 | [new UnionType('Union'), FALSE], 181 | [new EnumType('Enum'), TRUE], 182 | [new InputObjectType('InputObject'), TRUE], 183 | ]; 184 | } 185 | 186 | /** 187 | * @dataProvider identifiesOutputTypesProvider 188 | * 189 | * @param \Fubhy\GraphQL\Type\Definition\Types\TypeInterface $type 190 | * @param bool $answer 191 | */ 192 | public function testIdentifiesOutputTypes(TypeInterface $type, $answer) { 193 | $this->assertEquals($answer, $type->isOutputType($type)); 194 | $this->assertEquals($answer, (new ListModifier($type))->isOutputType()); 195 | $this->assertEquals($answer, (new NonNullModifier($type))->isOutputType()); 196 | } 197 | 198 | public function identifiesOutputTypesProvider() { 199 | return [ 200 | [Type::intType(), TRUE], 201 | [new ObjectType('Object'), TRUE], 202 | [new InterfaceType('Interface'), TRUE], 203 | [new UnionType('Union'), TRUE], 204 | [new EnumType('Enum'), TRUE], 205 | [new InputObjectType('InputObject'), FALSE], 206 | ]; 207 | } 208 | 209 | /** 210 | * @dataProvider prohibitsPuttingNonObjcetTypesInUnionsProvider 211 | * @expectedException \LogicException 212 | * @expectedExceptionMessageRegExp /Union BadUnion may only contain object types, it cannot contain: .+\./ 213 | * 214 | * @param \Fubhy\GraphQL\Type\Definition\Types\TypeInterface $type 215 | */ 216 | public function testProhibitsPuttingNonObjcetTypesInUnions(TypeInterface $type) { 217 | new UnionType('BadUnion', [$type]); 218 | } 219 | 220 | public function prohibitsPuttingNonObjcetTypesInUnionsProvider() { 221 | return [ 222 | [Type::intType()], 223 | [new NonNullModifier(Type::intType())], 224 | [new ListModifier(Type::intType())], 225 | [new InterfaceType('Interface')], 226 | [new UnionType('Union')], 227 | [new EnumType('Enum')], 228 | [new InputObjectType('InputObject')], 229 | ]; 230 | } 231 | } 232 | --------------------------------------------------------------------------------